Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
17 KiB
fix-aba v2 — closing review v1's MAJOR: the lanes cover the slot, a unit test covers the generation
Tree ~/w7/p2-fix-aba, branch p2/fix-aba. Two new commits on top of 55d2af9b
(== c52364cc byte for byte), not pushed:
| commit | subject |
|---|---|
9bd05ae4 |
[Fix, Test] (Magma, MG_Test): cover the {slot, gen} generation in a unit test that forces a real slot reuse, and stop claiming the ABA lanes do |
c290693d |
[Fix] (Magma, MG_IntegrationTest): drop the fourth ABA-control consumer, which was unreachable, and the scenario comment its own body contradicts |
Files: MG_Backend/DirectVulkan/Renderer/{MagmaPipeArms.h, VertexInputStateFactory.cpp, VulkanRenderer.cpp}, MG_IntegrationTest/{CMakeLists.txt, Scenarios/HandleRecycleScenario.cpp},
MG_Test/Pipe/{CMakeLists.txt, MagmaPipeIdentityTest.cpp} (new).
1. MAJOR 1 is accepted in full, and independently re-measured
The reviewer is right and the claim was mine to make correctly. AbaControlHandles does not
defeat the generation in {slot, gen}, and it cannot:
- Magma's mint has no death notification —
grep -rn NotifyStateObjectDestroyed MobileGL/MG_Backend/DirectVulkanis empty — so a slot returns to the free list only throughMagmaPipeIdentityTable::OnFrameBoundary's age sweep (kSweepInterval = 256,kRetireAgeBoundaries = 1024); AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInputissues fiveEndFrame()s, so the replacement VAO acquires against an empty free list and gets a brand-new slot atGen = 1. The knob-off FRESH verdict is decided by the slot;- a real reuse needs ≥ 1024 idle boundaries after the dead object's last draw, which puts the two
draws in different frames — and
ResolvedVertexBindings, the only memo carrying a GPU slice rather than a layout, declines across frames by design. The two requirements are mutually exclusive, so no same-frame pixel reproducer can ever reach the generation for this memo.
Re-measured here, not taken on trust. With ++m_entries[index].Gen (MagmaPipeArms.h:365)
commented out and build-push rebuilt:
ctest --test-dir build-push -R HandleRecycle -j 4 -> 100% tests passed, 0 failed out of 32
ctest --test-dir build-push -L unit -j 8 -> 99% tests passed, 3 failed out of 1566
All 32 integration entries — Handles, Legacy, AbaControl, AbaControlHandles, both
backends — stay green with the generation deleted. The three reds are three of the four cases
this round adds. That is the MAJOR, reproduced from the other side.
1.1 What the five sites say now
| site | now says |
|---|---|
commit message (9bd05ae4) |
states the correction, the mint's 256/1024 mechanics, the measured slot=2 gen=1 / slot=3 gen=1, and the 32/32-vs-1563/1566 re-measurement |
MagmaPipeArms.h (MagmaPipeAbaControlDefeatsIdentity) |
the constant defeats the object identity that SELECTS the slot — the key the handle arm ships — followed by a "WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN" block with the three bullets above and a pointer to the unit suite |
VertexInputStateFactory.cpp (MemosFor) |
"what the control defeats HERE is the identity that SELECTS the entry"; the generation half is named as the unit suite's business |
MG_IntegrationTest/CMakeLists.txt |
AbaControlHandles "defeats the object identity that SELECTS THE SLOT"; a new paragraph states that neither lane exercises the generation and no lane of this shape can |
| this file, §2 below | replaces v1 §2's "on the handle arm the constant defeats the GENERATION … which is the whole of what makes the re-keyed memos ABA-safe" |
Nothing about the lanes' behaviour changed — they were, and remain, a real control over the key the handle arm ships. Only the claim changed.
2. Where the generation IS expressible: MG_Test/Pipe/MagmaPipeIdentityTest.cpp
Remedy (b) of the review, not (a): the paragraph alone would have left the shipped ++Gen with
no test anywhere in the tree.
No test-only hook was needed. Driving the real sweep costs a 1280-iteration loop over
OnFrameBoundary() and runs in under a millisecond, so the suite exercises the production retire
path rather than a back door — no MOBILEGL_BUILD_TEST escape hatch, and nothing new in a
shipping build.
Where it lives, and why. Beside SlotAllocatorTest.cpp, which asserts the same identity
contract ("Gen moves on REUSE and never on respecify") for the client allocator. There is no
existing directory covering VertexInputStateFactory / VaoDrawMemo / Magma's mint;
MG_Test/Backend/DirectVulkan is registered only under ENABLE_INTEGRATION_TESTS and its two
targets need GLFW, a Vulkan loader and a device. This suite needs none of that —
MagmaPipeArms.h is header-only — and MG_Test/Pipe is registered unconditionally, so the four
cases are in ctest -L unit in all three build directories.
The shape. A keep-alive object holds the first allocatable slot and is touched on every boundary, so (i) it is never retired and (ii) the slot under test is not slot index 0, the one negative control C aliases everything onto — the reuse is therefore non-degenerate.
| case | asserts |
|---|---|
AnIdleSlotIsRetiredAndReusedWithANewGeneration |
the precondition, on its own so a failure reads as "the mint stopped reusing slots": after 1280 boundaries LiveCount() drops to 1, and the next Acquire returns the same slot with Gen + 1 and mints no third slot |
AReusedSlotDoesNotServeItsPredecessorsMemo |
knob OFF: a memo stamped at {slot, gen=N} is not served at {slot, gen=N+1} — the entry comes back claimed for its new owner with a zero payload. The slot is asserted equal first, so only the generation can be doing the work |
TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot |
knob ON: the same entry comes back with the predecessor's payload, uncleared and unclaimed (Owner still null) — which is what makes the case above a control rather than a tautology |
ALiveObjectKeepsItsSlotItsGenerationAndItsMemo |
the respecify half: across two sweeps a live object keeps its handle and its memo, so a deleted ++Gen cannot be "fixed" by bumping Gen on every acquisition |
The claim rule is production code, not a copy of it. VertexInputStateFactory::MemosFor's
body moved into MagmaPipeArms.h as MagmaPipeClaimSlotMemos(table, handle) — the same three
branches (the control's alias, the Owner == handle compare, the clear-and-claim) — and
MemosFor is now one call to it. The suite calls the same function. Semantics are unchanged on
both paths; see §4 for the codegen check that says so.
2.1 The test is load-bearing (the ++Gen deletion, with the committed assertions)
MagmaPipeArms.h:365, ++m_entries[index].Gen; commented out, build-push rebuilt:
$ ctest --test-dir build-push -R MagmaPipeIdentity --output-on-failure
MagmaPipeIdentityTest.cpp:131: Failure Expected equality of these values: Which is: 1 / Which is: 2
MagmaPipeIdentityTest.cpp:132: Failure Actual: true Expected: false
[ FAILED ] MagmaPipeIdentityTest.AnIdleSlotIsRetiredAndReusedWithANewGeneration
MagmaPipeIdentityTest.cpp:153: Failure Expected: (second.Gen) != (first.Gen), actual: 1 vs 1
MagmaPipeIdentityTest.cpp:157: Failure Expected equality of these values: Which is: 57005 / Which is: 0
the replacement inherited the dead object's memo out of the SAME slot: the generation is the
only thing that separates {slot, gen=N} from {slot, gen=N+1}, and it did not
[ FAILED ] MagmaPipeIdentityTest.AReusedSlotDoesNotServeItsPredecessorsMemo
MagmaPipeIdentityTest.cpp:183: Failure Expected: (second.Gen) != (first.Gen), actual: 1 vs 1
[ FAILED ] MagmaPipeIdentityTest.TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot
25% tests passed, 3 tests failed out of 4
57005 is 0xDEAD: the dead object's payload, served to its successor out of the same slot. The
fourth case stays green, correctly — it is about respecify, which the deletion does not touch.
The generation-check in the two knob cases is EXPECT_NE rather than ASSERT_NE on purpose, so
the memo assertion still runs and the failure names the inheritance rather than only the
precondition.
++Gen restored, build-push rebuilt: 4/4 pass, and symbol_report.py --threshold 0 between
the pre-experiment library and the restored one reports 0 added, 0 removed, 0 resized, 0 renamed — no residue (the files differ only in the linker's build id).
3. The two minors, closed (c290693d)
Minor 1 — the fourth, unreachable knob consumer. VulkanRenderer.cpp's legacy VaoDrawMemo
compare read MG_Config::Features.PipeHandleAbaControl directly, bypassing the one-question
accessor. The control's early return fires ahead of both arms, so it could never be false
there. Deleted rather than routed: the #if MOBILEGL_PIPE_PUSH / #else pair is gone and
the lifetime-id compare is unconditional again, with a comment saying where the question is
answered and to use MagmaPipeAbaControlDefeatsIdentity() if that early return is ever narrowed
— which is the trap the dead line was.
Minor 2 — the stale comment. HandleRecycleScenario.cpp's vertex-array header said "BOTH the
VAO and the buffer are recycled here so that a key built out of raw addresses matches"; the body
has contradicted that since 55d2af9b. It now says only the VAO is recycled, that both buffers
are realised before the window on purpose (buffer traffic inside it moves VkBufferManager's
slice-epoch counter, which is not an identity gate), and that what recycles is the GL name,
not the heap block.
Minors 3, 5 and 6 are not addressed this round — see §6.
4. What the knob costs the shipped push arm (review minor 4)
Stated honestly first. D18's consumer sat on the legacy path, after the handle arm had returned, so the arm P2 measures carried no control branch at all. It now carries, per draw:
| site | asks per draw | on the hit path? |
|---|---|---|
VulkanRenderer::LookupVaoDrawMemo |
1 | yes, every draw |
MagmaPipeClaimSlotMemos via MemosFor |
up to 3 (TryGetMemoizedHash, GetOrComputeHash, GetOrCreateVertexInputState ×2) |
yes, every draw |
VertexInputStateFactory::ComputeHash |
1 per enabled attribute | no — only on a VAO reconfiguration, which the memo above suppresses |
Each is a load of one Bool out of MG_Config::Features plus a never-taken, perfectly
predictable branch. Nothing was hoisted and no [[unlikely]] was added this round; the honest
description is "about four predictable branches per draw on the shipped arm".
4.1 Codegen: measured, exactly
build-push library with the knob present, versus the same tree with
MagmaPipeAbaControlDefeatsIdentity() compiled out (return false;). symbol_report.py --threshold 0, so nothing is below the reporting floor:
.text 10872867 -> 10872611 (-256, -0.002%)
28031 -> 28031 defined symbols: 0 added, 0 removed, 6 resized, 0 renamed
| symbol | knob present | knob compiled out | delta |
|---|---|---|---|
VertexInputStateFactory::GetOrCreateVertexInputState(const VertexArrayObject&) |
562 | 494 | −68 |
VertexInputStateFactory::GetOrComputeHash(...) const |
303 | 255 | −48 |
VertexInputStateFactory::MemosFor(...) const |
158 | 111 | −47 |
VertexInputStateFactory::TryGetMemoizedHash(...) const |
249 | 210 | −39 |
VertexInputStateFactory::ComputeHash(...) const |
480 | 444 | −36 |
VulkanRenderer::LookupVaoDrawMemo(const VertexArrayObject*) |
459 | 434 | −25 |
263 bytes across exactly six functions, all of them the vertex-input path, and no others —
which is also the tightest available check that MagmaPipeClaimSlotMemos changed no semantics:
the extraction moved the rule into a template and the only symbols that moved at all are the ones
the knob is in.
4.2 Wall clock: attempted, and it cannot resolve the effect on this host
DriverBench mc_vanilla_draw (5495 draws/frame, 120 frames), magma backend, the two libraries
above, interleaved, run_driver_bench.sh with MGL_EGL_VENDOR=50_mesa.json,
MGL_VK_ICD=lvp_icd.json:
| rep | knob present ns_per_op |
knob compiled out ns_per_op |
|---|---|---|
| 1 | 17141.7 | 17532.5 |
| 2 | 17268.1 | 17686.3 |
| 3 | 17392.8 | 17819.5 |
| 4 | 17533.4 | 17292.8 |
| 5 | 17253.4 | 17202.6 |
| median | 17268.1 | 17532.5 |
The arm that should be faster is slower at the median, which is the correct reading of "no
resolvable difference". The only GPU stack in this WSL tree is llvmpipe/lavapipe, so ns_per_op
is ~17.3 µs of software rasterisation per draw with a run-to-run spread of ±400 ns (±2.3 %),
against an expected effect on the order of 1 ns (0.006 %) — a noise floor roughly 400× the
effect. Treat the wall-clock number as unmeasured. The measurement that can see it is D.4.2's
device run on real hardware, or a perf stat instruction count; the codegen table in §4.1 is the
evidence this round actually has.
5. Gate re-runs, at c290693d
All three build directories rebuilt first (cmake --build … -j 12, rc 0 each).
| gate | command | result |
|---|---|---|
| G8 | ctest --test-dir build-push -R HandleRecycle --no-tests=error -j 4 |
32/32 |
| G8 | ctest --test-dir build-verify -R HandleRecycle --no-tests=error -j 4 |
40/40 |
| unit | ctest --test-dir build-linux -L unit --no-tests=error -j 8 |
1566/1566 (the 4 new cases SKIP visibly in the pull build) |
| unit | ctest --test-dir build-push -L unit --no-tests=error -j 8 |
1566/1566 |
| unit | ctest --test-dir build-verify -L unit --no-tests=error -j 8 |
1566/1566 |
1562 → 1566 is exactly the four new MagmaPipeIdentityTest entries, and they are named in all
three builds (G2's name parity: the pull build declares them through the same
MGL_..._TEST_LIST(X) skip-list shape CsoCacheTest.cpp uses).
G1, the pull build, --threshold 0:
$ python3 scripts/symbol_report.py --before ~/w7/p2-before-libMobileGL.so \
--after build-linux/libMobileGL.so --threshold 0
symbol-report: .text 10792579 -> 10792739 (+160, +0.001%)
symbol-report: 27799 -> 27799 defined symbols: 0 added, 0 removed, 4 resized, 0 renamed
MobileGL::MG_State::GLState::RenderState::RenderState() 1700 -> 1848 (+148)
MobileGL::MG_State::GLState::RenderState::SetCapability(CapabilityInput, bool) 850 -> 927 (+77)
MobileGL::MG_State::GLState::RenderState::IsCapabilityEnabled(...) const 239 -> 268 (+29)
_GLOBAL__sub_I_DirectGLES.cpp 1340 -> 1331 (-9)
Exactly INTEGRATOR-DECISIONS ID-3's four, at the same sizes as v1, and nothing else — no symbol
to attribute. Expected: the new unit suite is a test target, MagmaPipeClaimSlotMemos is
#if MOBILEGL_PIPE_PUSH, the VulkanRenderer.cpp deletion removes a push-only branch, and the
rest of the round is comments.
6. Open items for the integrator
BRIEF-P2.mdD18 and the G8 row still want amending, and now to these words, not v1's: theAbaControl*lanes are a control over the object identity that selects the memo slot; the generation is covered byMG_Test/Pipe/MagmaPipeIdentityTest.cpp. Two lanes, not one, remains a declared deviation from D18. v1 §4.1 (the "STALE on both arms" premise) is refuted and stands refuted.MEASUREMENTS.mdshould record why: Magma's mint retires by age (256/1024) and the only pixel-visible vertex-input memo is same-frame, so a generation-only regression is out of reach of anyHandleRecycleScenario-shaped reproducer. This is also the second, independent reason magma-v3's probe B stayed green over 432 integration cases; the result file's §4.4 attributed it to the cross-frame gate alone.- Not addressed this round (all from review v1, all pre-existing or cosmetic):
- minor 3 — the GL-name allocator is still a dependency:
HandleRecycleScenario.cpp:547skips ifglGenVertexArraysdoes not hand the name back, and a skip in a control lane is a control that stopped controlling whilecteststays rc 0.TheReproducerRecyclesEveryNamereds the same lane in that case, which is why it is survivable; - minor 5 — with the knob on, the aliased
VaoDrawMemoentry can hand outvkBuffersbelonging to whichever VAO last used slot 0. Confined to the two control lanes and bounded by the frame-serial gate; - minor 6 — the
AbaControl*lanes do not setMOBILEGL_PIPE_VERIFY=1, so running G8 inbuild-verifyproves the build, not the armed comparator. Pre-existing lane design.
- minor 3 — the GL-name allocator is still a dependency:
- The
-j 8arming-lane race (v1 §V5.6, corroborated by review §5.7) is unchanged and still a liveintegration-gpuhazard independent of this work. MOBILEGL_PIPE_HANDLE_ABA_CONTROLstill steers nothing on DirectGLES; Espryt's Track-H slice has no negative control of this kind, and now no unit-level generation test either. Espryt's mint isMG_Impl/Pipe/SlotAllocator, whichSlotAllocatorTest.cppdoes cover forGen— but nothing joins that to a consumer memo the way §2 does for Magma.