- 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