# CI cleanup log Every removal or replacement of a CI test, gate or lane is logged here with a one-line reason; decisions to keep a red case (and fix the code or the tooling instead) are logged too, so the next reader can see why a check still exists. Started in P13 (2026-10-06). The rule: a check is removed only when it is superseded or no longer relevant (tied to the pull build, or a finished migration-phase audit), never to hide a real bug. Goldens change only when the GL spec proves them wrong. ## Removals | Date | Check | Workflow / job | Reason | Successor | |---|---|---|---|---| | 2026-10-06 | unit `BufferTest.{ANonPersistentWriteMapOfAShadowBackedStoreStillFlushesThroughTheBackend, RespecifyingAShadowBackedStoreWhileItIsMappedPushesNoRangeDown}` | Test / unit | P13 W6: they pinned the monolith frontend arm (`ScopedMonolithFrontendArm`) to assert how a shadow-backed map reaches the backend's own flush op - the arm is deleted; the record arm pushes the mapped span itself | `SplitBufferTest` (persistent-map push, live-host-writes), `PersistentCoherentMapScenario` and the buffer integration lanes on monolith's record arm | | 2026-10-06 | unit `TextureTest.{ShadowReadbackRefusesALayoutItCannotProduceInsteadOfOverrunningTheBuffer, ShadowReadbackRefusesAPackStateItCannotHonor}` | Test / unit | P13 W6: they pinned the frontend arm's CPU-shadow readback, whose pack-state limits they asserted; that path is deleted - the record arm reads the backend's image under a neutral pack and scatters every pack state itself | `PixelStoreSweepScenario`, `PackBufferReadbackScenario` (every pack state, both backends, every arm) | | 2026-10-06 | unit `TextureTest.GetTexImageOfOneCubeFacePacksIntoAOneFacePixelPackBuffer`, `TextureViewTest.CubeMapViewOfAnArrayReadsTheFaceEachTokenNames` | Test / unit | P13 W6: GL rules, but asserted through the deleted frontend arm's shadow readback; a unit process has no backend image to read | moved, not dropped: `CubeFaceReadbackScenario` (integration-gpu on both backends plus the split arms) asserts the same two rules on a real context | | 2026-10-06 | `DirectVulkan.HandleRecycle.AbaControlHandles.*` | Test / integration-gpu (`integration-monolith-control`) | P13 W6: it pinned `MOBILEGL_PIPE_DATA_ARM=frontend` - its knob defeats the identity half of Magma's frontend-arm keys, which the deleted arm owned; the record arm keys every object on the applier's {slot, gen} | `DirectVulkan.HandleRecycle.Handles.*` (the same cases on the record arm) | | 2026-10-06 | `build explicit pull runtime control` (the pull `libMobileGL.so`, `-DMOBILEGL_PIPE_PUSH=OFF`) | Test / `build-linux-monolith-control` | P13 W2 retires the pull build: CMake defaults to push and nothing ships pull (ID-P13-1) | the same job builds the push monolith without MG_Remote (the FCL shape); its `runtime_mode_proof --mode monolith` is the zero-MG_Remote-symbol check | | 2026-10-06 | `Negative control (pull library)` | Test / `retrace-split` (every leg) | its library was the pull build above | `Negative control (library without a transport)`: same script (renamed `retrace_transport_control.sh`) and evidence sentence, with the push library without MG_Remote; W1 ran both side by side on every leg | | 2026-10-06 | `DirectGLES.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.AbaControl.*` | Test / integration-gpu (`integration-monolith-control`) | they ran `MOBILEGL_PIPE_PUSH=0`, the pre-handle arm, which P13 W3b removed with `MOBILEGL_PIPE_LEGACY_MEMOS` (ID-P13-3) | `HandleRecycle.Handles.*` and `DirectVulkan.HandleRecycle.AbaControlHandles.*` (the ABA control against the handle arm that ships) stay | | 2026-10-06 | `DirectGLES.ResourceSubsystemControl.Off.*`, `DirectGLES.ResourceSubsystemOff.*`, `DirectGLES.ObjectSubsystemControl.{Off,Refused,RefusedTexture}.*` | Test / integration-gpu | they cleared subsystem bits (masks 0x7f / 0x1ff / 0x9ff / 0x5ff) to run legacy arms; since W3b bits 0-13 are fixed on (ID-P13-3) | one named-refusal control per lane, `DirectGLES.SubsystemMaskRefusal.`: the same scenario and mask must go red with `Fatal{PipeSubsystemsFixedOn}` naming that mask (`Harness/RefusedMaskControl.cmake`); the `On` lanes now run 0x3fff | | 2026-10-06 | `SanityTest`: `DirectGLESSlotTable.{AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane, TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem, EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping}` | Test / unit | they pinned the `MOBILEGL_PIPE_LEGACY_MEMOS=0` + cleared-bit knob pair; the knob is gone and the mask is refused at startup | the SubsystemMaskRefusal controls above | | 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView` | Test / unit | **not a behavior gap.** At 0x3fff the parameter push is record-addressed (`SyncTextureParamsToBackend` reads the texture's applier record; nothing in it consults a sampler view), but the unit lane cannot create that record (`NoP4aConsumer`, as above), so the probe's handle half never had a record to read; its legacy half pinned the pre-handle push that W3b removed | `TextureParamsWithoutASamplerViewScenario` (G9): its four cases take a WHITE-BOX reading of the driver-side parameters before any sample (`TakeTheWhiteBoxReadingBeforeAnySample`, ID-19), at the shipping mask on every lane - the same property, observed where records exist | | 2026-10-06 | unit `RenderStateSpans.{ResidualTripWireIsSilentUntilTheApplierOwnsTheBytes, ResidualTripWireHoldsAcrossAVerbClassThatDoesNotPublishTheBlock, ResidualTripWireFiresNamingTheCapability}` and PeerLatchTest's `SetResidualValueStateBlobSize` row | Test / unit | P13 W3c deleted ResidualValueBlock and its applier trip wire (op 46 is a retired, declined row) | integration-verify's new `Negative control A2 (corrupted capability)`: the verify comparator must go red on a corrupted `IsCapabilityEnabled`; `PipeWireCodecTest.ResidualValueBlockCrossesAsItsOwnBlob` now pins that op 46 is declined; `TrackerTest...AClipDistanceEnableReArmsTheResidualBlock` now asserts the capability reaches the applier's block | | 2026-10-06 | unit `DirectGLESBufferDrawProbe.ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws` | Test / unit | P13 W6: it pinned the frontend arm's draw-time `SyncPersistentMappedRange` keeping a live host map's probe dirty; that per-draw hook is deleted - the record arm publishes a live map's span at push time (`PersistentMapTracker`) | `PersistentCoherentMapScenario` and `SplitBufferTest`'s persistent-map push / live-host-writes cases, on every arm | | 2026-10-06 | unit `DirectGLESSanity.{BindsAMultisampleTextureDespiteTheDefaultMipmapFilter, BindingZeroClearsPreviousNativeTextureBinding}` | Test / unit | P13 W6: GL 4.6 8.17 / 8.17.1 rules, but asserted through the deleted frontend arm's unit walk (`BindCurrentTextures` over frontend binding slots); every library walks the units by handle now | moved, not dropped: `TextureUnitSamplingScenario` (integration-gpu on both backends plus the split arms) asserts both rules on a real context | | 2026-10-06 | unit `DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture` | Test / unit | P13 W6: the borrowed-slot work list and its `PairingsIntact` check it pinned are deleted with the frontend arm (the 'Kept after investigation' entry below said it goes when monolith moves to the by-handle unit walk, which W6 did) | `DsaUpdateKeepsTheBoundUnitScenario` (GL 4.6 8.1/8.5 semantics, every lane) | | 2026-10-06 | unit `GeneralBufferTest.{General_PersistentCoherentZeroCopyStressNoPerDrawReupload, General_PersistentCoherentFallbackSyncsPerDrawWhenBackendDeclines}` | Test / unit | P13 W6: both drove the frontend arm's draw-time `SyncPersistentMappedRange` against a mock backend (zero-copy acquire vs the declined CPU-shadow fallback); the hook and the frontend's backend-acquired map are deleted - the record arm owns the map and pushes the span itself | `PersistentCoherentMapScenario`, `SplitBufferTest` persistent-map cases; per-draw cost is tracked by the device fps/CPU numbers (DEBTS ID-P13-2) | | 2026-10-06 | unit `SubsystemDepsTest.{TheServerConsumerGateNeverArmsAFamilyTheTableRefuses, TheTwoDocumentedRefusalLanesStillRefuseAndTheFullMaskStillArms}` | Test / unit | P13 W6: they swept the per-family resolvers over refused masks, which worked because a refused family returned false and ran the legacy arm; with one data arm a refused family stops (`StopOnTransportWithoutRecordArm`) on every library, and ConfigLoader already refuses those masks at startup | `DirectGLES.SubsystemMaskRefusal.` named-refusal controls; `SubsystemDepsTest`'s table cases stay | | 2026-10-06 | unit `SplitBufferSet.TheWholeSetIsInertOnTheMonolithPath` | Test / unit | P13 W6: it pinned that monolith ran none of the split buffer set - monolith runs the record arm now, so the set is live there by design | the rest of `SplitBufferSet` (now exercised on monolith's record arm too) | | 2026-10-06 | unit `ContextEpochTest.RawDepthSamplerTakesADifferentObjectOnEachArmAndTheServerNeverTakesTheLegacyOne` | Test / unit | P13 W6: it asserted the two raw-depth sampler arms were distinct objects; the monolith (legacy) arm is deleted, as its own comment filed for P13 | `ContextEpochTest.RawDepthSamplerOwnsANativeIdForEachContextGeneration` (the one arm) | | 2026-10-06 | unit `MagmaPipeIdentityTest.{AnIdleSlotIsRetiredAndReusedWithANewGeneration, AReusedSlotDoesNotServeItsPredecessorsMemo, TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo}` | Test / unit | P13 W6c: they drove `MagmaPipeIdentityTables`, Magma's {slot, gen} mint for the frontend arm's VAO draw memos, and its ABA-control claim rule; the mint, the memos and the `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` knob (no consumer left) went with the frontend arm - the record arm keys every object on the applier's own handles | `HandleRecycleScenario`'s `Handles` arm (both backends) and the applier's slot/generation tests (`SlotAllocatorTest`) | | 2026-10-06 | unit `DirectVulkanSanity.MutableStorageImageViewsUseVulkanCompatibilityClasses` | Test / unit | P13 W6c: it pinned `VkTextureManager::AreStorageImageViewFormatsCompatible`, which only the frontend arm's storage-image path called; that path and the helper are deleted | the wire storage-image path's format checks (`InvalidImageUnitScenario`, `ImageFormatQualifierScenario`) | | 2026-10-06 | unit `VkClearManagerTest.{CollectGarbageRemovesExpiredTexturesAndTheirPendingClears, StalePendingClearsAreRejectedBeforePeriodicGarbageCollection}` | Test / unit | P13 W6e: they drove Magma's frontend pending-clear queue, keyed on frontend texture objects and only fed by the frontend `ClearNamedFramebuffer*` / render-pass entries, which the verb port replaces in every mode; the queue and the entries are deleted | the record arm's clears (`ClearThenReadPixelsScenario`, `MonolithAttachmentClearScenario`, `LayeredAttachmentShapeScenario`, both backends, every arm) | | 2026-10-06 | unit `FramebufferTest.WidenedDrawBufferIsIdentifiedPerDrawBufferSlotNotPerAttachmentPoint` | Test / unit | P13 W6e: it pinned Espryt's `ComputeAlphaWidenedDrawBufferMask` over a frontend `FramebufferObject`; the record arm computes the same mask from the framebuffer record (`IsAlphaWidenedColorSurface`) and the frontend form had no caller | `ThreeChannelAttachmentScenario` (widened RGB attachments end to end, both backends) | | 2026-10-06 | unit `DirectGLESSlotTable.{ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire, GetOrCreateToleratesANullStateObject}` | Test / unit | P13 (`# P13` ratchet gate): they pinned the slot table's frontend-keyed half - `HandleOf`'s lifetime-id memo and the minting `GetOrCreate(StatePtr)`'s null-object parking - which minted handles off frontend lifetime ids from inside MG_Backend; that half is deleted (the table only receives client-minted handles now). The other ten `DirectGLESSlotTable` mechanics cases were RETARGETED at the handle entries (`GetOrCreate(handle)` / `FindByHandle` / `ReleaseTwinByHandle`), not removed | the retargeted `DirectGLESSlotTable` cases, `SlotAllocatorTest`, and the link ratchet's P13 exit gate (no server object references the client allocator) | | 2026-10-06 | unit `RemoteGuards.{SlotTableHandleOfFromTheApplyThreadIsFatalByName, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFatalEvenAtTheOldDebtSites, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes, LegacyRegistryWrapperCannotExposeFrontendKeysOnApply, AnExemptionScopeHeldOnTheGLThreadDoesNotExemptTheApplyThread}` | Test / unit (split) | P13 (`# P13` ratchet gate): they drove the apply-thread refusals hung on Espryt's frontend-keyed registry entries (`HandleOf`, `GetOrCreate(StatePtr)`, `NoteStateForHandle`, `StateForHandle`, `Find(object)`, `begin()`) and the two no-op marker scopes; the entries, the registry refusal and both scopes are deleted, so there is no apply-thread surface left to refuse | `RemoteGuards.AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName` / `BarrieredLegacyScopesCannotExemptAllocator` (the allocator's own guard, kept) and the link ratchet's P13 exit gate | | 2026-10-06 | unit `ServerLoopEglTest.FrontendFramebufferDeathDeletesOnTheContextOwner` | Test / unit (split) | P13 (`# P13` ratchet gate): its pin was the refusal of the apply-thread `GetOrCreate(StatePtr)` mint of a framebuffer twin; that overload is deleted | `CtWireScenario`'s object_death cases (framebuffer death by handle on the server) | | 2026-10-17 | `Negative control (library without a transport)` (with its `Download/Unpack control runtime` steps) | Test / `retrace-split` (every leg) | a full replay rerun on all 154 legs (~60 runner-min) that, since the per-role logs, only ever proved "no client log was written"; a broken control build once reddened every leg at the download and hid every real split replay | in-test: `run_trace_case.cmake`'s transport assertions now leave `output/split-evidence.txt`, which `retrace_split_evidence.py` checks on every leg; `test_run_trace_case.py` (pipe-gates) proves each assertion red on canned logs, including the no-client-log and env-dump-line shapes this control used to rely on | | 2026-10-17 | `Negative control (dropped draws)` | Test / `retrace-split` (every leg) | a rerun of every leg with `MOBILEGL_IPC_E2_DROP_DRAW`, a production hook; its knob's own early return produced the DirectVulkan `Fatal{RoleViolation, "gPipeInputs"}` reds of 10-02 to 10-05 | in-test on the OpenRA legs (both backends, inproc and spawn): `MGTRACE_WIRE_EVIDENCE` turns on the library's counters and `run_trace_case.cmake` requires DrawVbo records on the wire and draws on the server role (758 each locally); canned reds in `test_run_trace_case.py` | | 2026-10-17 | `scripts/ci/retrace_transport_control.sh`, `scripts/ci/retrace_drop_draw_control.sh`, the retrace and drop-draw halves of `control_smoke_test.sh` and `stub_ctest.sh`, and `scripts/ci/redcheck_control_smoke_test.sh` | Test / pipe-gates (R-16) | the bodies of the two controls above and their smoke cases; the redcheck script had no caller and mutated the deleted scripts | `test_run_trace_case.py` | | 2026-10-17 | unit `RemoteEmitTable.TheE2DropSwitchStartsDisarmed`, and the `MOBILEGL_IPC_E2_DROP_DRAW` / `MOBILEGL_IPC_E2_DROP_CLEAR` knobs with their per-draw and per-clear branches in the shipped emitter (`VerbPort.cpp`) | Test / unit (split); product code | the knobs existed only for the retired dropped-draws control (the clear knob was set by nothing); the unit case only toggled the clear setter | the wire-volume assertion in `run_trace_case.cmake` on the OpenRA split legs (DrawVbo records on the wire, draws on the server role) | | 2026-10-17 | the 802-entry `DirectGLES.Split.` baseline rerun inside `split_negative_controls.sh` (skip-census lane `junit-integration-split/isplit`) | Test / `integration-split`, every branch | a rerun of entries 'Split lanes' had run minutes earlier on the same runner and library (~2.5 min); its logs also overwrote the lane's in the uploaded artifact | the script tallies the lane step's `split-baseline.xml` and its exit code (`split-baseline.rc`) with the same three refusals; it still reruns when that step never ran (the 10-12/10-13 catches); the `split-baseline` census lane covers the names; smoke cases for the reuse, the fallback, a red exit code under a green JUnit and an empty arm | | 2026-10-17 | `Negative control B (omitted fill point)` | Test / `integration-verify` | it re-ran `PoisonOmissionScenario.WithoutOmissionCompletes` with `MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters` and accepted any red | the in-lane `DirectGLES/DirectVulkan.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb` entries: the same pair, and they also require SIGABRT with the named `Fatal{UnmigratedPipeInput}`; now required to pass by name | | 2026-10-17 | `Verify lane arming check` (grep of `pipe-verify-arming-*.log`) | Test / `integration-verify` | the VerifyArming entries assert the armed line in-test; the grep only added "the entry did not skip" | `Verify lane control entries`: `junit_tally.py --require-entry-passed` on the VerifyArming, VerifyCorrupted and PoisonOmitted entries of both backends (a skip fails) | | 2026-10-17 | `Negative control A (server-side read)` and `Negative control B (omitted stamp)` | Test / `integration-verify-split` | the first re-ran the two VerifySplitReadCorrupted entries the lane had just run and grepped what they already assert in-test (at least two where=read reports in the server half); the second re-ran with the pair the in-lane VerifySplitPoisonOmitted entries already pin, and accepted any red | `Verify-split control entries`: `--require-entry-passed` on VerifySplitArming, VerifySplitCorrupted, VerifySplitReadCorrupted and VerifySplitPoisonOmitted of both backends | | 2026-10-17 | the pre-flip arm of `Strict split lane` (red-by-design while `kMGPipeP5eRunAheadReady` was false) and the "GREEN with rows outstanding" rule of `Dual-block census lane` | Test / `integration-split-strict` | dead: the constant is true, and `dualblock-expected-fatals.txt` has no rows, so neither branch could run | the hard-green arm and its two-sided marker census, and the empty-census rule (rows == 0 requires ctest success), both kept | | 2026-10-17 | `scripts/ci/tcp_log_forwarding_control.py` | none | no caller outside the archived planning notes | none needed | | 2026-10-17 | the compile of `build-linux-split` on the disaggregation branches (when `build-guard` proves the configurations identical) | Test / `build-linux-split` | it built the library `build-linux` builds there, from a configuration that differs only by the benchmarks (211 paired runs never disagreed); it delayed every split consumer by up to 9 minutes | the `build-guard` job configures both lines on one runner and compares them with `build_fingerprint.py` (cache, compile commands, link statements, CTest files; benchmarks excluded; one shared FetchContent directory, a failed configure or one killed after 10 minutes retried once) and records its image and compiler; only the verdict `identical` lets the consumers take `build-linux`'s artifacts, and only a Release one, while `different` (annotated as a warning) and `unknown` (a configure that failed twice, any error, too little evidence; a notice) keep both builds and the full split lanes; the guard job's own failure never fails the run; `build-linux` runs the link ratchet on every disaggregation build (artifact `link-ratchet-primary`) and integration-split reds on it when it takes that build; dev runs no guard and always builds | | 2026-10-17 | `Transport A/B (monolith vs inproc)` (skip-census lanes `arm-monolith`, `arm-inproc` now optional) | Test / `integration-split` | with `build-linux` reused it repeats the integration job's two legs on the same library (79 paired runs, no one-sided red) | the integration legs; `Default-knob resolution check` (disaggregation branches) reads 'Split lanes' own run and proves, from every passed entry's client log, strict=0, role-split-state=1 and run-ahead=1 unless pinned (`split_log_paths.py ipc-config`); A/B still runs when a default differs or the build is not reused, and the skip census then requires the two arm rows | | 2026-10-17 | `Unit tests on the split runtime` (skip-census lane `unit-split` now optional) | Test / `integration-split` | with `build-linux` reused the test job runs the same unit lane on the same build (182 paired runs; the one recurring one-sided red was a fixed test race) | the test job, which now carries `MOBILEGL_ITEST_REQUIRE_GPU=1` and `libegl-mesa0` on the three branches; the strict unit step is unchanged; without a reused build the skip census requires `unit-split` | | 2026-10-17 | the integration-gpu entries that pin their own `MOBILEGL_TRANSPORT` (804 inproc, 1446 spawn, 131 monolith) and the 12 `TcpServer` fixture entries only they need, in the inproc-acceptance leg (both passes) | Test / `integration` (inproc-acceptance) | their pin overrides the only variable the two legs differ in, so they ran identically in both (173 paired runs; the one one-sided red was a port bind fixed by cde6512b) | the monolith-control leg, which runs them on every branch; `transport_pinned.py` proves per run that the trimmed selection lost exactly that population and that the monolith-control leg ran every one; when it cannot, the leg runs everything under the JUnit stems `integration-all` and `integration-staged-copy-all`; skip census: the monolith-control lanes list their own names, the inproc-acceptance lanes drop the pinned ones, and two optional `-all` rows (the full names, the old skip lists) stand in for them on that fallback | | 2026-10-17 | `Strict split lane` on the disaggregation branches (dev keeps it) | Test / `integration-split-strict` | with the role-split state defaulting on, its environment equals the dual-block census lane's, whose entries are a superset (31 paired runs after the flip, never red only in the strict lane) | the dual-block step's first run leaves the knob at its default, proves per entry from its client log that strict, the dual block and run-ahead resolved on (`ipc-config`), writes the integration-split part of its JUnit as `strict-split.xml` (every name exactly once, `junit_tally.py --select-from`), and makes the strict lane's checks on it (hard green, two-sided marker census, StrictArming ran); a default that changed reruns the census with the knob set | | 2026-10-17 | `Magma split lane (strict, dual-block)` on the disaggregation branches when the dual-block step proved its defaults | Test / `integration-split-strict` | the dual-block census run covers the `integration-magma-split` label in the same environment | the dual-block census run | | 2026-10-17 | the 75 retrace-split inproc rows other than OpenRA's, when `build-linux-split` reuses `build-linux` | Test / `retrace-split` | the retrace job's inproc-acceptance rows replay the same cases on the same library, strict and with the dual block and run-ahead on; no one-sided pair since 10-05 | the inproc-acceptance rows, which now prove per leg from the client log that they run with strict=1, role-split-state=1 and run-ahead=1 (`MGTRACE_EXPECT_IPC`) and keep a refusal census; every remaining retrace-split leg proves its defaults (strict=0, role-split-state=1, run-ahead=1) and a changed default reds every leg with the rows to restore; `trace_cases.py --self-test-split-matrix` (disaggregation branches) proves every dropped row is an acceptance row; OpenRA's inproc rows stay for the wire evidence | | 2026-10-17 | `Transport resolution check`'s grep of one entry's log for the inproc resolution sentence | Test / `integration-split` | it checked one entry (`DirectGLES.Split.PersistentMapArm.`) after the fact | in-test: the split fixture (`Harness/ScenarioFixture.h`) now fails every armed Split/Spawn/Tcp entry whose process did not resolve the transport its registration claims, or whose server is not where the claim says (a thread here for inproc, a process of its own for spawn and tcp), from the running process (`SplitRuntimePeek`, `ClientSession::PeerServerPid()`); `SplitLaneHarness.TransportClaimRule` (unit) proves the rule red on canned inputs; the step keeps the IPC default proof as `Default-knob resolution check` | | 2026-10-18 | `Negative controls (barrier, map push)` (`scripts/ci/split_negative_controls.sh`; skip-census lane `junit-integration-split/control`) | Test / `integration-split` | it reran the split entries with `MOBILEGL_IPC_VERB_BARRIER=0` and `MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0`, two product knobs that exist only for it; of what was left without them, the ownership check and the baseline tally repeat `SplitLogPaths.PrivateAndDistinct` and the lane step's own `junit_tally.py --require-split-ran`, and the `DirectGLES.Split.` rerun for a job whose lane step never ran stays as the step `Split lane fallback (lane step skipped)` | E1: `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` (unit, always on); E3(a): `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`, always on in the split-arm entries (DirectGLES inproc, spawn, tcp and small ring; Magma's in the informational tier), within one frame and across a Present. Narrower than the control: it takes the push away with `PersistentMapTracker::Forget`, which also drops the membership (only the push reads it today), and draws its own frames, so it no longer shows that the bodies of `TwoWrites...` and `AWriteAfter...` make no announcing call | | 2026-10-18 | `scripts/ci/wait_boundary_negative_control.py` | Test / `integration-split` | E1's body inside the step above: the three boundary cases rerun with the barrier off | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`: the same ApplierReset published with no wait must read parked=0, the observation the owed waits read as parked=1 | | 2026-10-18 | `scripts/ci/control_smoke_test.sh`, `scripts/ci/testdata/stub_ctest.sh`, `scripts/ci/testdata/split_private_log_smoke.sh`, and the `reset` / `results` / `assertion` / `evidence` modes of `split_log_paths.py` | Test / pipe-gates (R-16) | smoke cases and helpers of the step above only | none needed | | 2026-10-18 | unit `RemoteRunAhead.ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit`, and the `MOBILEGL_IPC_VERB_BARRIER` knob with its arm in the shipped client (`ClientSession`: the free-running `EmitAndWaitTails` return, the run-ahead latch term, the `gPipeInputs` guard's early return, the `verb-barrier=` field of the `Config: IPC` line) | Test / unit (split); product code | `0` was never a product mode, only E1's negative control, retired above; the unit case pinned that the control also disarmed run-ahead | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`; `MOBILEGL_IPC_RUN_AHEAD=0` stays the lockstep A/B arm | | 2026-10-18 | unit `SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited` (and its skip stub in the non-disaggregated half), and the `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` knob with its `0` arm in the shipped tracker (`PersistentMapTracker::PushBlocksForChecked`'s early return and once-per-process warning, `BufferObject::NotePersistentMapStateChanged`'s, the `persistent-block=` field of the `Config: IPC` line) | Test / unit; product code | `0` was never a product mode, only E3(a)'s negative control, retired above; the unit case pinned that `0` shipped nothing; the block size is now the constant 64 KiB (`PersistentMapTracker::BlockBytes()`) | `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer` | | 2026-10-18 | `Negative control A (corrupted snapshot)` | Test / `integration-verify` | it re-ran the five `DirectGLES.Verify.ClearThenReadPixelsScenario` entries with `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` exported job-wide and accepted any red | the in-lane `DirectGLES/DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported` entries, required by name (`Verify lane control entries`), which require `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@...` in the log; the abort with `MOBILEGL_PIPE_VERIFY_FATAL` at its default is unit `PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry` on the verify runtime; that the ambient entries run the comparator armed and fatal is `VerifyArming.PipeVerifyArmingScenario.Armed`, whose environment is now the ambient entries' list plus its marker and log, and which requires `fatal=1` on the arming line | | 2026-10-18 | `Negative control A2 (corrupted capability)` | Test / `integration-verify` | the same five-entry rerun with `MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled`, accepting any red; it proved that the comparator compares the capability field (P13 W3c moved the residual block's cross-check there) | unit `PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator` (`Unit tests on the verify runtime`): every field with storage, the capability field among them, corrupted in turn must be the field the comparator names; then, in a forked child with the knob set to `IsCapabilityEnabled`, each of Clear, DrawArrays and ReadPixels whose class fills the field must report it at entry, and one must | | 2026-10-18 | `Negative control A (corrupted snapshot)` and `Keep lane logs before controls` | Test / `integration-verify-split` | the first re-ran the ten `VerifySplit.ClearThenReadPixelsScenario` entries of both backends with the same knob and accepted any red; the second only kept the lane's logs from being overwritten by that rerun | the in-lane `VerifySplitCorrupted` and `VerifySplitReadCorrupted` entries of both backends, required by name (`Verify-split control entries`); that the ambient entries arm is the per-entry arm proof, and that they run fatal is `VerifySplitArming.PipeVerifyArmingScenario.Armed` (the ambient environment plus its marker and log), which requires `fatal=1` on the arming line | | 2026-10-18 | unit `PipeInputsTest.{PoisonOmitKnobArmsTheOmission, BadPoisonOmitKnobIsFatalNamingTheKnob}`, and the `MOBILEGL_PIPE_POISON_OMIT` knob: `Features.PipePoisonOmit`, its parser on every verb (`MGPipeValidateForVerb`'s `ParsePoisonOmissionKnob`, compiled into every build) and the server stamp's own parse of it | Test / unit (every poison lane); product code | the knob existed only for negative control B, and the two cases pinned its parser and its `Fatal{PipeVerifyBadKnob}` | the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker `MGITEST_POISON_OMIT` to the same pair, and the re-executed child arms it through `MGPipeSetPoisonOmission`, compiled only with `-DMOBILEGL_PIPE_VERIFY=ON` (`Harness/PoisonOmissionArm.h`); `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale`, `ReadingAnOmittedFieldAbortsNamingTheVerb` and `TheMutationNoticeRefreshesTheValueButNotTheStamp` withhold the stamp test-side and still run in every poison lane; the setter's scope to its verb, not its class, is checked by `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` in a forked child, on the verify runtime only | | 2026-10-18 | the `serial == resets` assertion of `CtWireScenario.ApplierResetCrossesAtThePrimedEdgeInSerialOrder`, and the `MOBILEGL_TEST_SERVER_COUNTERS` knob: `ServerMain`'s `MGPipe server counters:` line on every LogFlush and `ServerVerbSink`'s test-only tallies (`ApplierResets`, `ObjectDeaths`, `ExpectedApplierResetSerial`) | Test / `integration-split`, `integration-split-strict`, `integration-verify-split`, `integration (inproc)`; product code | the knob existed only so the spawn and tcp arms could read the sink's tallies; the serial assertion could not fail, since `OnApplierReset` moves the count and the serial together and any other serial latches `Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}` | `ar` / `od` on the server role's `MGPipe stats:` line, counted in `ServerVerbSink::OnApplierReset` / `OnObjectDeath`; every CtWire entry that counts them sets `MOBILEGL_PIPE_STATS=1` with period 1 and reads them on every arm, inproc included, over a window closed by a swap and a readback; the unit cases `ApplierResetWireRoundTrip.TheSerialSequenceIsAssertedAndTheServerResetRuns` and `ObjectDeathWireRoundTrip.TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration` read the same counters, and the first shows the expected serial moving by having serial 2 accepted | ## Kept after investigation ### `minecraft-1.17-main-menu-854` (Test retrace / retrace-split / retrace-verify, APK retrace), 2026-10-06 - **Symptom**: every lane, both backends, SSIM 0.0186 against the golden; the frame is Minecraft's red loading-screen color. Local repro on WSL llvmpipe: same SSIM, monolith and inproc. - **Root cause**: the trace was captured on an implementation that gave shaders and programs **overlapping names** (shader 1 and program 1 live at once, likewise shader 5 / program 5, calls 159-209). GL 4.6 core and ES 3.2 ยง7.1 put shader and program objects in one shared name space, so a conformant implementation never hands out such a pair. Since `5a0f13fdf` / `168808d1a` (2026-10-01/02) MobileGL advertises `GL_ARB_shader_objects`, and apitrace's glretrace then resolves every shader and program name through one ARB handle map (`_handleARB_map`): program 1 overwrote shader 1's entry, `glAttachShader(1, 1)` attached a program name (GL_INVALID_OPERATION at call 170), the blit program linked without a vertex shader, and nothing after it drew. - **Classification**: (b) retrace tooling - not a MobileGL bug and not a bad golden. The core entry points are typed (`glAttachShader(program, shader)`), so mapping each argument through its own type's map is lossless even when the capture's names collide; only the untyped `*ObjectARB` handle calls need the shared map. - **Fix**: MobileGL-Dev/apitrace `45843686` (branch `mobilegl/android-trace-replay`): typed `GLprogram` / `GLshader` arguments resolve through `_program_map` / `_shader_map` first and fall back to the ARB handle map only for names created by `glCreate*ObjectARB`; typed creators record both maps. Submodule bumped in the same MobileGL commit. Local: 1.17 passes on DirectGLES (monolith, inproc) and DirectVulkan (lavapipe). - **Decision**: keep the case. With typed mapping it checks GL-level correctness of MobileGL like any other fixture; the namespace overlap is a property of the capture that a spec-portable replay can absorb. - **Side note (not changed)**: MobileGL advertises `GL_ARB_shader_objects` while its `glCreateShaderObjectARB` / `glCreateProgramObjectARB` entry points are stubs (`MG_Impl/GLImpl/Exporting/Definitions.cpp`). The string was added on purpose for KWin; whether to implement the ARB handle entry points is a separate decision. ### `SanityTest` `DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture`, 2026-10-06 (removed in W3b, then RESTORED) - **First call (wrong)**: retired with the P2-mask probes as "its arm is gone". The orchestrator's review asked whether its failure at the shipping mask was a behavior gap. - **Finding**: the code it pins is LIVE. The monolith arm (DISAGG builds under `MOBILEGL_TRANSPORT=monolith`, and the FCL build) still walks the frontend units with the borrowed-slot work list and its `PairingsIntact` check (`UnitTexturesByHandle()` is false on monolith). Its red at 0x3fff is not a product gap: at that mask the twin-sync body it observes reads applier records, and a unit-lane process cannot create them (`MGPipeApplyResourceCreate` declines under `NoP4aConsumer`), so the probe observed nothing. - **GL-level replacement tried and measured**: `DsaUpdateKeepsTheBoundUnitScenario` (A bound to unit 0; B respecified and filled by name; unit 0 must still sample A). Red-checked locally: it stays GREEN with `PairingsIntact` disabled, and still green with the frontend's by-name bind-generation bump ALSO removed, and with by-name mipmap / readback / parameter / copy calls added in the window. The hazard is no longer reachable through public GL: the by-name emulation restores the slot before any draw can replay the list. The scenario stays as a GL 4.6 8.1/8.5 semantics test on every lane, but it is NOT a guard of the pairing check and is not claimed as one. - **Decision**: the probe is restored, pinned to the P2 mask in its own process (the code under test is mask-independent; the mask only selects the sync body it observes). Red-checked locally: with `PairingsIntact` disabled it goes red at "the replay never reached the texture now in the slot" (the stale pairing was replayed instead of the texture the slot holds); restored, green. The resident-respecify assertion now runs first so a red names that damage when it occurs. It goes with the borrowed-slot list itself when W4c moves monolith to the by-handle unit walk. - **P13 W6**: monolith moved to the by-handle unit walk and the borrowed-slot list was deleted; the probe went with it (Removals table). ## Infrastructure fixes (no check removed) | Date | What was red | Cause | Fix | |---|---|---|---| | 2026-10-06 | `Direct{GLES,Vulkan}.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb` (verify tree, local; P13 W5 S7) | monolith now runs the record arm, which resolves GenerateMipmap's texture from the verb's handle and never reads the omitted `GetActiveTextureUnit` - the control could not go red | the monolith lanes omit `ReadPixels:GetPixelStoreParameters` (the pair the split lane already uses), which the record arm still reads; verified red-for-its-reason on both backends. Same change in test.yml's `integration-verify` "Negative control B (omitted fill point)" step, which went green with the old pair on Test 37515697754 | | 2026-10-06 | `integration-verify`, 184 aborts `Fatal{PipeVerifyDiffer, "GetPixelStoreParameters@ReadPixels"}` (verify tree, local; P13 W5 S7) | the verify read hook took the live context as the pack oracle inside the monolith verb port's neutral-pack readback window (it already used the applied pack for server-stamped verbs) | `MG_Record::MonolithPortApplyingForVerify()`; the hook uses the applied pack payload in that window too | | 2026-10-06 | `ResourceEmit.TheLiveHostWritesWireFiresOnTheCallAPersistentMapProducerWouldSetItOn` (verify unit, local; P13 W5 S7) | the always-false `HasLiveHostWrites` pin is lifted wherever the record arm is built (it gives the bit a producer), and every library builds it now | the case skips under `MOBILEGL_BUILD_RECORD_ARM` with the existing reason; `PinLiveHostWritesNamesABuffer` remains the successor | | 2026-10-06 | every `ctest` invocation in integration / verify / split jobs exited 8 after the tests passed (`integration-verify`, `integration-split*`, `integration (inproc)`, `spawn_lane_parity.py`) | `PipeCatalogSourceRoot.cmake` (a CTest include file) used `if(... IN_LIST ...)`; CTest scripts run without policy CMP0057 on the pinned CMake 3.31.10, so the include errored | use `list(FIND)` instead (`MobileGL/MG_Test/Pipe/CMakeLists.txt`) | | 2026-10-06 | `TcpServer.Start.P8aUnlocatedIoBlocks` failed in 0.07 s on `integration (monolith)` | the loopback TCP endpoints (40613, knob lanes 41613-41617) sat inside Linux's ephemeral port range; with thousands of loopback client connections in the lane, an outgoing socket can own the port the supervisor binds | default endpoint moved to 30613 (knob lanes 31613-31617), below 32768 | | 2026-10-06 | spawn lanes on hosted runners: `no Welcome within 5000 ms` | a cold `eglInitialize` on the hosted software stack took ~7 s, longer than the spawn handshake's Welcome wait | the Welcome wait takes `max(5000, MOBILEGL_IPC_COLD_START_MS)` (`ClientSession.cpp ColdHandshakeBudgetMs`); the initial-caps wait stays 5000 ms, which `InitialCapsStartup` pins | | 2026-10-06 | `FencePollScenario` on `integration-verify-split` | the test took its `before` counter snapshot after `glFenceSync`, racing the apply thread that answers the fence | snapshot taken before the work and the fence are queued (test bug, not a product bug) | | 2026-10-06 | `LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime` on `integration-split-strict` (run 37433779649) | test race: when the channel's sender thread started late it dequeued its first line after the 16 KiB queue had already filled, so the lossy class recovered mid-flood and closed a first gap (4 drops) with its own notice - two notices, 1880 named vs 1884 counted. The channel did what its contract says (one notice per gap) | the test parks the sender in its held write (`WaitForAttempt`) before the flood, so the flood makes exactly one gap; 100/100 local runs green | | 2026-10-06 | `TcpLane.EventForfeitPeer` (`EventForfeitPeer.StreamLogFloodWhileControlIsNotReadHoldsNoApplyThreadAndLosesNoError`) on `integration-split` (run 37433779649) | `notices <= 8` was one machine's measurement: every time the socket takes another burst during the flood, the WARN class recovers and its gap closes with a notice; the loaded runner made 14. Every other F2 assertion held (drops named exactly, no ERROR lost, no forward held its thread) | the bound is now `kFloodRecords / 10` (40), still an order of magnitude below the regression it guards (373, one notice per ERROR line) | | 2026-10-06 | `DirectGLES.SubsystemMaskRefusal.ObjectSubsystemControlRefusedTexture` flaky under `ctest -j` (a W3b control) | the control globbed `*` for its logs, which also matched the longer-named sibling `...RefusedTexture`; the shorter control deleted the sibling's log while it ran | glob `.*` (`Harness/RefusedMaskControl.cmake`) | | 2026-10-06 | `MGPipe generators and hygiene gates` and `trace case matrix` (fatal census) on the W3b/W3c push | W3c's retired `Wire_SetResidualValueState` died through its own `std::abort()` (the census counts server-image aborts that bypass `SessionFail`), and W3b's mechanical `MGB_CTX` rewrite reached a string in `FieldOwnership.def` that the generator copies into `PipeFieldOwnership.inc` | the row dies through `SessionFail(ResidualBlockSize, ...)`; the `.def` text is restored so the generated file is unchanged | | 2026-10-06 | skip census: `Direct{GLES,Vulkan}.HandleRecycle.Handles.*` and `...AbaControlHandles.*` leak cases pass -> SKIPPED | W3b stopped pinning `MOBILEGL_PIPE_PUSH` on those lanes (bits 0-13 are fixed on), and the scenario's `LanePinnedALiveAllocator` read "no pin" as "no allocator". A real regression in coverage, caught by the census on its first full run | an unpinned HandleRecycle lane runs the full mask and has the allocator; the cases run (and pass) again. The census also retired the `PeerLatchSite/SetResidualValueStateBlobSize` row W3c deleted | | 2026-10-06 | skip census on the W6a/b push (run 37534648608): `DirectGLES.*CubeFaceReadbackScenario.CubeMapViewOfAnArrayReadsTheFaceEachZOffsetNames` SKIPPED, baseline new (8 lanes) | the new case skips where the backend does not advertise `GL_ARB_texture_view` (Espryt on the CI's ES driver refuses `glTextureView` rather than emulate it by copying); a legitimate capability skip, not a lost case | the 8 lane entries are recorded as skip in `scripts/data/skip_census_baseline.json`; the Magma lanes run the case |