- ForEachLive walked with a range-for and handed fn a reference INTO m_slots, so a callee
that reached GetOrCreate on the same table would resize the vector under both. Index loop
and a copied twin, the shape ReclaimDeadSlots already uses. The one caller today happens
not to insert; that is not a property the walk should depend on.
- GetOrCreate(nullptr) reset the parking twin on EVERY call, so a second null call destroyed
what the first was handed. The map arm kept its null-keyed entry until a sweep, so this
was an arm difference in the one path (SyncTextureObjectToBackend) that documents relying
on the tolerance. It now keeps the parked twin, and the case makes a second call.
- The one-entry memo's comment claimed the three per-draw resolution paths ask for the same
object every draw. Two of them do not: BindCurrentFBO resolves both targets in a frame and
ResolveUnitSamplerBackend asks per texture unit, so both thrash a single-entry memo and
pay a probe P1 did not. The comment now says so and names the fix (per-unit / per-target)
and the gate that would price it (G11, device-side, owed).
- HandleOf caches a NULL answer too - deliberate, because a bound-but-never-synced object
would otherwise re-probe every draw - and what makes it safe is that GetOrCreate refreshes
the memo. Nothing pinned that; RepeatedLookupsOfALiveObjectKeepOneHandle now does.
- Removed the dead #if MOBILEGL_PIPE_PUSH nested inside #if MOBILEGL_PIPE_PUSH in
ScopedDetachedTextureFramebufferAttachments.
- e2 was landed for two of six kinds, so Texture, Framebuffer, SamplerCso and
VertexElementsCso still discovered death in a sweep and ROADMAP.md:18's "delete the GC"
was undelivered. TextureObjectBase (the one base every concrete texture derives from),
FramebufferObject, SamplerObject and VertexArrayObject now raise
NotifyStateObjectDestroyed from their destructor, on RenderbufferObject's pattern: out of
line, declared only under MOBILEGL_PIPE_PUSH, so the pull build keeps its implicit
destructor and its symbol set (G1 still 0 added / 0 removed / 0 renamed and 0 resized
against p2/contract).
- With all six announcing, BackendSlotTable loses BOTH sweep drivers: no draw tick, no
creation tick, no kGCInterval / kCreationGCInterval / m_gcTick / m_creationTick.
CollectGarbageIfNeeded() is empty on this arm; CollectGarbageNow() stays as an EXPLICIT
collection and is the backstop for a notice that InProcessTeardown() drops.
- The seven CollectGarbageIfNeeded call sites in DirectGLES.cpp keep their spelling because
they are the legacy registry's driver and that arm is still compiled beside this one; the
registry's body is now guarded on MOBILEGL_PIPE_LEGACY_MEMOS, so a build without the
legacy arm has no collector at all. On the handle arm each site is a predicted branch.
- The weak_ptr per entry stays for exactly two jobs it is honest about: ForEachLive()'s
strong hand-over to ScopedDetachedTextureFramebufferAttachments, and the explicit
collection. It is never an identity test; Gen is.
- Tests: AProgramAndARenderbufferAnnounceTheirOwnDeath becomes
EveryReKeyedObjectClassAnnouncesItsOwnDeath and drives all six classes, by membership
rather than count because every texture owns a private sampler that also announces;
ObjectChurnAloneDrivesTheSweep becomes
AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep and pins that 256 churned
objects hold one live twin at a time with no CollectGarbage* call anywhere.
- 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).
- UnitBindingsSnapshot was split by #if MOBILEGL_PIPE_PUSH, so a push build ran P2's
lifetime-id debounce on the MOBILEGL_PIPE_PUSH=0 arm too. That arm has to reproduce P1
(ConfigLoader.cpp), or the integrator's A/B measures this slice's mechanism on both sides
and attributes it to neither - the same complaint g_fbSlotCache was already fixed for. The
snapshot now carries P1's WeakPtr fields beside the lifetime ids whenever the legacy arm
is compiled, and Capture/Unchanged pick by EsprytSlotTablesEnabled(). The two answers are
equivalent (OwnerEquals on two empty pointers is true and LifetimeIdOf(nullptr) == 0 == 0;
a live-versus-expired control block and two distinct lifetime ids both compare unequal),
so this is A/B fidelity, not a behaviour change, and a build with no legacy arm carries
neither the fields nor the branch.
- TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin deliberately never swept, so it left
a live MGPipeKind::Query slot behind for good, and ObjectChurnAloneDrivesTheSweep reads
HighWater/LiveCount of that same process-global kind. Deltas made them pass today, but
--gtest_shuffle or a third case on kind Query would have made them interact. The
two-holder case now has a kind to itself and returns its slot at the end.
- Its comment claimed two live tables of one kind "cannot arise outside this case". They
can and do: ScopedDirectGLESTextureBindings holds a second live table of kind Texture, and
package D's subsystem 4 re-keys VaoDrawMemo out of the same per-kind allocator. The
comment now states the real hazard (whichever holder frees first orphans the other's
entry; safe, because Free is generation-guarded and FindByHandle compares Gen, but not
free) and flags it for the integrator.
- 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.
- SyncTextureObjectToBackend re-resolves its slot after the nested glTextureView sync. The
assert that it is still there is right - the caller holds the frontend object, so nothing
can reclaim its slot - but the function returns a REFERENCE, and in a release build the
assert is gone and the deref is not. It now falls back to GetOrCreate and puts the twin
the caller is about to use back.
- ResolveVaoTwin documents why its raw twin pointer survives the whole draw on both arms
instead of pointing at TwinLookupMemo, which the handle arm does not compile.
- ResolveVaoTwin, SyncCurrentProgram and BindCurrentFBO stop consulting the three
TwinLookupMemos on the handle arm. The memo existed to replace the registry hash probe
with an array index, and the slot table Find already IS that array index; its safety
argument - owner-equality of a weak snapshot against a recycled heap address - is
answered by the generation instead of re-derived per lookup. OwnerEquals, the memo
template and its three instances are now compiled only under MOBILEGL_PIPE_LEGACY_MEMOS.
- UnitSamplerLookupMemo compares {slot, gen} instead of owner-equality, and keeps its "a
miss is never cached" contract verbatim: the sampler twin is created later in the same
draw by the program pass.
- UnitBindingsSnapshot holds lifetime ids rather than weak_ptrs under push. It cannot hold
handles: a bound-but-never-synced texture has no twin and so no handle, and two of those
would read as equal. A lifetime id exists before the twin does and is never handed out
twice, which is the property the weak_ptr was there for.
- SyncTextureObjectToBackend keeps its by-value copy only to hold the twin alive across the
nested glTextureView sync; the second Find-or-create and the put-the-twin-back repair are
gone on the handle arm, because nothing there erases a live entry.
- The one direct-iteration site walks ForEachLive, which hands over a strong reference to
the framebuffer instead of the map key - the raw frontend address it had to null- and
expiry-check by hand before dereferencing.
- GetFramebufferBindingSlotFast becomes GetFramebufferBindingSlotChecked and, under push,
reads MGB_CTX->GetFramebufferBindingSlot(target) every time. This closes the P1 accessor
bypass: the cached raw pointer ran the checked accessor once per context change and then
handed out the pointee forever, so the per-verb poison stamp and the verify read-hook
were skipped at all five call sites.
- Every one of these is a push-build arm; the pull build compiles the pre-P2 text and its
symbol report stays 0 added / 0 removed / 0 renamed with no new resize.
- 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.
- FramebufferSrgb, DepthClamp and TextureCubeMapSeamless get real storage. All three fell
to SetCapability's "not supported currently" arm and IsCapabilityEnabled's default:
glEnable was swallowed and glIsEnabled lied, so DirectGLES' sRGB block and the
DirectVulkan read points consumed a constant. The three Bools land in the three
alignment bytes at [581, 584) between ColorMasks and ClearColor, so
sizeof(RenderStateParameters) stays 1168 and NO existing offset moves - Espryt's
kBlendSpanBegin/kBlendSpanEnd (312/536) and the whole chunk table depend on that.
- MGPipeRenderStateSpans.{h,cpp}: the pipeline/dynamic split, written in exactly one place.
The rule is the only rule - a byte is pipeline state iff a public RenderState setter that
calls BumpVersions() writes it - which makes G7's "the subset hash moves iff
m_pipelineStateVersion moves" true by construction. 16 boundaries, all offsetof or
sizeof, alternating dynamic/pipeline: 8 dynamic chunks / 772 bytes and 7 pipeline chunks
/ 396 bytes, partitioning [0, 1168) exactly, asserted at compile time.
MGPipeComputePipelineSubsetHash is XXH64 over the seven pipeline chunks, seeded with a
table version so a chunk-table change invalidates every persisted key.
- The pipeline subset is now a strict SUPERSET of the 24 members ComputePipelineStateHash
hashed: 44 members, adding sample coverage, the front face, the provoking vertex, the
scissor-test mask, the back polygon mode, eleven capability bools the hash never read and
the three above. Demoting those setters to ++m_version instead would have changed
MG_State semantics in the PULL build for the push path's sake. The hash runs only when
m_pipelineStateVersion moves, which is exactly when Magma re-hashed before.
- PipeApply.{h,cpp}: the in-process applier, the server half of the P2 calls. The server's
working RenderStateParameters IS PipeInputs::m_renderState, which is why DirectGLES'
SyncRenderState is not one line changed and why the verify comparator stops being a
tautology. Per-context CSO store indexed by slot, gen-validated; the residual block's
capability bits are compared against the assembled block, so a capability a later call
takes over and forgets to carry is Fatal{PipeResidualDiverged}.
MGPipeDeriveRenderStateFields is a declared STUB - its 29 derivations are commit c1.
- SlotAllocator.{h,cpp}: the client's per-kind {slot, gen} allocator, free list plus
high-water, first allocatable slot 1, gen bumping only on slot REUSE, a debug assert on
gen wrap, the composite ShaderCso band held back, and a lifetimeId -> slot map per kind so
a GL name never enters a key. In the contract because both Track H slices need it.
- ResidualValueBlock 1248 -> 8 bytes, one Uint64 of capability bits.
RenderStateParameters retired to create/bind_render_state and set_dynamic_state, Pack to
set_pixel_pack_state, the patch quintet to set_patch_state. gen_pipe.py now emits the
member-by-member offsetof assertions the ratchet comment always promised.
- gen_pipe.py: PIPELINE_STATE_MEMBERS grows to the 44-member set in declaration order and
PipeSpanTable.inc's "deliberately absent" block records the answers instead of the
questions; Coverage.def gains MGP_COVERAGE_EMITTED_LIST (34 rows) and PipeFilled.inc
gains kMGPipeFieldEmittedBy[], which is what lets the residual fill loop skip a field a
P2 call now supplies. One more --self-test negative control covers the new list.
- MOBILEGL_PIPE_PUSH becomes a per-subsystem bitmask with named bits (0..6 migrated at P2,
bit 63 the CSO-content-addressing negative control), defaulting to 0x7f in a push build
and staying 0 in a pull build. New CMake option MOBILEGL_PIPE_LEGACY_MEMOS, ON, forced ON
when MOBILEGL_PIPE_PUSH=OFF where it is the only arm. New Features.PipeHandleAbaControl
under MOBILEGL_PIPE_PUSH, negative control C for HandleRecycleScenario.
- PipeStats gains CallClass::{RenderStateCsoMints, RenderStateCsoBinds} (csom / csob on the
summary line), and they are PUSH-ONLY: growing the enum in the pull build would resize
the counter arrays, the name table and FormatWindowLine for two counters that could never
leave zero, and G1 admits no such resize.
- Four MG_Test/Pipe stubs plus their CMake registration, so the packages that own their
contents never touch MG_Test/Pipe/CMakeLists.txt.
G1, pull build, symbol_report --threshold 0: 0 added, 0 removed, 0 renamed, 4 resized, and
every resize is attributed:
RenderState::RenderState() 1700 -> 1848 (+148) the three {}
RenderState::SetCapability(CapabilityInput,bool) 850 -> 927 (+77) three switch arms
RenderState::IsCapabilityEnabled(CapabilityInput) 239 -> 268 (+29) three switch arms
_GLOBAL__sub_I_DirectGLES.cpp 1340 -> 1331 (-9) the static
initialiser of DirectGLES.cpp's `static RenderStateParameters
g_syncedRenderStateParameters` re-scheduling around the three new default-initialised
members. A shrink, and the only unforeseen entry; it is a direct consequence of the
struct gaining members and touches no interface.
- Every MG_State::pGLContext-> in the seven DirectVulkan TUs becomes MGB_CTX->
(DirectVulkan.cpp 12, BackendObject_DirectVulkan.cpp 2, UniformManager.cpp 14,
VkClearManager.cpp 1, VkRenderPassManager.cpp 3, VkTextureManager.cpp 2,
VulkanRenderer.cpp 130 occurrences on 127 lines); each TU includes
<MG_Pipe/PipeInputsSwitch.h> right after its MG_State/GLState/Core.h include
(VkRenderPassManager.cpp after its include block, it never included Core.h).
- The 43 MOBILEGL_ASSERT(pGLContext) / (pGLContext != nullptr) lines become
MOBILEGL_ASSERT(MGB_CTX_LIVE, ...): identical in every INFO build (the macro is
empty there) and still a null-context assert in a DEBUG build.
- The six code-bearing guards are like-for-like pointer tests in the pull arm:
if (MGB_CTX_LIVE) at the two InvalidateCompileEnv sites, MGB_CTX_LIVE in the
XFB query counter condition and the ternary that snapshots the paused-primitive
counter, !MGB_CTX_LIVE in BeginXfbCaptureForDraw, MGB_CTX_LIVE && in the
provoking-vertex resolve. No semantic rewrite: under push MGB_CTX_LIVE is simply
"a context exists", the real meaning of these guards is P2's business.
- VertexInputStateFactory.h's comment stops naming pGLContext so purity gate C
(grep -rc pGLContext MobileGL/MG_Backend/DirectVulkan) reads 0 in every file.
- The 12 SyncPersistentMappedRange and 3 SyncGpuWrites sites of the D10 table are
untouched; PipeStats AddCalls literals unchanged.
- Proof on the pull build (Release/INFO, LTO off, vs feat/disaggregated@087685d1):
symbol_report --threshold 0 -> 27799 symbols, 0 added / 0 removed / 0 resized /
0 renamed, .text 10792579 -> 10792579 (+0); nm --defined-only name set identical;
ctest -N name set identical (2334); unit 1466/1466; DirectVulkan integration lane
427/427 on lavapipe (two ArmedWhenTheEnvironmentPinsItOn entries flake under -j 4
exactly as on the baseline and pass serially). The push build compiles and links.
- 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.
- The seven F-class forwarders are the declared exception to D4's "every accessor body": a forward is a live call, not a stored value, and InvalidateCompileEnv is reached from backend initialisation before any verb has filled, where a check would be Fatal{...@<none>} on every start. Their sticky stamp is consulted by no accessor; the tests pin it through MGPipeInputFieldIsFresh directly. Written down so P2's tracker does not "fix" the missing check.
- MGPipeVerifyReadHook: InHook is what keeps a GetProgramForDraw-triggered backend re-entry from recursing into a second hook, and the whole-field re-read is a per-read cost to keep in mind when reading the verify lane's wall time.
- GetBufferBindingSlot(Index) is polymorphic on GLContext (the bound VAO's element-buffer slot) and null in the block by design: a push Fatal there is not a missing row. No backend reads it today.
- kMGPipeForwardedFieldCount = 7 was a hand-written twin of the generated kMGPipeInputStickyFieldCount, tied only by PipeCatalogue.StickyFieldsAreExactlyTheSeven; a static_assert in the header makes a PipeFields.def sticky row without a forwarder a build error instead of a test failure.
- Entry compare: at the end of MGPipeFillForVerb a file-static second PipeInputs is filled by SnapshotFromGLContext (the branch that survives P13) over the same class mask, MOBILEGL_PIPE_VERIFY_CORRUPT perturbs one field of that snapshot arm, and MGPipeVerifyInputs compares every field in the mask through MGPipeInputsFieldEqual (V by G4's MGPipeFieldEqual, O by identity, F equal by definition). Both arms come from the same context at the same instant, so this arm is tautological until P2 - the CORRUPT knob keeps it falsifiable.
- Compare-at-read: MGP_INPUT_VERIFY_READ now calls MGPipeVerifyReadHook(*this, field, i0, i1), which re-reads the whole field from the live context into a scratch block and compares it against the stored value on the live block only - a superset of "the same indices"; the indices decorate the report. This is the arm that is real in P1.
- Reporting per D8: MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"Field@Verb\", verb=<serial>, where=entry|read}") then abort unless MOBILEGL_PIPE_VERIFY_FATAL=0, which counts and summarises at teardown with MGLOG_E; arming logs "MGPipe: verify armed - 63 fields, 69 verbs, fatal=N" once, the knobs acknowledge themselves, an unknown field name is Fatal{PipeVerifyBadKnob}; a push build without the comparator answers MOBILEGL_PIPE_VERIFY=1 with one MGLOG_W_ONCE.
- MGPipeVerifyInputs carries default visibility so the retrace-verify job's nm -D probe can prove the verify library was the one swapped in; pointer corruption flips low bits instead of nulling (a null pointer already null was invisible to the compare), a SharedPtr becomes an aliasing pointer with no control block; CurrentVertexAttributeValue gets its own bitwise equality.
- MGPipeFillForVerb now walks kMGPipeClassFieldMask[kMGPipeVerbClass[verb]] and copies every field in it by calling the GLContext accessor of the same name (MGPipeFillAccess::CopyField, one switch over the 56 stored fields; the seven forwarded fields copy nothing), stamping each with the new serial; the sticky seven get FilledGen = 1 on the first live fill through the same mask walk, since every class mask carries them.
- MOBILEGL_PIPE_POISON_OMIT (<Verb>:<FieldName>) is parsed once on the first fill and MGPipeSetPoisonOmission is the programmatic form for the unit tests; the omitted pair keeps its value copy and loses only its stamp, so the omission is indistinguishable from a forgotten FillPoints.def row. An unknown name is Fatal{PipeVerifyBadKnob}; a non-poison push build acknowledges the knob with one warning because no stamp exists to omit.
- PipeInputs names one friend, struct MGPipeFillAccess, instead of two friend functions, and VisitStorage gains a const overload for the comparator that follows.
- MG_Pipe/PipeInputsSwitch.h is the strangler switch (ARCHITECTURE.md 9.2): MGB_CTX is the
live GLContext in the pull build and &gPipeInputs under MOBILEGL_PIPE_PUSH, so the pull
arm's pGLContext spelling stays outside MG_Backend/ and purity gate C's grep.
- MG_Backend/MGPipe/PipeInputs.h holds one struct with every accessor a backend reads (63:
the 61 Coverage.def rows plus GetBoundTransformFeedbackLifetimeId and
HasOpenTransformFeedbackSpan), each keeping its GLContext name, parameters and return
type so the site conversion is type-neutral; V fields are copied values, O fields are
SharedPtr copies or pointers into the context, the seven F fields forward to the live
context from MG_Impl/Pipe/PipeFill.cpp and are the only sticky ones (Coverage.def's
MGP_COVERAGE_STICKY_LIST argues each: argument-keyed lookups and reverse-channel writes,
never a version or generation counter).
- MG_Pipe/FillPoints.def is the verb table: one row per GLFunctionsTable function pointer in
declaration order (69), nine classes and the may-read field rows; gen_pipe.py parses the
struct and refuses a row set that is not exactly its member set, then emits
generated/PipeFillPoints.inc (verb enum, class tables, per-class field masks with the
sticky fields OR'ed in). MGP_FILL(Verb) in MG_Impl/Pipe/PipeFill.h is the fill point;
MGPipeFillForVerb only bumps the serial, records the verb and stamps the sticky fields
here - the per-class copies land in the next commit, the fill points in MG_Impl after.
- MOBILEGL_PIPE_POISON is derived once in PipeInputs.h from MOBILEGL_PIPE_PUSH and the DEBUG
level, MOBILEGL_BUILD_DISAGGREGATED or MOBILEGL_PIPE_VERIFY (the tree has no
MOBILEGL_DEBUG); under it every accessor is a read-side freshness check that aborts with
Fatal{UnmigratedPipeInput, "Field@Verb"}.
- CMake: MOBILEGL_PIPE_PUSH and MOBILEGL_PIPE_VERIFY options (VERIFY forces PUSH on), the two
new sources appended only under PUSH, the compile definitions; Config.h/ConfigLoader.cpp
gain PipeVerifyFatal / PipeVerifyCorrupt / PipePoisonOmit under #if MOBILEGL_PIPE_PUSH so
the pull build's FeaturesTable does not change size.
- Pull build proof: symbol_report.py against the 087685d1 baseline reports 0 added / 0
removed / 0 resized / 0 renamed and a .text delta of 0; ctest -N names unchanged; gen_pipe
--check clean; unit tests green in the pull, push and verify builds.
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself.
- The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs.
- PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct.
- AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers.
- Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green.
- VulkanRenderer::CurrentXfbCounterSlot keyed m_xfbCounterSlotByObject on
GetBoundTransformFeedbackName(). glGenTransformFeedbacks hands a deleted name
straight back (IndexGenerator is LIFO) and nothing ever removed a map entry, so
a transform feedback object created on a recycled name was served the DEAD
object's counter group - and with it that group's m_xfbCountersValid and
m_xfbLastSeenGeneration entries, which are the resume/fresh decision for
vkCmdBeginTransformFeedbackEXT. This is D21 in plan B v2 4.7.3, the one entry
in that table whose today-key "guards nothing", and 10.4-5 asks for it to land
on dev on its own - hence this separate commit, kept in files no other commit
on this branch touches so the cherry-pick applies unaided.
- Frontend: TransformFeedbackObjectState gains a never-reused `lifetimeId`
through a default member initialiser, so every route into existence
(operator[] materialisation, `= {}` in GenTransformFeedbackNames and
CreateTransformFeedbackObject) mints a fresh one and a recycled name cannot
carry the dead object's id back. The allocator is the same shape as
BufferObject::AllocateLifetimeId (atomic, starts at 1 so a zeroed backend slot
is never a live object).
- The bound object's id is mirrored in m_boundTransformFeedbackLifetimeId,
refreshed by RestoreBoundTransformFeedbackState - which every bind, and the
revert that deleting the bound object performs, goes through - and seeded for
the default object by the GLContext constructor. GetBoundTransformFeedback
LifetimeId is therefore a const load. Reading it through operator[] instead
would have been an INSERT on the per-draw path, and UnorderedMap is
ska::flat_hash_map, whose rehash invalidates every reference into the
container, not just its iterators.
- Backend: the UnorderedMap is replaced by a fixed 16-entry owner table, which
fixes the second half of the same defect - the map was keyed on a value that
recycles yet was never pruned, so it grew for the life of the context. With
lifetime ids as keys a map would have grown without bound instead, so the
bounded table is required, not cosmetic.
- Slot exhaustion: past sixteen owners a group has to be taken over, and the
victim is chosen among owners with NO OPEN SPAN, which
GLContext::HasOpenTransformFeedbackSpan answers; an identity no live object
carries any more answers false, and that is what lets a dead owner's group
come back. Least-recently-used ALONE would have been exactly the wrong rule:
GL only permits another object to capture while this one is PAUSED, so the
paused span these groups exist to protect is by construction the least
recently used entry, and an LRU takeover would reset the one resume offset
that still matters. LRU is now only the tie-break among reclaimable groups.
Sixteen genuinely open spans at once is reported (MGLOG_E_ONCE) rather than
resolved silently, because whatever is taken then restarts at offset 0.
- Not done, and why: the natural place to hand a group back is
glEndTransformFeedback, but registering DirectVulkan's EndTransformFeedback
table entry would flip the test FixupGsStripCaptureOrder makes of that same
pointer (GL_Drawing.cpp:1255) to decide whether the backend already captured
in GL's vertex order, silently disabling the geometry-stage strip fixup for
DirectVulkan. Giving that discriminator a name of its own is a separate
change; until then the no-open-span rule is what keeps the table honest.
- CurrentXfbCounterSlot asserts the identity is never 0. Zero is the free-slot
sentinel, so an identity of 0 would match every free slot as "mine" without
ever claiming one - this bug reintroduced, with no symptom at the call site.
- TransformFeedbackLifetimeIdTest, in its own translation unit, pins the
frontend halves: an object created on a recycled name must not report the dead
object's id, the default object has an identity before anything binds it, and
a PAUSED span still reads as open while another object is bound and capturing
- which is the whole correctness argument for the eviction rule. The name
reuse is not simulated: the test asks the real generator and skips (loudly) if
it never recycled. Still untested: the >16-owners path itself, which needs a
backend scenario with seventeen capturing objects and there is none.
- Negative controls, each applied then reverted: making a non-bound object's
span read as closed reddens APausedSpanStaysOpenWhileAnotherObjectCaptures;
making a vanished identity read as open reddens the same case on its delete
assertion; dropping the constructor's seeding reddens
AnObjectAtARecycledNameCarriesAFreshLifetimeId.
- Tested: cmake --build build-linux -j 24 (clean, 166 targets); ctest -L unit
-j 12 -> 1386/1386 passed; ctest -L integration-gpu -> 866/866 passed
serially, and 866/866 on one of two -j 8 runs. The other -j 8 run failed
DirectGLES.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnviron
mentPinsItOn, a member of the pre-existing parallel-ctest flake family: it
passes in isolation here, and the unmodified parent tree
(~/w7/p0-noop-wins-base) reproduces the same family under -j 8.
- GLFunctionsTable had three entries that are frontend queries wearing a
backend interface (plan B v2 §2.1(a)): GetIntegeri_v, GetInteger64i_v and
GetProgramiv. Two of them have NO caller at all - `grep -o
'gBackendFunctionsTable\.GL\.[A-Za-z_0-9]*'` outside MG_Backend/ lists 69
distinct entries and neither GetInteger64i_v nor GetProgramiv is among them -
so both table slots, both backends' implementations and both registrations are
deleted here. This is plan B §11 P0's first "strictly no-op free win".
- Nothing was moved into MG_Impl, because MG_Impl already answers all of it.
glGetInteger64i_v is served by GL_Getter.cpp:1240-1314, which handles the
indexed buffer queries itself and derives every other pname from its own
GetIntegeri_v ("Handing the leftovers straight to the backend instead made
glGetInteger64i_v disagree with glGetIntegeri_v on the very same pname").
glGetProgramiv is served by GL_Program.cpp, whose GL_COMPUTE_WORK_GROUP_SIZE
arm (:928-946) reads ProgramObject::GetComputeLocalSize - a link artifact of
the program the APPLICATION wrote, which is the only program in the
application's namespace. §4.7.1 class B.
- GetIntegeri_v stays, but only for what a backend genuinely owns. Its pure
frontend arms were unreachable: GL_Getter::GetIntegeri_v answers
GL_SHADER_STORAGE_BUFFER_{BINDING,START,SIZE} through
TryDecodeIndexedBufferQuery (:991-1029) and the six GL_IMAGE_BINDING_* pnames
at :1115-1153, and returns before touching the table. That left 9 dead cases
in DirectGLES.cpp and 9 in DirectVulkan.cpp - the plan's "15" undercounts the
two files separately. What still arrives is GL_MAX_COMPUTE_WORK_GROUP_COUNT /
_SIZE (GL_Getter.cpp:1161-1177, and MG_Util/ShaderTranspiler/CompileEnv.cpp
:134-138 asks the table directly), so DirectVulkan keeps exactly those two and
DirectGLES becomes a plain driver passthrough.
- The dead arms were also WRONG, which is why deleting rather than reconciling
them is the strict no-op: they clamped a bound range's size to the buffer's
current storage, while the frontend reports the size glBindBufferRange was
asked for verbatim (GL 4.6 core tables 23.4/23.5 - the clamp answered 0 for
KHR-GL43.shader_storage_buffer_object.basic-binding's shape). Had a later
refactor made the table the answer, the regression would have been silent.
- ProgramResourceCache::computeWorkGroupSize and the spirv-reflect entry-point
loop that filled it go with DirectVulkan's GetProgramiv; nothing else read it.
- Three cases added to AdvertisedLimitsScenario pin what the frontend answers,
on both lanes: the indexed SSBO binding/start/size on the 32- and 64-bit
widths INCLUDING a shrink of the store underneath the binding (the arm that
actually separates verbatim from clamped), the six image-unit pnames on both
widths, and the compute local size plus the INVALID_OPERATION a program with
no compute stage must give.
- Both new gates were shown to go red for their reason: making
GL_COMPUTE_WORK_GROUP_SIZE answer a defaulted (1,1,1) fails
ComputeLocalSizeComesFromTheLinkedProgram on both lanes, and re-introducing
the deleted store clamp in GL_Getter fails
IndexedBufferBindingsAreReportedVerbatimOnBothWidths on both lanes.
- Tested: cmake --build build-linux -j 24 (clean); ctest -L unit -j 12 ->
1382/1382 passed; ctest -R AdvertisedLimits -> 18/18 passed (6 pre-existing +
3 new, x DirectGLES and DirectVulkan).
- 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.