mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
43bf97cc8784a484c4ac764df63afcfe198f0e57
2709
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
43bf97cc87 |
[Test] (Pipe): pin the tracker's shutters and the CSO cache's content addressing, including the collision the memcmp exists to stop
- TrackerWalk drives the tracker and the cache DIRECTLY rather than through MGPipeValidateForVerb: the validate point reaches the library's one process-wide tracker, and a unit test that asserts on a shared singleton fails the moment ctest runs the suite in parallel. The three lines of emission logic it reproduces are the same three lines. - BlendToggleReusesTwoCsos is the Blaze3D shape the whole "push at validate, not in the setter" decision was made for: 32 enable/draw/disable/draw pairs mint exactly TWO CSOs, bind 64 times and hit 62. A per-setter design would show up here as 64 mints. - ViewportDoesNotMintACso is the regression RenderState.h records: 16 glViewports mint nothing, never move the pipeline version, and each sends exactly chunk D0 - not the other seven. - WrapAroundRePushesButNeverMisses drives m_version past 65535 and asserts every one of 70000 changes fired. The alternating value deliberately never touches the default: a setter that early-outs would otherwise make the first iteration a false miss and hide a real one. - AggregateGenerationCatchesABoundTextureMoving is the first test of the direction the P1 verify comparator cannot see - it compares object-class fields by identity only, so a bound texture whose content moved looks unchanged to it. The bit fires and then settles, so it is a shutter and not a stuck flag. - ANaNPatchLevelEqualsItselfAndDoesNotFireForever: a NaN outer level is a legal glPatchParameterfv value, float equality says it differs from itself and a byte compare says it does not. That is why the shutter is a memcmp. - ThePixelPackShutterIsAByteCompareOfThePackHalfOnly asserts an UNPACK write does not move the pack shutter, which is the half that deliberately has no carrier. - HashCollisionDoesNotAliasTwoStates needed a seam and got one: MGPipeCsoCache::s_hashForTest, null in every real build, one never-taken branch on a path that runs only when the pipeline version moved. Without it the memcmp confirm is unreachable code that nothing can prove is doing anything, and what it stops - two different render states on one CSO - is silent wrong pixels with no gate that can see it. - ContentAddressingOffMintsEveryTime pins that bit 63 really changes mint/reuse behaviour, so the negative control cannot rot into a dead switch. - The set-hash suppressor is exercised on all seven slots even though P2 wires one, including the reserved-zero contract: a computed hash of 0 is remapped to 1 so it is never confused with "never emitted". |
||
|
|
3302ee82b5 |
[Feat] (Pipe): map every frontend mutator onto the aggregate generation that publishes it, and make the dirty-surface scanner a gate
- MG_Pipe/DirtySurface.def: 73 rows, one per distinct mutator the scanner finds, each answering
"what publishes this". The answer vocabulary is a MGPipeDirty bit name or one of five
non-bit answers, and each of the five is documented in the file's header rather than left to
be inferred: kImmediate, kReverseChannel, kNoBackendRead, kExplicitDestroy and
kPulledEveryVerb. Where a mutator has more than one true answer the row carries the COARSER
one - the one that cannot under-fire.
- gen_pipe_dirty_surface.py --check is the gate and it fails in BOTH directions: an unmapped
mutator renders stale, and a row naming a mutator the scan no longer finds keeps a real hole
looking covered. It also rejects an answer that is neither a documented non-bit answer nor a
bit name read out of Tracker.h's own kMGPipeDirtyNames, so a renamed bit cannot leave a row
silently pointing at nothing.
- --self-test runs three canned negative controls - a withheld mutator, a stale row, a bad
answer - and each must trip; trips == 0 is itself an error, the shape
check_include_closure.py and gen_pipe.py --self-test already use. ROADMAP.md's rule is that
every gate must be able to go red for the reason it exists.
- --summary keeps working unchanged, because the CI file that still calls it belongs to
another package until it lands.
- The human report prints the mapped answer where it printed UNMAPPED.
- FillPoints.def: the verdict on the eight statically over-approximated rows, recorded per
group in the def's own comment. All eight are KEPT and the reason is the same in all three
groups - each row names a concrete backend path (the depth/stencil read emulation's paused
capture, VkClearManager::PreCompensateSrgbClearColor's GL_FRAMEBUFFER_SRGB read, the shader
blit's viewport / provoking vertex / binding-point reads), and the only evidence that could
retire one is dynamic. A corpus that never reaches a path proves nothing about it, and a row
dropped on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build.
The contract's new FramebufferSrgb storage in fact makes one of the eight MORE load-bearing
than it was, not less: it used to read a compile-time constant.
|
||
|
|
7dec32a574 |
[Feat] (Pipe): carry what has no call of its own in the residual value block and abort when it disagrees with the assembled state
- set_residual_value_state emits the 35 capability bits, read from the FRONTEND's own
IsCapabilityEnabled rather than from the assembled mirror. That direction is the whole
design: the applier then compares the carried answer against the assembled one, so the block
is an independent oracle instead of a tautology - which is the failure the P1 entry compare
had and P2 is paying to remove.
- It goes out AFTER the residual fill, not with the other emissions: the mirror the trip wire
compares against is written either by the applier's derivation or by that fill, so before it
the block would be compared against the previous verb's answer.
- It is HELD, not dropped, when the verb's class does not carry IsCapabilityEnabled. kQuery and
kXfbSpan do not read it, so at those verbs the mirror is stale by construction; a capability
that moved between two queries would silently disarm the wire if the emission were skipped
instead of deferred.
- ByteClass::ResidualValueBlock has been a placeholder that "stays at 0 until P2" since P0.
This makes it non-zero, which is half of G10.
- The wire is not theoretical: the first version of this commit fired it for real -
Fatal{PipeResidualDiverged, "Dither"} carried=1 assembled=0, on every verb whose class does
not read the capability mirror - and that is what the holding latch above is for. GL_DITHER
defaults to enabled, so the very first mismatch the block could have found is the one it
found.
- integration-verify 818 green, integration-gpu 878 green under the default bitmask and again
under MOBILEGL_PIPE_PUSH=0, unit 1499 green.
|
||
|
|
dcfa5ad311 |
[Feat] (Pipe): push pixel-pack, patch and vertex-attribute-default state as their own calls, the last one behind the set-hash suppressor
- set_pixel_pack_state on NEW_PIXEL_PACK, set_patch_state on NEW_PATCH_STATE, set_vertex_attrib_defaults on NEW_VERTEX_ATTRIB_DEFAULTS, each gated on its own runtime subsystem bit so the bitmask stays a per-subsystem A/B. - MG_Impl/Pipe/SetHashSuppressor.h: seven slots, one per kVarTail set_*, with SetVertexAttribDefaults the one P2 wires. 0 is reserved for "never emitted" and a computed 0 is remapped to 1, so the first emission always goes out. The other six are the carrier for the ~175 lines of backend debounce that move in P3b/P4b; landing the mechanism now means the shape is pinned by a test rather than by a plan. - The var-tail carries only the attributes that differ from the tracker's mirror, underneath the set-hash suppression of the whole resolved set - the two suppressors answer different questions and both are cheap. Two rows of Coverage.def's emitted list CANNOT yet retire their pull, and each says why in the code rather than being silently absent: - GetPixelStoreParameters is BOTH halves of the pixel store and set_pixel_pack_state deliberately carries only PACK, so the unpack half has no carrier at all. The field keeps being pulled and the verify comparator keeps proving it. - GetCurrentVertexAttribute's three views are not bit-identical - GLContext CONVERTS between them - while MGPipeApplySetVertexAttribDefaults memcpys one Data[4] into all three and ignores MGPAttribValue::ValueClass, which the wire type carries precisely so it does not have to. Until that applier reads ValueClass the carrier cannot reproduce the frontend value. The call is still emitted, so the wire shape, the payload bytes and the suppressor are all real; the residual fill runs after emission, so the mirror ends up correct either way. Both are contract-side defects in files this package does not own; they are reported to the integrator with the exact fix rather than worked around here. |
||
|
|
aa64c91052 |
[Feat] (Pipe): mint render-state CSOs on the pipeline subset and send only the dynamic chunks that moved - the steady-state cost of the whole render-state family is now two Uint16 compares
- MG_Impl/Pipe/CsoCache.h: 64 entries, LRU, hash -> probe -> MEMCMP -> handle. The memcmp is not optional: a bare 64-bit hash equality would let a collision alias two different render states onto one CSO, which is silent wrong pixels with no gate that can see it, and Mesa's cso_cache memcmps for exactly that reason. It runs only when the pipeline version moved, so never in the steady state. Eviction emits delete_render_state and frees the client slot. - kMGPipeBehaviourNoCsoContentAddressing (bit 63) turns off the PROBE and the handle reuse, not the records: every pipeline-version change then mints, binds and evicts, which is the whole-block content addressing the design is measured against. - The validate point's step 3: bind_render_state when the pipeline version moved (12 bytes, no hashing, no blob when the cache hits), set_dynamic_state when m_version moved, carrying only the dynamic chunks that differ from the tracker's staging mirror. An EMPTY chunk mask still sends the 32-byte header, because the version is what Magma's dynamic tail gates on and it moved. - The residual fill now skips a field a P2 call supplies, driven by the generated kMGPipeFieldEmittedBy[] and gated per subsystem on the runtime MOBILEGL_PIPE_PUSH bitmask, so the bitmask is a true per-subsystem A/B. THE STAMP IS UNCHANGED: a stamp says "this verb published this field", which is as true of an emitted field as of a copied one, and withholding it would abort every backend read of the fields the migration just took over. - PipeStats::RecordDrawPayloadBytes has been implemented, unit-tested and called by nothing since P0. This is its first emitter. - A field that reaches PipeInputs only through MGPipeDeriveRenderStateFields is skipped only when that derivation is really there. It is package A's and is a declared stub on the p2/contract tag this branch starts from, so rather than hard-code which branch this is, the filler probes once: a sentinel in a scratch block, the mirror cleared, the derivation run, the answer latched. It stays useful after A lands - if the derivation is ever deleted the filler degrades to PULLING those fields rather than rendering a default. - integration-verify, 818 entries, green: the comparator re-reads every field from the live context at every backend read, so "the assembled block equals the live context" is now proven rather than asserted, and the entry compare has stopped being a tautology. |
||
|
|
fcd4ad3799 |
[Feat] (Pipe): compute the per-verb dirty mask at the validate point and count how often each bit fires
- MGPipeFillForVerb becomes MGPipeValidateForVerb and MGP_FILL expands to the new name. The
macro spelling, the 83 call sites and the verb enum do not change: the dispatch is
kMGPipeVerbClass's nine classes, which is the same code as nine named ValidateFor* entry
points with one call site per verb instead of nine (P2 brief D1, against ARCHITECTURE.md's
eight - FillPoints.def argues in its own comment for splitting kProgramOp out, and the landed
table is what runs).
- MG_Impl/Pipe/Tracker.h: MGPipeDirty's 18 bits, the widened Uint16 shutters, the per-verb walk
and the per-bit-per-verb-class fire tallies. The widening happens in the TRACKER and MG_State
is not changed for it; a wrap costs one extra re-push and never a missed one.
- NOTHING IS EMITTED YET. The mask is computed, latched and counted, and the full P1 residual
fill runs after it unchanged. That is the point of this step: it says the walk is
semantically free before any field stops being pulled, so a regression in the next commit
cannot be blamed on the walk.
- Every shutter over-fires on purpose. Bits 2 and 3 are BYTE compares, not value compares,
because a NaN patch level is a legal glPatchParameterfv value and has to equal itself; bits
5..17 are composed with a mixing hash, which can in principle collide, and that is stated in
the file and is acceptable only because nothing consumes those bits in P2.
- The shutter for bits 6..8 reads the current program's version counters WITHOUT
GetProgramForDraw, so the walk never joins a pending link to answer "did the shader move".
- Header-only rather than Tracker.{h,cpp}: the root CMakeLists.txt that would have to name a
new .cpp belongs to package A and is frozen behind the p2/contract tag. One translation unit
in the library includes it, so inline costs nothing, and splitting it out is one list(APPEND)
line whenever the ownership allows.
- MG_Test/ScopedPipeVerb.h and MG_Test/Pipe/PipeInputsTest.cpp follow the rename. Five comment
references to the old name live in package A's files (MGPipe.h, the two generated .inc, and
gen_pipe.py) and are deliberately left for their owner.
|
||
|
|
de532f55a9 |
[Feat] (State): give the frontend six aggregate generations so a tracker can answer "did any bound texture, buffer, attachment or attribute move" with one Uint64 compare
- MGP_NOTE_AGGREGATE(Aggregate) next to MGP_NOTE_MUTATION in MG_Pipe/PipeMutation.h, ((void)0) in the pull build for the same reason and with the same shape. It answers a DIFFERENT question from MGP_NOTE_MUTATION - "did any object of this class move since the tracker last looked", not "did a backend move a frontend value inside its own verb" - which is why it is a second macro rather than an overload. - The counters are members of the owning MG_State container (VertexArrayState, FramebufferState, TextureState x2, BufferState) and are reached through a push-only GLContext facade, because the bump points sit on OBJECTS and an object has no back-pointer to the state that owns it. That is the free-function form P2 brief D4 allows, and it costs a global load on a path that has just written object state. - A SIXTH aggregate, VertexAttribDefault on GLContext, which D4 does not list. Its bit (NEW_VERTEX_ATTRIB_DEFAULTS) is specified there with a ContentHash over all 32 CurrentVertexAttributeValues, and hashing 768 bytes on every draw does not fit inside the T1 ceiling the same brief pins. The hash still decides whether to EMIT (D11's set-hash suppressor); the generation decides whether to hash at all. - 30 bump points: 3 VertexArrayObject config-version sites, 3 FramebufferObject object-version sites (one of them inside MOBILEGL_DEFINE_FRAMEBUFFER_DEFAULT_SETTER, so the statement carries its own line continuation), 5 texture content-version sites, 12 texture params-version sites, SamplerObject::BumpVersion as the sampler choke point, 7 BufferObject change-serial sites and the 3 glVertexAttrib* defaults. - Every counter is deliberately COARSER than the state it guards: over-firing costs one extra push, under-firing renders stale, and under-firing is the direction ARCHITECTURE.md 13.2 names as the dangerous one and the P1 verify comparator cannot see for object-class state. - TrackerTest: each bump point moves ITS aggregate and no other, plus a null-context note. - G1: the pull build is 0 added / 0 removed / 0 renamed and the four resized symbols are the contract commit's own, unchanged by this commit. |
||
|
|
3d1a866e82 |
[Test] (Espryt): pin that the armless cases put the operator's log path and MG_Config::Features back as they found them
- TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem drives ScopedLogFileRedirect and ScopedArmlessKnobPair with and without a pre-set MOBILEGL_LOG_FILE_PATH and asserts the restore byte for byte - in a build without the legacy arm the verdict is Handles whatever the knobs say, so the case pins the knob values rather than the NoArm verdict there |
||
|
|
df784c6752 |
[Fix, Test] (Espryt): deliver a death notice to every holder of a kind, retire the twin table's last sweep, and pin the bring-up call site under the armless knob pair
- 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) |
||
|
|
c9dd173201 |
[Fix] (Espryt): close four review minors on the twin table - a walk that can outlive its vector, a null call that is not arm-equivalent, and two comments that claimed more than the code does
- 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. |
||
|
|
f5bd1a0412 |
[Test] (Espryt): drive the acquire / look up / delete / re-acquire walk through the real registry for all six re-keyed kinds
- The D13 "must not break" pins made ZERO slot acquisitions: the scratch-FBO scrub and the
three context-generation guards build their twins with MakeShared directly or under
ScopedStateGuardMocks, so they never reach StateBackendObjectRegistry and pass
identically on both arms. The four acquisitions in the binary were all kind Texture, and
the eleven DirectGLESSlotTable cases drive BackendSlotTable directly on the throwaway
kinds Query and Fence - so they would pass had the six registries never been re-keyed.
Framebuffer, Renderbuffer, SamplerCso, ShaderCso and VertexElementsCso had no case that
could go red for the switch-over.
- EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm walks all six through the real
registry global the shipping paths call: GetOrCreate mints a non-null {slot, gen}, Find
and FindByHandle name the same twin storage, the object's own destructor notice frees the
slot with no sweep, and the successor lands on the freed slot with a moved Gen while the
predecessor's handle resolves to nothing. A kind still on the legacy arm answers the null
handle and fails the assertion by name.
- Evidence: gdb breakpoint on MGPipeSlotAllocator::Acquire counts 12 hits for this case
alone in build-push - six kinds times the two objects each - against 4 for the whole rest
of the binary. It skips visibly on the legacy arm and in the pull build (G2 parity).
|
||
|
|
6cb7d1b83b |
[Fix] (Espryt, State): let the last four object classes announce their own death and delete the twin table's garbage collector
- 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. |
||
|
|
7c97fcfee3 |
[Fix] (Espryt): stop the armless knob pair inside the test that needs an arm, not inside the EGL bring-up a forked pre-flight swallows
- 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).
|
||
|
|
caa0a7221b |
[Fix] (Espryt): pick the unit-bindings debounce by the runtime arm, and stop two slot cases sharing one kind
- 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. |
||
|
|
eb81705130 |
[Fix] (Espryt): tell the backend when a frontend object dies instead of discovering it in a garbage sweep
- 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.
|
||
|
|
e10f5d6750 |
[Test] (Espryt): run the sanity binary on the twin arm it was compiled for, and pin that it does
- SanityTest never calls MG_ConfigLoader::Init, so MG_Config::Features.PipePush kept its static default of 0 - the value a PULL build ships - and every case in the binary took StateBackendObjectRegistry's legacy UnorderedMap arm. In build-linux, build-push AND build-verify alike, the whole D13 "must not break" list (the scratch-FBO scrub, the three context-generation guards on the texture/framebuffer/renderbuffer twins, the sampled-set staleness walk and the whole-registry ScopedDirectGLESTextureBindings fixture) was therefore evidence about code this package did not change; a gdb breakpoint on MGPipeSlotAllocator::Acquire was the only way to see it. - A gtest global Environment now seeds Features.PipePush with ConfigLoader's own push-build default, so the binary runs the arm that SHIPS in the build it was compiled for: legacy in build-linux (where the handle arm is not compiled and every DirectGLESSlotTable case skips visibly), handles in build-push and build-verify. MOBILEGL_PIPE_PUSH in the environment overrides it with ConfigLoader's decimal/0x contract, so the legacy-arm run of the same binary is one env var. It is an Environment and not a static initializer because MG_Config::Features has a String member and is dynamically initialised. - TheTwinRegistryCasesInThisBinaryRunOnTheHandleArm is that gdb probe made falsifiable: delete the environment and it goes red naming the arm rather than a symptom. |
||
|
|
5a3c0616b7 |
[Test] (Espryt): pin the churn-driven sweep cadence, the null tolerance, and the one slot per object the two Track H slices share
- ObjectChurnAloneDrivesTheSweep churns 256 objects through GetOrCreate and never calls
CollectGarbage{IfNeeded,Now}: nothing but the creation tick can collect them. Verified
red - it is the only case that fails - when the creation-driven sweep is neutered, and
green again on restore. The churn count is a fixed constant rather than a multiple of the
interval so that a negative control which pushes the interval out of reach makes the case
FAIL instead of running for 2^32 iterations.
- TwoTablesOfTheSameKindAgreeOnOneObjectsHandle could not fail: HandleOf never reads the
table, so any two tables agree for any implementation. Replaced by a case that asserts
what is actually load-bearing - ONE slot of the kind is consumed however many tables hold
a twin of the object (which is what makes Magma's subsystem-4 table resolve the same
handle for the same VAO), and the shared handle still addresses each table's own twin.
- GetOrCreateToleratesANullStateObject pins the release-build behaviour the map arm had.
- The three names are registered as visible GTEST_SKIPs in the pull build, like the five
before them, so the pull and push builds keep listing the same ctest entries.
|
||
|
|
9a8369296e |
[Fix] (Espryt): sweep the twin table on object churn again, refuse to run an arm the operator disabled, and give the hot lookups back their array probe
- 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.
|
||
|
|
149e26a79a |
[Fix] (Espryt): do not return a reference through a null slot pointer, and stop a comment claiming a memo that is no longer there
- 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. |
||
|
|
3160c4b85b |
[Test] (Espryt): pin the twin table identity contract - a reclaimed slot is a new handle and the stale one resolves to nothing
- Five cases against BackendSlotTable directly, through a stand-in state object that carries
only GetLifetimeId(), so none of them needs a GLContext, a driver or ES entry points.
- The load-bearing one is the ABA: an object dies, the sweep returns its slot, the next
object takes the same slot with a moved generation, and the predecessor handle answers
null instead of the successor twin. That is the property the address key could only paper
over with a weak_ptr.
- Gen moves on reuse and only on reuse; Find never mutates the table (which is what lets
SyncTextureObjectToBackend drop its second Find); a whole table saves, resets with = {} and
restores, which is the shape ScopedDirectGLESTextureBindings needs; and two tables of one
kind agree on a single object handle, because the identity comes from the client allocator
rather than from either table.
|
||
|
|
bd2092f4eb |
[Refactor] (Espryt): key every backend twin on {slot, gen} instead of the frontend object heap address
- 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.
|
||
|
|
f4dbea2300 |
[Feat] (Espryt): give the backend a dense {slot, gen} twin table beside the address-keyed registry
- 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.
|
||
|
|
842af23331 |
[Chore] (Pipe): drop the executable bit from MGPipeRenderStateSpans.cpp
- A mode change on its own, so it does not ride inside a code commit. The other three of the four files created with 0755 were already corrected; this is the last one. |
||
|
|
d1a7c5f159 |
[Fix] (Pipe): stop claiming set_pixel_pack_state supplies a field it only half writes, and fold the chunk boundaries into the subset hash's seed
- Coverage.def's emitted list named GetPixelStoreParameters, but the field is PipeInputs::m_pixelStore[2] - pack AND unpack - and set_pixel_pack_state carries the pack half only, deliberately and permanently. An emitted row is a licence for the residual fill loop to skip the field, so the moment the render-state bitmask has its bit set the unpack half would be written by nothing while its poison stamp said it was published, invisible to the poison and to the verify comparator alike. The row is gone and the reason is in the file; the pack half is simply written twice until the field is split. - kMGPipeRenderStateChunkTableVersion was a promise nobody enforced: a boundary could move, the two byte-count assertions be updated, and every persisted key stay valid. The hash is now seeded with the version XOR a compile-time checksum of the boundary table, so a moved boundary invalidates the keys whether or not anyone remembered - and without a static_assert on the boundaries, which would turn G7's negative control into a build break instead of a red test. |
||
|
|
ce370a3e84 |
[Test] (Pipe): drive both redundancy trip wires and the four apply entry points nothing in any build reached, and finish the setter walk's stencil faces and hint targets
- Five of the applier's eight entry points had no caller in any build, including both wires the redundancy of the patch trio and of the residual block exists to arm. ROADMAP.md asks that every gate be able to go red for the reason it exists; these could not change colour at all. - Each wire is now driven in three states: DISARMED (the applier has not scattered the bytes it would compare, which is the MOBILEGL_PIPE_PUSH=0x10 shape), ARMED AND AGREEING, and ARMED AND DIVERGING. The diverging state is asserted in the form the build gives it - a poison or verify build aborts and the parent reads SIGABRT and the Fatal line out of the log, a shipped push build counts and logs - so neither case is skipped anywhere. - ResidualTripWireHoldsAcrossAVerbClassThatDoesNotPublishTheBlock is the draw-then-dispatch sequence itself: it aborts on the pre-ledger form of the wire and passes on this one. - The patch case carries a NaN outer level, a legal glPatchParameterfv value that must compare equal to itself, which is why the wire memcmps rather than compares. - set_pixel_pack_state, set_vertex_attrib_defaults and delete_render_state are driven and read back through the accessor a backend would use, each under the verb class that publishes it. MGPipeDeriveRenderStateFields - D-7's kept whole-block form, which production does not call - is checked against the chunk-scoped one. - The suite gains its own main() (and links gtest, not gtest_main) for PipeInputsTest's reason: the diverging cases read the wire's line back out of a log file this process names before anything logs. - SetterConsistency drives SetStencilOp and SetStencilFunc and SetStencilMask on BOTH faces and SetHint on all four targets. Face 0's Func ends chunk P2 and its three ops open P3, which face 1's Func closes, so the previous one-face-each walk never wrote P3 at all. |
||
|
|
bee07c3273 |
[Fix] (Pipe): arm both applier trip wires off the applier's own scatter ledger instead of off a bound handle, and stop leaving a mixed CSO or a malformed attribute tail to a DEBUG-only assertion
- MGPipeApplierState gains ScatteredChunkBits: the global chunk bits this applier has itself
scattered into PipeInputs::m_renderState, set by bind_render_state (the whole pipeline half)
and set_dynamic_state (the chunks it names), cleared by MGPipeApplierReset. set_patch_state's
own write is deliberately NOT in it - that is the other carrier, and a wire comparing against
bytes it had just written would be a tautology.
- The residual trip wire now compares capability i only once every chunk that capability's
answer is read out of is in the ledger. Both of the previous form's contracts were undeclared
and one of them was wrong: with the render-state subsystem off (MOBILEGL_PIPE_PUSH=0x10 is a
legal per-subsystem A/B, D14) the working block is the per-verb fill loop's, published per
verb CLASS, and FillPoints.def does not publish GetRenderStateParameters at kDispatch or
kTextureOp - so at a dispatch after a draw the block held the draw's bytes and a correct
context could abort. With the ledger empty the wire now says nothing there, and with the
subsystem on the applier is the block's only writer and its bytes are current at every class.
- The per-capability grain is not decoration: a bind alone owns the pipeline half, and the
eight ClipDistances are answered from ClipDistanceEnabledMask in dynamic chunk D7, so between
a bind and the first set_dynamic_state exactly those eight are unanswerable. The source
chunks come from the same MGP_PLAIN_CAPABILITY_LIST and the same boundary table
DeriveCapability reads, so the two cannot drift.
- The patch-carrier wire arms the same way, which replaces its "some CSO is bound" condition -
a process-global that stayed set from the first bind onward - and covers the verb-class
contract as well as the ordering one.
- Both wires now run in the shipped push build too, counting and logging where a poison or
verify build aborts (one MGP_TRIP_WIRE_REPORT/TAG pair, so only the fatal arm writes the
"Fatal{...}" marker G4 greps for). A wire compiled out of every build a device runs is not a
wire, and the counters are what let a unit case see it fire in every build.
- create_render_state no longer leaves a dead BaseCso to MOBILEGL_ASSERT, which is inert at
INFO - the level every gate and every shipped build uses. A recycled slot's record still
holds the previous occupant's 396 bytes, so inheriting nothing and scattering the delta on
top handed out a record that was half one CSO and half another. Both arms now start from a
defined base and report. The same for a brand-new CSO that does not name every chunk.
- set_vertex_attrib_defaults walks the mask's 32 bits rather than the slot array, consumes a
tail entry for every named location so a named-but-unstorable attribute cannot desynchronise
the rest, and reports all three consistency faults in every build.
|
||
|
|
7c2c1456f8 |
[Test] (Pipe): make the slot allocator's ABA case prove its claim without waiting for the heap, and narrow the DEBUG skip to the arm that needs it
- LifetimeIdSurvivesARecycledAddress could not distinguish what its name claimed: Acquire never sees an address, and each round freed its handle, so the next handle differed whether or not the heap repeated the address - a restatement of GenMovesOnlyOnSlotReuse - and the case could silently GTEST_SKIP on a machine that never repeats one. The reuse count is now recorded rather than depended on, and a deterministic arm proves the strictly stronger form: re-acquiring the SAME LIFETIME ID after a free - the key the map is actually built on, which MG_State never reissues - still cannot reproduce the handle, because the reused slot carries a new generation. If that cannot reproduce a handle, no recycled address can. - CompositeShaderBandIsNeverHandedOut skipped the whole case in a DEBUG build, including the two arms that trip no assert. The eight low-slot handouts and the "every other kind is unaffected" arm now run in every build; only the exhaustion walk, which trips the allocator's own "slot space is exhausted" MOBILEGL_ASSERT on purpose, is behind the skip. |
||
|
|
ad1238bd6f |
[Test] (Pipe): require a named pipeline member to be WHOLLY pipeline, drive every indexed setter at index 0, and walk the incremental chunk path
- ChunkTablePartitionsTheBlock asserted only that a named pipeline member is TOUCHED
by the pipeline half. A boundary that demotes part of one - four bytes at the head
of BlendStates, i.e. the glEnablei(GL_BLEND, 0) bit - left the case green except
for the two byte-count constants, which P3 will legitimately move; after that a
partial demotion would have been invisible, and its consequence is one CSO handle
serving two different pipeline states. Now IsWhollyPipeline for every named member,
with StencilStates the one documented straddler.
- Every indexed setter is driven at index 0 as well as at a middle index. Index 0 is
the element both backends consume and the one a head-of-array boundary demotes
first; with it, the demotion above also fails SetterConsistency, naming the setter.
- The round trip D2 rests on is asserted: the assembled block is memcmp-equal to the
live one. That is the only cover for the ~25 members with no derived field at all -
SampleCoverage*, SampleMaskValue, PolygonModeBack, the hints, ScissorBoxes[1..15],
ClipDistanceEnabledMask and the raw capability bools - which is exactly the set
Espryt's SyncRenderState reads through its span memcmp.
- IncrementalChunksKeepEveryDerivedFieldInStep: the shape the tracker actually emits.
A create_render_state naming only the pipeline chunks that moved against a BaseCso
(D7 step 2's miss path) and a set_dynamic_state naming only the dynamic chunks that
moved (D8's suppressor), ten steps plus all 35 capabilities one at a time, each
followed by the full derived-field comparison. It is the first caller of the
base-inherit branch, of MGPipeScatterPipelineChunks, MGPipePipelineChunkBlobBytes,
MGPipe{Dynamic,Pipeline}ChunksThatMoved and MGPipeHashPipelineBytes, and it is the
oracle for the applier's chunk-scoped derivation - a whole-block apply asks for
every chunk and so cannot tell a correct guard from one that is too narrow.
- The 25 kDraw comparisons move into ExpectDerivedDrawFieldsMatch, shared by the
whole-block walk and the incremental one.
|
||
|
|
a9bb99a46a |
[Fix] (Pipe): scope the derivation to the chunks a scatter moved, and give the patch trio and the residual block trip wires that do not depend on call order
- MGPipeDeriveRenderStateFieldsForChunks: the applier no longer recomputes all 29 fields on every scatter. The four wide walks - the 8-wide blend/colour-mask loop, the 16-wide viewport loop, the 16-wide depth-range loop and the 35-arm capability switch - are guarded by the chunks whose bytes they read, so a per-frame glViewport (the D8 case that sends dynamic chunk D0 alone) pays for one 16-entry copy instead of ~170 stores and 35 switch dispatches. That cost sat on the per-draw path and the gate it threatened is G11's pinned ns/draw. - Every guard is MGPipeRenderStateChunkBitsCovering(offsetof(member), sizeof(member)) over the members the guarded block reads, computed from the boundary table: there is no second, hand-maintained member-to-chunk mapping to go stale when a boundary moves. The scalar copies stay unguarded on purpose - they cannot go stale, which keeps the risk of the scoping confined to four guards. - MGP_PLAIN_CAPABILITY_LIST is written once and used twice, for DeriveCapability's switch arms and for the capability guard's chunk set, so the two cannot drift. - set_patch_state now asserts under poison/verify that the trio agrees with what pipeline chunk P0 delivered, which D6 and D10 both ask for and which was missing: a stale set_patch_state silently clobbered the CSO-delivered levels. Compared bitwise, because a NaN outer level is legal and must equal itself; armed only once a CSO has been bound, which states the ordering contract rather than assuming it. - set_residual_value_state's trip wire compares the carried bits against the WORKING BLOCK instead of PipeInputs::m_capability. Only the derivation writes m_capability, so a residual block emitted before the first bind of a context - what "once per context" means - compared against all-false storage while Dither and Multisample default to true, and aborted under poison. The check is unchanged in strength and now has no ordering contract at all. - PipeApply.h no longer implies the verify comparator is this package's oracle: it arms off MG_Config::Features.PipeVerify, which a unit-test process never sets, so the unit oracle is named for what it is and the comparator is credited to the retrace and integration-verify lanes. |
||
|
|
02b970e9c1 |
[Test] (Pipe): pin the slot allocator's identity contract - gen moves only on reuse and a recycled address never reproduces a handle
- Everything Track H keys off is only as sound as these statements, so each case is the
answer to a bug the {slot, gen} pair exists to close rather than a coverage exercise.
- GenMovesOnlyOnSlotReuse: the first handout of a slot is generation 0; nothing in the
interface can move a live handle's generation, which is "never on a respecify" stated as
an absence; the bump lands on the NEXT handout rather than on the free, so a double free
cannot skip a generation; and the stale handle then fails IsLive and cannot free the slot
its successor owns. Kinds are independent slot spaces.
- FreedSlotComesBackBeforeHighWaterGrows: two freed slots are both handed back before a
ninth is minted. Density is not a nicety - it is what lets the server's object table be
an array indexed by slot rather than a hash map.
- SlotZeroIsNeverHandedOut, over every kind and across a free/allocate churn: {0, 0} is
null for every kind and {0, 1} is the default framebuffer, so neither may be minted.
- LifetimeIdSurvivesARecycledAddress drives 64 construct/destroy rounds of a real
VertexArrayObject through a volatile address sink (ObjectLifetimeIdTest's trick, so the
new/delete pairs are not elided) and asserts that when the heap hands the same address
back, the handle is still a different one. It also asserts Acquire is an identity - the
same live object always answers the same handle - and that a freed lifetime id stops
resolving. When the allocator refuses to repeat an address the case SKIPS with
"inconclusive, not proven" rather than passing for the wrong reason.
- CompositeShaderBandIsNeverHandedOut walks the ShaderCso space to the composite base and
asserts the last ordinary slot is base - 1 and that the next call REFUSES rather than
stepping in. It skips in a DEBUG build, where reaching the edge trips the allocator's
own exhaustion assert on purpose; the INFO builds the gates run are where it is checked.
- Replaces the contract commit's placeholder, whose one live claim survives as
ReservedHandlesAreWhatMGPipeHandlesSaysTheyAre - the only case that is not push-only.
|
||
|
|
eec92cd221 |
[Test] (Pipe): walk every RenderState setter and assert the pipeline-subset hash moves exactly when the pipeline version does
- SetterConsistency is G7. It drives every public RenderState setter with a value that
differs from the one stored, and asserts the pipeline-subset hash moves IF AND ONLY IF
m_pipelineStateVersion moves. Every case also asserts m_version moved, which is the
vacuity guard: a setter handed the value it already holds satisfies "neither moved"
trivially and proves nothing.
- The cases that are not just a list: SetStencilFunc twice (a reference-only change must
move the version and NOT the hash, because Ref/ValueMask are dynamic and Func is
pipeline, and RenderState.cpp's pipeline bump is conditional on Func); SetPolygonMode
with only the back face moving (PolygonModeBack is one of the members P2's subset added
over the 24 ComputePipelineStateHash hashed); the eight ClipDistance capabilities (the
one family that moves m_version alone, so the hash must hold); SetScissorBox across the
ScissorBoxWrittenMask transition; all 25 SET_CAPABILITY names including the three that
had no storage before the contract commit; and SetPixelStoreParam, which must move
neither counter and touch no byte of the block.
- Verified red for the right reason: moving the P1/D2 boundary so ColorMasks falls in the
dynamic half - the partition stays complete, so it still compiles - makes
SetterConsistency fail naming SetColorMask and SetColorMaskIndexed, and nothing else.
- ChunkTablePartitionsTheBlock re-states the header's static_assert at run time and adds
the half the hash cannot check for itself: membership. It walks PipeFields.def's
MGP_FIELDS_RenderStateParameters - the same list gen_pipe.py checks against the struct -
and asserts a member is covered by a pipeline chunk exactly when
kMGPipePipelineStateMembers names it, that every other member is wholly dynamic, and
that StencilStates straddles at exactly the sub-member granularity the table intends.
- DerivationMatchesTheFrontendGetters drives 30-odd setters on a live GLContext AFTER the
filler has run, assembles the working block through the real create/bind/set_dynamic_state
path, and compares the derived fields against the frontend getters. The stale fill is the
point: an ASSERT_NE before each apply proves the block disagrees first, so nothing here
can pass by comparing the filler with itself. It runs in THREE verb phases, because the
fill table is the only thing that says what a verb may read: 25 of these fields are
kDraw's, the three clear values are kClear's and GetClampReadColor is kReadback's alone,
and reading a clear value under DrawArrays is Fatal{UnmigratedPipeInput} - correctly, and
the poison caught exactly that in the verify build before this shape.
- GetViewport's rounding is exercised on (1.5, 2.5, 63.5, 32.25) and the expected literal is
std::lround's answer - half away from zero - not the banker's rounding nearbyint gives.
- DynamicChunksCoverMagmasDynamicTailKey checks every GL-state input of DirectVulkan's
ApplyDynamicDrawStateTail against the dynamic half, and records the one exception the
design implies but no document states: ScissorTestEnabledMask is read by DynamicTailKey's
scissorEnabled yet is PIPELINE state, because SetCapability(ScissorTest) calls
BumpVersions(). Harmless - BumpVersions moves m_version too, so MGPDynamicState::Version
still moves and the tail still re-runs - and asserted the other way round so a later
table edit that demotes the mask is loud here.
- Replaces the contract commit's placeholder case, which existed only so the target had a
test before this package filled it in. The four names are the same four in every build:
in a pull build each is a visible SKIP, never a vanishing test.
|
||
|
|
810850b13a |
[Feat] (Pipe): derive every render-state PipeInputs field from the assembled working block instead of pulling it again from GLContext
- 29 of the 47 kDraw PipeInputs fields are pure functions of RenderStateParameters. Once bind_render_state and set_dynamic_state have assembled the working block - which IS PipeInputs::m_renderState - copying those fields out of GLContext a second time is the per-verb pull P2 exists to remove. The applier now derives them after any scatter. - Each line is a transcription of the RenderState getter of the same name; GLContext's accessors are one-line forwards to those, so the derivation and the pull path answer the same question from the same bytes. Two are not field copies and are transcribed exactly: GetViewport (viewport 0 rounded with std::lround, because glGetIntegerv on float state rounds to nearest and truncating a 63.5-wide viewport would hand the backends a rectangle one pixel short), and IsCapabilityEnabled (the 35-way switch, including Blend -> BlendStates[0].Enabled, ScissorTest -> ScissorTestEnabledMask & 1 and the ClipDistance run). The indexed twin is the same for Blend[8] and ScissorTest[16]. - IsCapabilityEnabled could not have been written before the contract commit: DepthClamp, FramebufferSrgb and TextureCubeMapSeamless fell to `default: return false`, so three of the 35 answers were a compile-time constant rather than state. - This departs from P1 brief D4's "no derivation logic is re-implemented in PipeInputs", deliberately: the alternative is to keep pulling those 29 fields per verb. The guard is the oracle P1 built - MOBILEGL_PIPE_VERIFY's compare-at-read re-reads every one of them from the live context at EVERY backend read and compares field-wise, so a transcription error is caught on the first draw that reads it, across 79 retraces and the integration-verify entries. - The body sits in MGPipeApplyAccess, the struct PipeInputs already names as its friend, so no new friend and no new accessor per field. Pull build untouched: PipeApply.cpp compiles only under MOBILEGL_PIPE_PUSH. |
||
|
|
9c6a8a25d8 |
[Feat] (Pipe): land the P2 contract - real storage for the three swallowed capabilities, the render-state chunk table and its subset hash, the in-process applier, the slot allocator, the subsystem bitmask and the residual ratchet down to 8
- 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.
|
||
|
|
4826806881 |
[CI] (Workflows): run the test and apk lanes on every push to feat/disaggregated
- the MGPipe phases land as a series of pushes and each one needs the full lane; dispatching by hand after every merge is a step that gets forgotten - both entries carry a remove-before-merging-to-dev note: dev's trigger set is what ships |
||
|
|
e7a6a72f6a |
[Docs] (Disaggregated): record the P1 landing and what its verify lane found
- README status, the ROADMAP P1 row with the measured site and accessor counts, and the ARCHITECTURE correction from 293 to the 277 arrow sites the tree actually has - MEASUREMENTS gains the P1 scale table, the two classes of finding the verify lane produced (nine missing fill rows, three fields a backend moves inside its own verb) with the push-on-mutation decision and its three hooks, and the acceptance numbers |
||
|
|
62a7786184 |
[Fix] (Pipe, Purity): end a declared verb honestly, and gate the header MG_State now includes
- MGPipeLeaveVerb() bumps the serial and puts the current verb back to none, so a test that drives a backend helper directly stops declaring where it says it stops and a later unguarded read aborts as "<Field>@<none>" instead of naming an unrelated verb - check_include_closure.py gains a fourth probe: F2 put MGP_NOTE_MUTATION into frontend mutators, so MG_State includes MG_Pipe/PipeMutation.h and that header must never reach back into MG_State, MG_Impl or a backend |
||
|
|
ef6227e19b |
[Test] (Pipe): let a test that drives a backend helper directly declare the verb it stands in
- ScopedPipeVerb (MG_Test/ScopedPipeVerb.h): an RAII "as if we were inside verb X" object
that runs the real MGPipeFillForVerb for the verb it names, so the eleven unit entries
that construct a GLContext by hand and call a backend helper with no GL entry point in
between stop reading an empty, unstamped PipeInputs block
- it weakens nothing: it fills exactly that verb class's may-read mask, so a read outside
it is still Fatal{UnmigratedPipeInput} naming the field and the declared verb; leaving
the scope re-arms the poison with a one-field kQuery fill, so one case's declaration
cannot cover a later one when the binary runs as a single process
- placed the way MGP_FILL is placed in production: immediately before the backend call,
after every frontend mutation it is meant to see; a second call after the test moved
state is a second verb (Renew()), a helper of another class gets a nested scope
- no-op in the pull build, no test renamed, no test added, no production source touched
|
||
|
|
9bd6d39403 |
[Test] (Pipe): pin the push-on-mutation shape - a frontend write inside a verb refreshes the pushed field, and only its value
- AFrontendMutationInsideAVerbRefreshesThePushedField: fills DrawArrays, then
does what UniformManager's fallback path does mid-draw (a SamplerObject filter
change) and what a bind reached from inside a verb does
(NoteTextureUnitTouched), and asserts the block still equals the live context
for GetSamplingResolutionGeneration, GetTextureBindGeneration and
GetMaxTouchedTextureUnit.
- AFrontendMutationInsideAVerbDoesNotDivergeAtRead: the lane failure end to end,
under the armed comparator in a forked child - the read after the mutation
must complete and the log must carry no Fatal{.
- TheMutationNoticeRefreshesTheValueButNotTheStamp: the notice must not restamp
a field whose stamp the fill withheld (negative control B), and must not stamp
a field the verb class never fills (the generation under a kQuery verb).
- Falsified: with the notice's body short-circuited, the first two fail (the
read-side one by SIGABRT on Fatal{PipeVerifyDiffer,
"GetSamplingResolutionGeneration@DrawArrays", where=read}) and the third
stays green, which is what a guard case should do.
- The three names also exist as visible GTEST_SKIPs in the pull build, as every
other case in this file does.
|
||
|
|
6b681c4a63 |
[Fix] (Pipe, State): refresh a pushed PipeInputs field when the frontend moves it inside a verb
- The verify lane aborted eight integration entries and two retrace cases with
Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays",
where=read}, always one line after "ResolveSamplerDescriptor: using fallback
texture for unbound sampler". The backends write into frontend objects during
their own verb - Magma synthesises a fallback texture for an unbound sampler
and gives it a shape, materialises a queued clear, overrides a unit's sampler
filter - and every one of those writes moves a counter MGP_FILL already
copied, so the pushed block stops equalling the live context for the rest of
the verb. That is a real divergence, not a harness artefact: the pull build
reads the moved value and the push build reads the boundary one.
- Takes the findings' preferred option, push on mutation, over the volatile-in-
verb class: it keeps the comparator's invariant ("the pushed block equals the
live context at every read") literally true, keeps push semantics equal to
pull, and is the shape P2's tracker needs. The fallback would have had to skip
compare-at-read for the field, which is the one comparator arm that is real in
P1 - it would have blinded the gate on the very field that found the bug.
- MG_Pipe/PipeMutation.h declares MGP_NOTE_MUTATION(Field), a no-op that
includes nothing in the pull build; MG_Impl/Pipe/PipeFill.cpp defines the
notice next to the filler it shares CopyField with. The notice refreshes one
field's value when a context is live, a verb has been filled, and the field is
in that verb class's may-read mask; it never touches the poison stamp, so a
stamp MOBILEGL_PIPE_POISON_OMIT withheld stays withheld and a field the verb
never filled stays Fatal{UnmigratedPipeInput} rather than being healed.
- The enumeration behind the three hook sites: of the ~40 backend->frontend
write sites, only the texture family reaches a pushed value. Every path
through them funnels into TextureState::BumpSamplingResolutionGeneration
(SamplerObject::BumpVersion for the sampler setters,
TextureObjectBase::BumpShapeVersion for AllocateStorage / SetInternalFormat /
TruncateMipmapLevels / SetSamples / SetFixedSampleLocations),
BumpTextureBindGeneration (a default texture becoming defined, delete-unbind,
a unit's sampler object changing) or NoteUnitTouched (which also moves the
touched-unit high-water mark), so the notice sits on the counters rather than
on each writer and covers the whole family including writers added later.
The buffer, program and VAO writes reach no pushed field: their objects are
read back through O-class live references, not copied values.
|
||
|
|
80a6b39003 |
[Fix] (Pipe): fill the capability set on a texture op and a dispatch, and the shader blit's viewport, provoking vertex and buffer bindings
- The verify retrace aborted four cases with Fatal{UnmigratedPipeInput,
"IsCapabilityEnabled@GenerateMipmap"} (x3) and "@DispatchCompute" (x1):
Magma materialises a texture's queued clear inside both verbs
(GenerateMipmap -> MaterializePendingClearForTexture,
DispatchCompute -> PrepareStorageImageTextures -> the same), and the clear
pre-compensates its colour against GL_FRAMEBUFFER_SRGB in
VkClearManager::PreCompensateSrgbClearColor. Neither class named the field.
- Audited every class the same way rather than stopping at those two rows. Two
more helper-program draws sit inside verbs whose class did not name what they
read: GenerateMipmap takes GenerateDepthMipmapWithShader for a depth texture
and BlitFramebuffer takes TryBlitToDefaultFramebufferWithShader for the
default framebuffer. Both bind their helper's descriptors through
BindProgramUniformBuffers, whose sampler resolver reads the draw framebuffer
for its feedback-loop check and whose buffer-block resolvers read the frontend
binding points; the blit additionally sets the dynamic viewport through
ApplyGLViewportState -> ComputeGLViewport and picks its pipeline's provoking
vertex through GetOrCreateBlitPipeline -> SelectProvokingVertexMode.
- kTextureOp gains IsCapabilityEnabled, GetFramebufferBindingSlot and
GetBufferBindingPoint; kDispatch gains IsCapabilityEnabled; kBlitOrCopy gains
GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode and
GetBufferBindingPoint. Every row carries the path it was derived from.
- Also unfolds the kReadback transform-feedback rows
|
||
|
|
97b997d5da |
[CI] (Pipe): read the verify library's static symtab, grep only the arming lane's log, and stop the two retrace lanes overwriting each other's evidence
- both `nm -D --defined-only ... | grep -q MGPipe...` gates could never pass: the library is built CXX_VISIBILITY_PRESET hidden in every non-Debug configuration and the MGPipe entry points carry no export attribute, so the dynamic table holds none of them (0 of 11930 exported symbols on this tree, while `nm --defined-only` finds MGPipeFillForVerb as a local `t`). build-linux-verify, and with it integration-verify and every retrace-verify entry, would have been red forever for a reason unrelated to the comparator. Both now read the static table, name what a miss means, and refuse a stripped artifact instead of reporting its silence as a missing symbol - "Every verify process really armed" grepped one shared per-lane log that holds only the LAST process of 406 - and the last ambient entry is the poison control's parent, which forks, execve()s and waits without ever issuing a verb, so the step would have red a healthy lane while proving nothing about the other 405. It now greps the two VerifyArming. lanes' own logs, requires one per backend, and says what that establishes - remove-artifact-clutter kept fixtures for failed `retrace (` jobs only, so a failed `retrace verify (` case lost the fixture needed to reproduce it; both prefixes now count - the retrace negative control replayed OpenRA into the same case directory, so "Upload actual image" shipped the deliberately corrupted run's output under the good run's name. The verified output is put aside and restored before the verdict - build-linux-verify regains build-linux's "Show installed toolchain" step, and its deliberate divergence (Release even under ACTIONS_STEP_DEBUG - a Debug build would flip visibility and arm the poison through a different #if arm) is written down - the lane's scope is stated where it is run: every integration ENTRY under the comparator, not every configuration - `integration`'s second MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 pass (186 entries here) is not affordable at the 5-10x the comparator costs, so that tier stays covered unverified by `integration` |
||
|
|
7d80c9678e |
[Test] (Retrace): scan a verify retrace's log for the third MGPipe Fatal too
- the block looked for Fatal{PipeVerifyDiffer and Fatal{UnmigratedPipeInput only. A
misspelt MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT reports
Fatal{PipeVerifyBadKnob, and it was caught only because D2 makes that one abort the
process - which is precisely what MOBILEGL_PIPE_VERIFY_FATAL=0, the supported triage
configuration, takes away. A typo'd knob would have left a negative-control run
looking healthy
- the FATAL_ERROR text now says what each of the three means and where the two
vocabularies live, because that message is the whole diagnosis a `cmake -P` step gets
|
||
|
|
72aa9191b4 |
[Fix] (Tooling): gate symbol_report on all four buckets and on a .text that moved in either direction
- G1 is spelled "added == removed == resized == renamed == 0, .text delta 0", but gate_failures() took only added and removed and fired on text_delta > N: a pull build whose .text SHRANK, or whose functions were resized with zero net delta - the exact shape a null-guard or ternary rewrite produces - walked through `--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` green - --fail-on-symbol-set-change now covers the four buckets the report prints, and --fail-on-added-bytes 0 means byte-identical rather than "did not grow" (a positive budget keeps the one-sided meaning). --fail-on-text-delta is the explicit spelling for a run that wants no byte budget at all - the gates read the threshold-0 buckets whatever --threshold says: --threshold is a report control, and a gate that read the thresholded resize list would have quietly weakened itself the day someone raised it. The run says so in its output - the self-test drives the three shapes that used to pass (a shrunk .text at budget 0, a resize and a rename at zero net delta) from the same canned transcripts |
||
|
|
1e3a74686f |
[Test] (Pipe): give the arming assertion a lane and a log of its own, and stop the poison child calling a sequence it never ran a success
- the arming case read the ambient lane's MOBILEGL_LOG_FILE_PATH, and that log is opened fopen(path, "w") by every process in the lane: with 406 entries per backend and CI running them -j 4, a whole-file read races a neighbour's bring-up, and the file that survives the lane holds only the LAST writer. Every other log-reading scenario in this suite (UnlocatedIoBlocks, the primgen reroute, the point-size demotion) is registered in a filtered lane with its own log for exactly that reason; PipeVerifyArmingScenario.Armed now follows them, in DirectGLES.VerifyArming. / DirectVulkan.VerifyArming., and skips anywhere MGITEST_PIPE_ARMING_LANE is unset - what that can prove is written down where it is asserted: arming is a property of (this library, this environment) and these two processes share both with their ~400 ambient siblings. A per-process census is not available through a shared log, and a comment that claimed one was the reason CI grepped a file that could not answer - the re-exec'd poison child ran RunSequence() and then _exit(0) unconditionally, so a fatal assertion inside it - the shader failing to compile, say - returned before the draw and the glGenerateMipmap and still reported success: WithoutOmissionCompletes, the one green entry negative control B turns red, passed on a child that ran none of the sequence. It now exits HasFailure() ? 1 : 0, and checks glGetError() after the mipmap so a rejected sequence is part of the answer rather than stderr nobody reads |
||
|
|
0fe7bf82d2 |
[CI] (Pipe): the third CI mode - a verify build, its integration and retrace lanes, and the two negative controls as always-on steps
- build-linux-verify is a second Release/INFO build with -DMOBILEGL_PIPE_VERIFY=ON, because the comparator is a compile-time option and does not exist in the shipped library. It refuses to ship an artifact whose libMobileGL.so does not export MGPipeVerifyInputs and MGPipeFillForVerb: a typo'd -D is not an error in CMake, and every lane below would then be green having compared nothing. - integration-verify runs the suite with the comparator armed and then proves it armed twice over: --no-tests=error reds a build whose verify entries were never registered, and a step greps every pipe-verify-*.log for the arming line. - The two negative controls are steps of that job, not a manual exercise: a gate that can only be shown to work by someone remembering to break it has already stopped working. Each passes when ctest FAILS, and each first counts its own selection - an empty selection also exits non-zero under --no-tests=error, and a control that passed because it ran nothing would be worse than no control. - Control B targets PoisonOmissionScenario.WithoutOmissionCompletes, the only integration entry in the tree that calls glGenerateMipmap at all; the case no longer skips itself when the omission knob is set, precisely so that the control has a green entry to turn red. - retrace-verify replays the eight "verify": true cases against the verify library, copied over build-linux/libMobileGL.so because build-retrace freezes that absolute path into every case, with an nm check that the swap happened and an inverted OpenRA step that must go red under MOBILEGL_PIPE_VERIFY_CORRUPT. remove-artifact-clutter now waits for it: it deletes the trace fixtures these jobs download. - monolith-symbol-report is G1 as a job: two pull builds with identical flags and LTO off, the baseline named by a workflow_dispatch input, symbol_report.py with both hard gates, and a refusal of any MG_Remote symbol in the monolith. It is dispatch-only because its answer is about a baseline, not about this push. - pipe-gates gains the two --self-test steps. Regenerating and diffing cannot see a structural check that silently stopped checking; a broken gate and a clean tree produce the same green. Its dirty-surface comment now says P2, which is where ROADMAP.md:18 puts the first mapping round. |
||
|
|
416cd23c28 |
[Feat] (Tooling): give symbol_report.py the two hard gates G1 needs, with the report written first
- --fail-on-added-bytes was a reserved no-op that printed "this run stays
informational"; it now exits non-zero when .text grew past the budget, and
--fail-on-symbol-set-change joins it for the added/removed buckets. Together they
are the spelling of P1's G1 ("the pull build is byte-identical"): --threshold 0
--fail-on-symbol-set-change --fail-on-added-bytes 0.
- Default behaviour is unchanged: with no gate flag the tool prints its report and
exits 0, which is what every existing caller and the informational
monolith-symbol-report job expect.
- A gate fires AFTER the Markdown and JSON are written, never before: the report is
the diagnosis, and a CI job that failed before uploading its artifact is one
nobody can act on.
- The decision lives in a pure gate_failures(), so --self-test drives it from the
same two canned transcripts as the buckets: each flag fires on the canned
add/remove/+100 delta, each stays quiet when it was not asked for, and a
tolerated budget is tolerated. A gate whose only test is a real build is a gate
nobody re-tests.
|
||
|
|
5f8e8db1b9 |
[Test] (Retrace): make a retrace under MOBILEGL_PIPE_VERIFY prove it armed, and give the lane a label
- A retrace that exports MOBILEGL_PIPE_VERIFY=1 at a library configured without
-DMOBILEGL_PIPE_VERIFY=ON is a no-op: the frames still match their goldens and the
case reports green having compared nothing. run_trace_case.cmake now demands the
evidence whenever the variable is set to anything but 0/false - mobilegl.log must
exist, must carry "MGPipe: verify armed", and must carry neither
Fatal{PipeVerifyDiffer nor Fatal{UnmigratedPipeInput. With the variable unset the
script is byte-for-byte the old one.
- The Fatal scan is not redundant with the replay's exit status:
MOBILEGL_PIPE_VERIFY_FATAL=0 is the supported triage configuration, and there a
divergence is logged and counted rather than aborted, so the run would finish 0
with its own report sitting unread in the log.
- LABELS retrace on both registrations: the lane was selectable only by regex, so
`ctest -L retrace --no-tests=error` - the spelling that reds a lane which
registered nothing - could not be written at all.
- "verify": true on eight cases (the five 180s cases plus minecraft-1.21.4-in-world,
minecraft-1.21.4-fabric-sodium-in-world and improved-transparency-minecraft-26.3),
and --format github-verify-matrix over that subset: 16 entries, against the full
lane's 77. The verify build compares at every verb boundary and again at every
accessor read, which the design budgets at 5-10x, so the per-push job runs the
subset and the full sweep is a phase-exit / workflow_dispatch run.
- The flag is validated in the manifest loader, not at the matrix, so a non-boolean
or a verify case excluded from CI is a loud error in every consumer instead of a
subset that is quietly one case short.
|
||
|
|
bdf05514c3 |
[Test] (Pipe): the integration-verify lanes and their two always-on negative controls
- ARCHITECTURE.md 13.2-(2) asks for a third CI mode, and a third mode whose only evidence is "ctest was green" proves nothing: MOBILEGL_PIPE_VERIFY=1 against a library that never compiled the comparator in is a silent no-op that looks exactly like a clean pass. Six registrations, all under if (MOBILEGL_PIPE_VERIFY) and all labelled integration-verify, make both halves falsifiable - a mis-configured build registers nothing and --no-tests=error reds the lane, and PipeVerifyArmingScenario.Armed fails a lane whose library never printed its arming line. - PipeVerifyArmingScenario.CorruptedFieldIsReported is negative control A (G4): its lane pins MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters with MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and can read the report back; the CI step that exports the same knob against the ambient lane, where FATAL keeps its default, asserts the other half - the abort. - PoisonOmissionScenario is negative control B (G5), in two cases that cannot share a process because the knob is process-wide: the omitted (verb, field) pair must abort the glGenerateMipmap and NOT the draw before it, and the same sequence with the knob unset must complete with no Fatal at all. - The sequence runs in a fork()+execve() of this same binary rather than a bare fork(): the fixture has already brought a context up, and a bare fork of a process holding a live Vulkan device inherits the driver's mutexes with no threads to release them - measured here as a 120s wedge on DirectVulkan against a clean pass on DirectGLES. The child gets its own MOBILEGL_LOG_FILE_PATH because the library opens its log with fopen(path, "w") and would otherwise truncate the file the parent is about to read. - No ambient Verify. entry names MOBILEGL_PIPE_VERIFY_CORRUPT or MOBILEGL_PIPE_POISON_OMIT in its ENVIRONMENT property, because a property entry overrides the job environment for the names it lists: the two CI negative-control steps export those knobs into the job environment and must reach the processes. Every list appends MGL_ITEST_COMMON_ENV / MGL_ITEST_VULKAN_ENV for the same reason, so the vendor and ICD pinning survives. |
||
|
|
bf8b39a867 |
[Refactor] (Magma): route every frontend read through MGB_CTX - 164 arrow sites sed'd, 49 non-arrow lines converted (43 asserts keep their meaning as MGB_CTX_LIVE); pull build byte-identical
- 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. |
||
|
|
d4504e30f8 |
[Refactor] (Espryt): route every frontend read through MGB_CTX - 113 arrow sites sed'd, 9 non-arrow lines converted (the fb-slot cache keys on MGB_CTX_IDENTITY); pull build byte-identical
- 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
|