- BackendSlotTable links every instance into a per-type holder list; OnFrontendObjectDestroyed resolves the handle once, drops the twin in each holder by handle and frees the slot once, last - a notice delivered to one registry left the fixture's saved copy holding a dead twin, and its driver storage, for the life of the process (review v4 minor 2)
- ReclaimDeadSlots and CollectGarbageNow leave the slot table: nothing called them, so the header's "backstop" was a claim without a caller; the registry's pre-P2 CollectGarbageNow is a no-op on the handle arm (minor 1)
- HandleOf no longer memoises a null answer, which a second holder's acquire could never refresh (minor 8)
- EnsureProcessTeardownSentinel is armed by the slot table's first insertion, as D13 says; the registry arms it only on the legacy arm (minor 9)
- new SanityTest cases: OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice, ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire, and EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs InitPbufferSurface under the pair in a forked child and fails naming both knobs if InitDisplayAndContext ever stops there again (minor 4)
- AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane writes a per-process, per-case log path and restores MOBILEGL_LOG_FILE_PATH and MG_Config::Features through RAII guards on every exit path (minor 5)
- Fatal{PipeLegacyMemosDisabled} was raised from InitDisplayAndContext(), i.e. from inside
eglMakeCurrent. The integration harness pre-flights that exact sequence in a forked child
(MG_IntegrationTest/Harness/HeadlessGL.cpp) and reports a child that dies on a signal as
"no usable GPU/display/ICD", so every scenario SKIPPED and ctest called the lane 100%
passed while running nothing - on the very pair of env vars the D14/D18 A/B is driven
with. ROADMAP.md:7 forbids a gate that cannot go red for the reason it exists.
- The arm decision becomes a pure function of the two knobs, ClassifyEsprytSlotArm(), with
three verdicts. Bring-up now calls DiagnoseEsprytSlotArm(), which names both knobs at
ERROR and RETURNS; the stop stays in ResolveEsprytSlotTablesArm(), which the inline latch
reaches at the first twin lookup - a scenario body, where a crash is a test failure.
- A process that never looks a twin up never needs an arm and is no longer stopped by one
it would not have used. That is the only behaviour this moves.
- SanityTest gains an always-on case: the four knob combinations of the pure classifier,
that the diagnosis does not stop, and that the stop is SIGABRT whose log line names
PipeLegacyMemosDisabled, MOBILEGL_PIPE_PUSH, MOBILEGL_PIPE_LEGACY_MEMOS=0 and the bit -
the message and not merely the signal, because "Subprocess aborted" alone tells an
operator nothing. It skips visibly in the pull and no-legacy builds (G2 name parity).
- P2 step e2, as far as the file-ownership table lets one package take it. New frontend
header MG_State/GLState/StateObjectDeathNotice.h carries BufferBackendOps' shape for the
other six kinds: an ops table the backend fills in, and one entry point that takes
{kind, lifetimeId} rather than the object, because by the time the last SharedPtr has
dropped there is no object left to pass and the lifetime id is exactly what the client
slot allocator resolves a handle from. Declared only under MOBILEGL_PIPE_PUSH, so the
pull build's symbol set is untouched.
- BackendSlotTable::DestroyByLifetimeId drops the twin and returns the slot at the moment
the object goes, instead of at the next sweep - which for a renderbuffer or a texture
atlas is the difference between freeing the driver allocation now and freeing it 64
creations from now. It returns the slot only when THIS table holds it: two holders of one
kind already exist (the ScopedDirectGLESTextureBindings fixture; Magma's subsystem-4
table shares the VertexElementsCso kind), and a table that never twinned the object must
not free a slot the other one still names. The legacy arm keys on the frontend heap
address, cannot answer a notice at all, and keeps the sweep - which is the announced-
versus-discovered half of the A/B the compile-time arm exists for.
- Managers.cpp registers one dispatcher for all six kinds from ResolveEsprytSlotTablesArm(),
i.e. exactly when the arm that can answer a notice is the arm that runs, and drops a
notice that arrives after exit() has begun.
- FIRING it needs a destructor per class, and the P2 ownership table gives
{Texture,Framebuffer,Sampler,VertexArray}State/* to other packages, so only ProgramObject
and RenderbufferObject raise it here. The other four still rely on the sweep; their four
one-line calls retire it entirely.
- Three cases pin the three halves: the slot comes back with no sweep and the notice is
idempotent and does not free another holder's slot; a program and a renderbuffer announce
their own death when the last SharedPtr drops and not before; and the handle arm actually
installs a consumer, rather than the two halves each being fine on their own.
- The handle arm took only ONE of the registry's two sweep drivers. The map arm sweeps
every 64 first-time insertions BECAUSE object churn, not draw count, is what makes the
sweep urgent: a CTS-shaped case runs ~10 per-draw ticks, so the 1024-tick draw-path
driver alone spans ~100 cases' worth of dead, gigabyte-sized twins. BackendSlotTable now
carries the same kCreationGCInterval = 64 creation tick, swept before the entry reference
exists for the same reason the map arm sweeps there. Without this the slice REGRESSED the
memory it was supposed to leave unchanged.
- Fatal{PipeLegacyMemosDisabled} now aborts. It logged and then returned false, which fell
straight into the legacy arm the operator had just made unreachable: a green run measured
on the wrong arm, and the exact lever HandleRecycleScenario's arms are selected with. It
is also resolved at backend context creation now, not on the first twin lookup, so a
process that twins nothing still learns its knobs leave it with no arm at all.
- EsprytSlotTablesEnabled() becomes an inline latch over an out-of-line resolver. It is
consulted on every Find/GetOrCreate/HandleOf/ForEachLive/CollectGarbage*, i.e. several
times per draw, and as a cross-TU call with no LTO that was a PLT call per lookup.
- HandleOf keeps a one-entry lifetimeId -> handle memo, so the three per-draw resolution
paths whose TwinLookupMemos this slice deleted go back to an integer compare plus an
array index instead of the allocator's ByLifetimeId hash - which is the "direct slot
indexing" the memo removal was traded for. It cannot serve a stale answer: a lifetime id
is never handed out twice, and FindByHandle compares Gen anyway.
- GetOrCreate(nullptr) returns a parking slot instead of dereferencing null in a release
build; the map arm inserted a null key and SyncTextureObjectToBackend documents relying
on that tolerance.
- ReclaimDeadSlots moves the twin out before it writes the entry, so a twin destructor that
re-entered GetOrCreate and grew m_slots could not make the writes land in freed memory.
- The framebuffer binding-slot cache is gated on kMGPipeSubsystemEsprytSlots rather than on
the compile-time MOBILEGL_PIPE_PUSH, so MOBILEGL_PIPE_PUSH=0 stays the faithful
all-subsystems-pull control ConfigLoader.cpp documents. The poison bypass stays closed on
the arm that ships.
- MGB_TWIN_KIND_PARAM/ARG stop leaking into every TU that includes Managers.h: the twelve
declaration and definition sites name a TwinRegistry alias template that swallows the
kind in the pull build, and the one remaining macro is #undef'd after the class.
- The twin lookup inside BindCurrentFBO stops shadowing the framebuffer binding slot in a
function whose whole subject is which "slot" is meant.
- SlotTables.h: BackendSlotTable<StateObject, BackendObject, kKind>, indexed by
MGPipeHandle::Slot and validated by Gen, with the handle minted by the client
MGPipeSlotAllocator off the frontend object GetLifetimeId(). A lookup is one bounds
check plus one array index, and unlike the registry Find it never mutates the table,
so a returned BackendPtr* is not invalidated by the next call on it.
- StateBackendObjectRegistry keeps its name, its signature and all ~40 call sites, and
becomes the two-arm facade ARCHITECTURE.md 9.6 asks for: the pre-handle map under
MOBILEGL_PIPE_LEGACY_MEMOS, the slot table under MOBILEGL_PIPE_PUSH, chosen once per
process by EsprytSlotTablesEnabled() off kMGPipeSubsystemEsprytSlots. A clear bit with
MOBILEGL_PIPE_LEGACY_MEMOS=0 leaves no arm at all and is Fatal{PipeLegacyMemosDisabled}.
- The kind is a template parameter only in the push build (MGB_TWIN_KIND_ARG): a third
template argument would rename every instantiation and G1 wants the pull build byte
identical. Pull-build symbol report is 0 added / 0 removed / 0 renamed and adds no
resize beyond the three RenderState symbols the contract commit already moved.
- ForEachLive replaces begin()/end() under push and hands the callee a strong reference
to the state object instead of the map key, which was the raw frontend address.
- Nothing switches over yet: the tables are built and reachable, and the twins still go
through whichever arm the bit selects.
Advance the buffer-id generation when Ops_Respecify retires immutable storage on the context thread, matching the existing adoption and deferred-retirement paths.
Add pixel regression coverage for unchanged VBO/IBO attachments across same-size redefinition, growth and shrinkage, including bound and unbound VAOs sharing a vertex arena and an index arena returning to shadow storage.
- P1 package B (BRIEF-P1 C.2): the four DirectGLES TUs include <MG_Pipe/PipeInputsSwitch.h> right after
their MG_State/GLState/Core.h include (Managers.cpp after its Managers.h include) and spell every
frontend read MGB_CTX->Accessor(...). In the pull build MGB_CTX is MG_State::pGLContext, so the
code is the tree before this commit token for token; in the push build it is &gPipeInputs, the block
the frontend fills at every verb boundary.
- 113 arrow occurrences on 113 lines went through the mechanical sed (DirectGLES.cpp 91, Managers.cpp 15,
MultiDraw.cpp 5, Utils.cpp 2). The 9 non-arrow lines follow D9: Managers.cpp's five bare/compound
guards and three ternary conditions become MGB_CTX_LIVE (UniquePtr::operator bool spelled out, so the
pull build does not move); DirectGLES.cpp's GetFramebufferBindingSlotFast keys its static cache on
MGB_CTX_IDENTITY (a const void* compare) instead of pGLContext.get().
- The cache refill loop dereferences MGB_CTX once into a local reference and reads the slots through it,
instead of &MGB_CTX->GetFramebufferBindingSlot(i) per iteration as D9 spells it: a store into
g_fbSlotCache (a pointer) may alias the unique_ptr's own pointer under clang's TBAA, so the per-iteration
spelling re-reads pGLContext inside the loop and grows the three functions the loop is inlined into
(SyncCurrentProgram +16, ForceBindCurrentFBO +9, BlitNamedFramebuffer +1; .text +32). Hoisting the
dereference restores the single load and a zero .text delta.
- grep -rc pGLContext MobileGL/MG_Backend/DirectGLES reports 0 in every file; symbol_report --threshold 0
against the 087685d1 baseline: 0 added / 0 removed / 0 resized / 0 renamed, .text +0, nm --defined-only
set identical. The only bytes that move are __LINE__ immediates in RecordError sites after the inserted
include line. The 8 SyncPersistentMappedRange and 3 SyncGpuWrites sites and the PipeStats AddCalls
literals are untouched.
- A window with no Present divided by a faked 1 and printed the window TOTALS under
"bytes/f[...]": the *MultiDraw* slice (47 draws, no present) reported 1,404,550 bytes as a
PER-FRAME figure, a 47x overstatement of exactly the SEG_STAGE sizing input plan B section
8.2 / section 11 P0 asks this package to produce. FormatWindowLine now relabels the bracket
to "bytes[...]" and prints draws/f=n/a when the window holds no frame; acc/draw=n/a follows
the same rule, because "0.00" beside a non-zero acc= is the same lie. Pinned by
PipeStatsTest.SummaryLineSurvivesZeroFrames and the reworked SummaryLineSurvivesZeroDraws.
- Every per-frame and per-draw field now goes through one FormatFixed2 helper. draws/f read 1
for 26 draws over 14 frames (1.86) and buf read 97 for 1360 bytes (97.14): a systematic
downward truncation of up to a whole unit on the figures the package exists to produce.
Pinned by PipeStatsTest.PerFrameFieldsKeepTwoDecimals.
- FormatSummaryLine rewrote the window bases as a side effect of formatting, so any second
reader silently zeroed the next window. Split into a pure FormatWindowLine() and an explicit
AdvanceSummaryWindow(); EmitSummaryLine calls both. Pinned by
PipeStatsTest.FormattingTwiceDoesNotConsumeTheWindow.
- Init() called ResetForTesting(), against the header's own "not used by any shipping path".
Both now forward to an internal ResetCounters().
- TracyPlot published only the MISS half of each gate under the gate's unqualified name, so
the headline output channel of section 11 P0 carried no denominator. Two series per gate now
("...-hit" / "...-miss") from static literal arrays. The payload histogram stays unplotted
and says why: it is a run-total distribution over draws (section 4.5.7), not a per-frame
scalar, and it reaches the operator through the JSON dump.
- GetOrCreatePipeline's 15-call tally sat ABOVE the list-topology primitive-restart refusal,
so a declined draw added ten reads it never made - an OVER-count, which breaks the lower
bound contract every other tally keeps. Moved to immediately before the payload build, and
the enumeration reconciled with the constant: the excluded read is the sample-shading
capability, short-circuited by m_sampleRateShadingFeatureEnabled.
- Six real staging paths were uncounted while the inventory claimed coverage. The inventory
claim "DirectVulkan's own buffer staging ... has no second copy to count" was simply false.
Now wired: MultiDraw.cpp's UploadScratch/UploadScratchRing (indirect commands, the compute
tier's draw-info array, the rebased index stream - the class is passed in, so a new tier
cannot forget it); Managers.cpp's pool-recycle reseed and the VBO-backed Float64 narrowing;
VkBufferManager's eight host->device copies of buffer contents plus UploadTransient, the
single chokepoint for Magma's per-draw vertex/index/indirect staging; VkTextureManager's
packed staging slice, with the same box/rect SHAPE split Espryt already reported.
- New byte class stage-indirect-cmd, for draw PARAMETER bytes a backend synthesises and
stages. Kept out of stage-index-client because these are the population that becomes MGPipe
command-record payload (section 4.5.7), not resource bytes. A name addition, not a rename:
no recorded baseline is invalidated.
- stage-ubo-named moved past the zero-copy direct-bind decision in ResolveUniformBufferPayload
(a direct bind repacks nothing, so counting it there reported a copy that never happened),
and Magma's default-uniform-block image now feeds stage-ubo-global the way Espryt's does,
counted after the per-frame slice memo.
- The site inventory in PipeStats.cpp is rewritten to name every unwired path by file and
function. An inventory that overstates coverage is worse than a missing counter, because
the zero is then read as an answer.
- Evidence, lavapipe/llvmpipe, MOBILEGL_PIPE_STATS=1: the MultiDraw slice now prints
"window=0 draws/f=n/a bytes[buf=1404550 ...]"; the indirect tiers
(MOBILEGL_ESPRYT_MULTIDRAW_MODE=indirect|multiindirect) move icmd 0 -> 660; Magma's GuiBatch
line moves from buf=0 tex=0 ubog=0 to buf=685.71 tex=41.14 ubog=157.71 with tex[emit=9
box=9 rect=0 jobs=9]. Off-path A/B against the base tree with the env unset, 9 runs each of
a 40-scenario draw slice, sorted totals in ms: Espryt base 389..794 (median 399) vs branch
384..403 (median 394); Magma base 406..472 (median 410) vs branch 408..761 (median 414) -
the always-compiled guard is below this harness's noise on both backends.
- 1410 unit tests green (15 PipeStatsTest). integration-gpu 860/860, 860/860, 859/860; the one
failure is DirectVulkan.PointSizeDemotion...TheDemotionIsActuallyArmedWhenTheEnvironmentPins-
ItOn, a member of the load-dependent *IsActuallyArmed* flake family already present in the
untouched base tree, and it passes 8/8 standalone here. stdio gate and gen_pipe check green.
- The sites of plan B section 2.3.1, verified against dev@81b17c0b (the plan's own line
numbers for DirectGLES drift by 4-9 lines; the DirectVulkan ones are exact):
SyncRenderState is DirectGLES.cpp:1994 with the version read at :1998 and the
early-out at :2007-2010 (plan says 2003 / 2007 / 2016-2018); SyncNeccessaryTextures
at :1511 (plan :1520); CurrentUnitBindingsEpoch at :1412-1435 (plan :1418-1436);
PrepareForDraw at :2907-2968 (plan :2916-2976); the global-UBO upload at :3355-3397
(plan :3369-3392); TrySetupDrawFastPath :5994, GetOrCreatePipeline :4948-4993,
ApplyDynamicDrawStateTail :5871-5893 and UniformManager ResolveUniformBufferPayload
:2022/:2052 all as cited.
- Accessor counting is STATIC TALLIES at ten hot entry points, not a wrapper around the
293 pGLContext-> sites: each instrumented function adds the number of accessor calls
its own body made on the path taken. Reads inside callees, and every conditional read
(the sRGB capability in SyncRenderState, the XFB probe and the version-gated parameter
fetch in TrySetupDrawFastPath, the cull-mode/logic-op/tessellation reads in the
pipeline payload builder) are excluded, so the number is a consistent LOWER bound. The
full inventory of what is and is not counted is the header comment of PipeStats.cpp.
- The Magma fast-path gate is counted from SetupDraw, not from inside
TrySetupDrawFastPath: that function has 27 decline returns and one success return, and
counting at the caller is the only shape that cannot miss one.
- The texture upload counts the SHAPE (union box vs N-rect list) separately from the
bytes, because SSIM is blind to the shape and the +6 ms/frame Mali regression of
section 7.3 was a shape regression, not a byte one.
- Frame boundary: DirectGLES::Present after the ring upkeep, and DirectVulkan's backend
Present rather than VulkanRenderer::Present - the latter has an early return for the
no-usable-swapchain case, and a suspended frame is still a frame the counters close.
- Measured on lavapipe/llvmpipe with MOBILEGL_PIPE_STATS=1, GuiBatchScenario, 14 frames
and 26 draws: Espryt 20.65 accessor calls per draw (gates ers 22/18, etl 71/9, eub
71/9), Magma 15.54 (mfp 12/14, mpm 0/14, mdt 12/14). Both land inside the 10-25 band
section 2.3.1 predicted and far below the 124/169 static counts, which is the
correction that section was written to force.