- VulkanRenderer::LookupVaoDrawMemo's legacy arm read MG_Config::Features.PipeHandleAbaControl
a fourth time, bypassing the one-question accessor MagmaPipeAbaControlDefeatsIdentity that
55d2af9b's own message says all the control's sites go through. It could never be false
there: the control's early return fires ahead of BOTH arms, so a run that reaches the legacy
compare has the knob off. The compare is unconditional again, with a note saying where the
question is answered and to route through the accessor if that early return is ever narrowed
- which is the trap the dead line was, since narrowing it would have silently restored D18's
retired semantics at this one site.
- HandleRecycleScenario's vertex-array case still said "BOTH the VAO and the buffer are
recycled here so that a key built out of raw addresses matches", which the body has
contradicted since 55d2af9b: both buffers are created before the ABA window and neither is
deleted inside it, deliberately, so that no buffer traffic moves VkBufferManager's
slice-epoch counter inside the window. Only the VAO is recycled, and only its GL name - the
heap block is not handed back, which is why the knob rather than the allocator constructs the
AbaControl arms' collision.
- 55d2af9b claimed - in its message, in MagmaPipeArms.h, in VertexInputStateFactory.cpp and
in MG_IntegrationTest/CMakeLists.txt - that the AbaControlHandles lane defeats the
GENERATION in {slot, gen}. It does not, and no lane of that shape can. Magma's mint has no
death notification (nothing in MG_Backend/DirectVulkan consumes NotifyStateObjectDestroyed)
and returns a slot only through OnFrameBoundary's age sweep, kSweepInterval 256 /
kRetireAgeBoundaries 1024; HandleRecycleScenario issues five frame boundaries, so the
replacement VAO acquires against an empty free list and gets a BRAND-NEW slot at Gen 1
(measured: redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
decided by the SLOT alone, and deleting ++m_entries[index].Gen leaves all 32 HandleRecycle
entries green - re-measured this round.
- What the lane does defeat is the object identity that SELECTS the slot, which IS the key the
handle arm ships, and that is what the three code sites now say. The two requirements are
mutually exclusive for the pixel-visible memo: a genuine slot reuse needs >= 1024 idle
boundaries after the dead object's last draw, which necessarily 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.
- So the generation is covered where it IS expressible. MG_Test/Pipe/MagmaPipeIdentityTest.cpp
drives the mint's real retire -> reuse (1280 boundaries, with a keep-alive object holding the
first allocatable slot so the reuse is not the slot the control aliases onto) and asserts
four things: the retired slot comes back with Gen+1; with the knob OFF a memo stamped at
{slot, gen=N} is NOT served at {slot, gen=N+1}; with the knob ON it IS, out of one uncleared
and unclaimed entry; and a live object keeps its slot, its generation and its memo across two
sweeps, so the generation cannot be "fixed" by bumping it on every acquisition.
- The claim rule itself moves into MagmaPipeArms.h as MagmaPipeClaimSlotMemos so the suite
exercises production code rather than a copy of it. VertexInputStateFactory::MemosFor is now
one call to it and is otherwise unchanged, on both the knob-on and the knob-off path.
- Load-bearing, measured: with ++m_entries[index].Gen commented out, ctest -L unit in
build-push goes 1563/1566 - three of the four new cases red, one of them naming the inherited
0xDEAD payload out of the same slot - while ctest -R HandleRecycle stays 32/32. Restored, all
four pass in build-push and build-verify and skip visibly in the pull build, so the ctest name
sets stay identical (G2).
- an ENVIRONMENT property overrides only the variables it names, so the gate's all-pull control
arm (MOBILEGL_PIPE_PUSH=0 over the whole integration-gpu label) leaked into the Handles and
AbaControlHandles lanes, turned their LEGACY_MEMOS=0 into D14's armless combination and made
the bring-up abort by design; the lanes now pin the P2 default bitmask themselves
- MOBILEGL_PIPE_HANDLE_ABA_CONTROL asserted the corruption and saw correct pixels, so
DirectVulkan.HandleRecycle.AbaControl.*AVertexArray* was RED in an always-on
integration-gpu lane while every guard it was supposed to be defeating stood. Two
measured reasons, neither of them the {slot, gen} re-key: (1) D18 spelled the control as
"hash the raw BufferObject* instead of its lifetime id, and skip the vaoLifetimeId
compare", which only collides if the allocator hands the freed block back - it does not.
glGen* recycles the NAME, but a VertexArrayObject is 3920 bytes, past glibc's tcache, so
its chunk goes to the unsorted bin and is split by the next allocation the replacement
path makes; four create/delete cycles in one run gave four addresses ~1 MiB apart, and
the BufferObject behaves the same. (2) The reproducer put a frame boundary between the
arming draw and the recycled draw, and the only memo that carries a GPU slice rather
than a layout - ResolvedVertexBindings - declines across frames by design, so no key
collision whatsoever could have shown up in pixels.
- The control no longer asks the allocator for the collision: on both arms it replaces the
object identity in DirectVulkan's vertex-input keys with a constant, which is the
strongest form of "the block came back" and is deterministic. Three sites, all behind
one question (MagmaPipeAbaControlDefeatsIdentity): the buffer identity leaves
VertexInputStateFactory::ComputeHash, VertexInputStateFactory::MemosFor claims one entry
without its Owner compare, and VulkanRenderer::LookupVaoDrawMemo hands one entry back
uncleared ahead of both arms.
- That is what makes the control cover the key P2 SHIPS. Under MOBILEGL_PIPE_PUSH=0 the
handle arm is not executed at all, so the old control said nothing about the generation
in {slot, gen} - the whole of what makes the re-keyed memos ABA-safe. A second lane,
DirectVulkan.HandleRecycle.AbaControlHandles., runs the handle arm with the knob and
asserts the same corruption; D18's lane is kept verbatim beside it for the pre-handle arm.
- The reproducer's two draws now share a frame, and both buffers are realised before the
window, so a moved slice epoch cannot mask the ABA behind a gate that is not about
identity. Nothing else is relaxed: the frame serial, the slice epochs and the host-map
check stay in force, so a green arm still means "a replacement object was handed its
predecessor's resolved vertex bindings because the identity halves of the keys were
defeated".
- ExpectPixelsFor now prints what it OBSERVED (STALE/FRESH/NEITHER) next to what the arm
expected, on every arm and whether or not the case passes.
- Knob-off is unchanged and the pull build is untouched: every new branch is
#if MOBILEGL_PIPE_PUSH, and symbol_report.py --threshold 0 against the pre-P2 baseline
still reports 0 added / 0 removed / 0 renamed and the same four resized symbols
(RenderState::RenderState, SetCapability, IsCapabilityEnabled, _GLOBAL__sub_I_DirectGLES.cpp).
- bench.sh's pin_freqs writes /proc/ppm and /proc/gpufreq, which exist on neither 35d0befa
(SM8750) nor 3B159D009VZ00000 (MT6993 dropped both for /proc/gpufreqv2); the guard the
PROFILE_VERIFIED key exists for was about to be defeated on the device its own comment
guessed was safe
- pin_device.sh pins big/little/GPU through the nodes each device actually has (Xiaomi:
policy6 1958400 / policy0 1555200 / kgsl pwrlevel 0; Oppo: policy4 2000000 + policy7
2000000 / policy0 1600000 / gpufreqv2 fix_target_opp_index 0), checks only against its own
pins because ColorOS moves stock maxima by itself, and refuses to call a zero-key read a verdict
- both profiles are PROFILE_VERIFIED=1 on 30 s pinned windows with zero drift; the evidence and
the two things not verified (Oppo GPU held at DVFS-request level only, no full bench window)
are in docs/Disaggregated/devices/pin-verification-2026-09-07.md
- -Pmobilegl.pipePush=ON (or MOBILEGL_PIPE_PUSH_APK=ON) passes -DMOBILEGL_PIPE_PUSH=ON to
the NDK build the way the spike opt-in already does; OFF by default so nothing shipped moves
- spans (d1a7c5f1) removed GetPixelStoreParameters from the emitted list because the call
carries the pack half of a two-element field, so the generated MGPipeFieldEmitter no longer
has a SetPixelPackState enumerator; the tracker's subsystem map and its static_assert, written
against the contract, still named it and the push and verify builds did not compile on the
integrated tree
- the NEW_PIXEL_PACK dirty bit still names the pixel-pack subsystem, which is what the
emission gate consults; the field itself stays in the residual fill loop by design
- bench.sh and session.sh are invoked as ./tools/device_bench/bench.sh in their own README
and are the harness for the paired two-device A/B the GO/NO-GO asks for; a previous commit
in this package dropped the bit and every check invoked them through bash, so nothing saw it
- The verified-profile guard read the process environment as well as the profile: the test ran
after the source, so PROFILE_VERIFIED=1 exported in an operator's shell re-opened the fail-open
hole for every profile that says nothing. Both scripts now set PROFILE_VERIFIED=0 immediately
before sourcing, so the file is the only thing that can answer.
- A --device path that cannot be sourced was diagnosed as an unverified profile, because both
scripts cd to their own directory first and neither checked readability. The path is now also
tried relative to the directory the script was invoked from (which is what a repo-root-relative
--device means), and an unreadable one is reported as unreadable, naming both places tried.
- Verified: exported PROFILE_VERIFIED=1 + an unverified profile -> rc 2; exported 1 + a profile
with no key -> rc 2; a repo-root-relative path -> resolved, then refused for its own reason;
a missing file -> "cannot read the device profile"; odinlite.env -> past the guard;
--allow-unverified-profile -> the three warnings, then proceeds.
- test.yml's new step described a mechanism the tree does not have. G6's and G10's entries are
registered in the pull build too - they must be, for G2's name-for-name comparison - and skip
inside their bodies. The step's value is unchanged and its comment now says the true thing: the
`test` job runs those names as a column of skips, and this is the first CI job that unpacks a
build which compiled the assertions.
- trace_benchmark takes the wall baseline before the CPU baseline, the order OnFrameBoundary
already reads them in, so frame 0 stops reporting a CPU delta biased upward against its own
wall delta; and it includes <time.h> rather than <ctime> for the POSIX names it uses.
- All four capability markers are now content probes over the directory the owning package owns,
through one helper. The magma pair still read a single hard-coded VertexInputStateFactory.cpp
while package D already keeps one of its two Features.PipeHandleAbaControl consumers in
Renderer/VulkanRenderer.cpp, so one file move on D's side was a permanent AbaControl skip - the
same defect the CSO probe was rewritten for. The DirectGLES probe stops asking whether
SlotTables.h exists and asks for kMGPipeSubsystemEsprytSlots, the bit the arm is actually gated
on. Every globbed file stays in CMAKE_CONFIGURE_DEPENDS, and the glob is CONFIGURE_DEPENDS.
- Verified: with one throwaway header naming each symbol, build-push configures to "keyed on
{slot, gen}" / "has an emitter" / "has a consumer" and all four MGITEST_* markers appear 24
times in the generated ctest environments; with the headers gone, all four are back to 0 and
the four "will SKIP" verdicts return. The magma sim sat in Renderer/, not in the path the old
probe hard-coded.
- g7_negative_control.sh no longer exits 0 when the control trips for the wrong reason. A
SetterConsistency that had gone red for an unrelated reason satisfied "ctest failed" and never
named SetColorMask, and the integrator's D.3 reads this script's rc. The verdict is now taken
after the restore and the rebuild - a broken build directory is worse than any exit status -
and reported as rc 1 with the output kept, alongside the existing "did not trip" rc 1.
- HandleRecycleScenario writes down what the name-recycle proxy costs: the AbaControl arm asserts
corruption that needs the heap BLOCK back, sees only the NAME, and so can red an always-on
integration-gpu lane for an allocator reason. That trade is deliberate - the alternative is an
arm that is green on the day the reproducer stops reproducing - and the consequence is now
written both in the header and at the skip that is the last thing standing between the two.
- DriverBenchStateToggle was an entry that could not fail for the reason it was added. A case
name matching nothing in kBenchCases selected nothing, run_case is void, and main returned 0
unconditionally, so renaming or dropping mc_state_toggle left the entry green while measuring
nothing - the exact state it was landed to end (ROADMAP.md:7).
- DriverBench now refuses an unknown case name before any GL work (exit 2, listing the cases it
does have), so a caller that names a case - run_driver_bench.sh included - learns the case is
gone instead of getting an empty CSV.
- Both ctest entries additionally require the case's own output row via PASS_REGULAR_EXPRESSION,
so the gate stands on the evidence rather than on that check staying in the binary. The
toggle entry pins the ops-per-frame column to 46, because the mc_* cases are deliberately
excluded from the DRIVERBENCH_DRAWS scaling and 46 toggles per frame is part of what "this
case still runs" means. A PASS_REGULAR_EXPRESSION makes ctest ignore the exit code, which is
why the row is what is checked; the comment says so.
- Verified: renaming mc_state_toggle in kBenchCases -> DriverBenchStateToggle FAILS; setting its
ops-per-frame to 45 -> FAILS; restored -> both entries pass again.
- format_benchmark printed a p50 taken with the nearest-rank rule beside a medianFrameCpuMs the
device computes as the average of the two middle frames, and documented the two as one rule; on
an even window they differ (the pre-flight printed p50=8.261ms next to medianCpuMs=271.766).
p50 now goes through series_median, which is SummarizeSeries' rule transcribed; p95 and p99 stay
nearest rank, which is the device's rule for p95 and the honest extension of it for the p99 the
device does not compute at all
- require_verified_profile treated a profile that simply omits PROFILE_VERIFIED as verified, which
is the fail-open default a profile written by copying another one inherits - exactly the case the
guard exists for. It defaults to unverified now, odinlite.env carries PROFILE_VERIFIED=1
explicitly (it is the one profile that earned it), and the refusal says "says 0, or says nothing"
- the two new profiles claimed profile.sh refuses an unverified profile; it has no such check and
needs none - it records a simpleperf profile and pins nothing. The claim is corrected in both
profiles and in the README rather than a guard added where there is nothing to guard
- the handle-ABA / CSO control step in test.yml set only MOBILEGL_ITEST_REQUIRE_GPU while its
sibling verify step sets the three MOBILEGL_MAGMA_* fixes and arms core dumps. It runs the same
DirectVulkan binary on the same runner, so a crash there left no core; it now carries both
- the three capability markers were decided from source-tree file existence / file text alone, so
after packages C and D land they would have armed the PULL build too, where every arm they name
is compiled out: the AbaControl lane would have gone hard red on `ctest -L integration-gpu`
(gate G2 requires it green in both builds) and the Handles lane green against a library with no
{slot, gen} key at all. The whole block now sits under the same `if (MOBILEGL_PIPE_PUSH)` as
MGITEST_PIPE_PUSH_BUILD, and HandleRecycleScenario re-checks that marker before either push arm
asserts, so a hand-forced environment cannot arm an arm this build does not have either
- the two push-only knobs of those lanes (MOBILEGL_PIPE_LEGACY_MEMOS=0, MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1)
are set only in a push build. In a pull build the legacy arm is the only arm and every subsystem
bit is clear, which is D14's startup Fatal{PipeLegacyMemosDisabled} - the process would abort
before the scenario could report its skip. Test NAMES are unaffected, so G2 still compares equal
- the CSO control armed itself off `MG_Impl/Pipe/Tracker.cpp`, a file the owning package does not
create: it implements the tracker and the cache header-only, so all four CsoContentAddressing
entries would have kept skipping after it landed, with a reason that had become false. The probe
now greps every source under MG_Impl/Pipe/ for the two counters the control actually reads
(RenderStateCsoMints / RenderStateCsoBinds), watching the directory and each file, so the owning
package keeps control of its file layout
- an unrecognised MGITEST_HANDLE_ARM is a FAIL in SetUp instead of a silent downgrade to the
Legacy arm, which would have passed while claiming to be the lane it was not
- CsoContentAddressingScenario reads the library's own summary line, and a log is a per-LANE
resource: the library opens it fopen(path, "w"), so every process in a lane truncates it. The
file had TWO cases in each lane, which under `ctest -j` is a race whose failure mode is an
empty read - indistinguishable from "the counters were never emitted", which is precisely the
thing the case exists to report on.
- The separate plumbing case is folded into the control as its first ASSERT, keeping its own
message, so nothing is lost but the flake. Splitting it out bought a clearer failure message
and paid for it with a flake in the mechanism that message is about.
- This is the same hazard the file's existing comments describe for the arming lane; it is worth
saying out loud that the rule is "a log-reading case owns its lane", not "a log-reading case
owns its log path".
- Verified at -j 4: 44/44 on build-verify and 24/24 on build-push, and the pull/push ctest name
lists are still identical (1402 entries each; 0 names removed against the contract tree, 34
added).
- pipe-gates stops printing gen_pipe_dirty_surface.py --summary and runs --check && --self-test.
--check fails both directions - a scanned mutator with no row in MG_Pipe/DirtySurface.def, and
a row naming a mutator the scan no longer finds - so a deleted mutator cannot leave a stale row
behind claiming coverage. --self-test is what keeps --check honest: a completeness check that
silently stopped checking is indistinguishable from a complete mapping, so two canned negative
controls must both trip. Same shape as gen_pipe.py --self-test next to it.
- integration-verify gains `ctest -L unit`. G6's chunk-table walk and G10's residual assertions
live in MG_Test/Pipe, compiled only under MOBILEGL_PIPE_PUSH, and the `test` job builds the
PULL library - so before this those tests ran in no CI job at all. The artifact already carries
them (the packaging step tars MobileGL/MG_Test whole), so the whole cost is the run: ~14 s for
~1490 entries, measured locally on this tree.
- integration-verify also runs the two always-on negative controls by name. They are labelled
integration-gpu rather than integration-verify - they are about the handle key and the CSO
switch, not the comparator - and this is the only CI job that unpacks a push build, which
CsoContentAddressingScenario needs because both counters and the cso[] bracket are
#if MOBILEGL_PIPE_PUSH.
- build-linux-verify's arming check accepts the per-verb entry point under either of its two
names. P2 renames MGPipeFillForVerb to MGPipeValidateForVerb, and a check that named only the
old one would go red on the rename for a reason unrelated to what it tests. What it tests is
unchanged: the artifact has a per-verb entry point, and it still fails when there is none.
- G2 requires `ctest -L integration-gpu` to be name-for-name IDENTICAL between the pull build and
the push build, and the four CsoContentAddressing lanes were registered inside
`if (MOBILEGL_PIPE_PUSH)`. That is four entries the push build has and the pull build does not,
which breaks the comparison for this package and for every package that lands after it.
- They now register unconditionally. What the pull build lacks is not the entry but the thing the
entry is about, so the build passes MGITEST_PIPE_PUSH_BUILD in and the scenario skips saying
exactly that: no render-state CSO exists, no cso[] bracket is compiled into the summary line,
and the content-addressing bit steers nothing.
- The marker also sharpens the plumbing assertion it guards. Past that skip the process is known
to be a push build, and the cso[] bracket is unconditional inside that same #if - so a missing
bracket can no longer mean "wrong build configuration" and the failure message stops offering
that as an explanation.
- Verified: build-linux and build-push now differ by zero ctest names (diff empty over 1408
entries each), and the pull lanes skip with the push-build reason while the push lanes skip
with the tracker-not-landed reason.