- All four capability markers are now content probes over the directory the owning package owns,
through one helper. The magma pair still read a single hard-coded VertexInputStateFactory.cpp
while package D already keeps one of its two Features.PipeHandleAbaControl consumers in
Renderer/VulkanRenderer.cpp, so one file move on D's side was a permanent AbaControl skip - the
same defect the CSO probe was rewritten for. The DirectGLES probe stops asking whether
SlotTables.h exists and asks for kMGPipeSubsystemEsprytSlots, the bit the arm is actually gated
on. Every globbed file stays in CMAKE_CONFIGURE_DEPENDS, and the glob is CONFIGURE_DEPENDS.
- Verified: with one throwaway header naming each symbol, build-push configures to "keyed on
{slot, gen}" / "has an emitter" / "has a consumer" and all four MGITEST_* markers appear 24
times in the generated ctest environments; with the headers gone, all four are back to 0 and
the four "will SKIP" verdicts return. The magma sim sat in Renderer/, not in the path the old
probe hard-coded.
- g7_negative_control.sh no longer exits 0 when the control trips for the wrong reason. A
SetterConsistency that had gone red for an unrelated reason satisfied "ctest failed" and never
named SetColorMask, and the integrator's D.3 reads this script's rc. The verdict is now taken
after the restore and the rebuild - a broken build directory is worse than any exit status -
and reported as rc 1 with the output kept, alongside the existing "did not trip" rc 1.
- HandleRecycleScenario writes down what the name-recycle proxy costs: the AbaControl arm asserts
corruption that needs the heap BLOCK back, sees only the NAME, and so can red an always-on
integration-gpu lane for an allocator reason. That trade is deliberate - the alternative is an
arm that is green on the day the reproducer stops reproducing - and the consequence is now
written both in the header and at the skip that is the last thing standing between the two.
- the three capability markers were decided from source-tree file existence / file text alone, so
after packages C and D land they would have armed the PULL build too, where every arm they name
is compiled out: the AbaControl lane would have gone hard red on `ctest -L integration-gpu`
(gate G2 requires it green in both builds) and the Handles lane green against a library with no
{slot, gen} key at all. The whole block now sits under the same `if (MOBILEGL_PIPE_PUSH)` as
MGITEST_PIPE_PUSH_BUILD, and HandleRecycleScenario re-checks that marker before either push arm
asserts, so a hand-forced environment cannot arm an arm this build does not have either
- the two push-only knobs of those lanes (MOBILEGL_PIPE_LEGACY_MEMOS=0, MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1)
are set only in a push build. In a pull build the legacy arm is the only arm and every subsystem
bit is clear, which is D14's startup Fatal{PipeLegacyMemosDisabled} - the process would abort
before the scenario could report its skip. Test NAMES are unaffected, so G2 still compares equal
- the CSO control armed itself off `MG_Impl/Pipe/Tracker.cpp`, a file the owning package does not
create: it implements the tracker and the cache header-only, so all four CsoContentAddressing
entries would have kept skipping after it landed, with a reason that had become false. The probe
now greps every source under MG_Impl/Pipe/ for the two counters the control actually reads
(RenderStateCsoMints / RenderStateCsoBinds), watching the directory and each file, so the owning
package keeps control of its file layout
- an unrecognised MGITEST_HANDLE_ARM is a FAIL in SetUp instead of a silent downgrade to the
Legacy arm, which would have passed while claiming to be the lane it was not
- G2 requires `ctest -L integration-gpu` to be name-for-name IDENTICAL between the pull build and
the push build, and the four CsoContentAddressing lanes were registered inside
`if (MOBILEGL_PIPE_PUSH)`. That is four entries the push build has and the pull build does not,
which breaks the comparison for this package and for every package that lands after it.
- They now register unconditionally. What the pull build lacks is not the entry but the thing the
entry is about, so the build passes MGITEST_PIPE_PUSH_BUILD in and the scenario skips saying
exactly that: no render-state CSO exists, no cso[] bracket is compiled into the summary line,
and the content-addressing bit steers nothing.
- The marker also sharpens the plumbing assertion it guards. Past that skip the process is known
to be a push build, and the cso[] bracket is unconditional inside that same #if - so a missing
bracket can no longer mean "wrong build configuration" and the failure message stops offering
that as an explanation.
- Verified: build-linux and build-push now differ by zero ctest names (diff empty over 1408
entries each), and the pull lanes skip with the push-build reason while the push lanes skip
with the tracker-not-landed reason.
- HandleRecycleScenario builds the ABA the Track H re-key has to survive: an object is drawn
for three frames so every per-object memo is armed against it, unbound so its last SharedPtr
drops, deleted, and replaced immediately by one with a byte-identical configuration and
different contents. Three kinds - a vertex array whose buffer is recycled with it, a texture,
a framebuffer - and the readback must come from the replacement.
- The reproducer is asserted, not assumed. TheReproducerRecyclesEveryName pins that the name
allocators hand every deleted name straight back, and a case whose names were not recycled
SKIPS as "inconclusive, not proven" rather than passing - the shape ObjectLifetimeIdTest
already uses.
- Three always-on arms, one ctest lane each, named by the harness marker MGITEST_HANDLE_ARM:
Handles ({slot, gen} only), Legacy (today's lifetimeId + weak_ptr guards) and AbaControl
(MOBILEGL_PIPE_HANDLE_ABA_CONTROL=1, which expects the CORRUPTION so that a reproducer that
stopped reproducing is a red rather than a quieter green). AbaControl is DirectVulkan only:
the knob reverts two DirectVulkan guards and steers nothing on DirectGLES.
- CsoContentAddressingScenario is the G12 control. A Blaze3D blend toggle - enable/draw/
disable/draw x 8 inside one frame - must mint a BOUNDED number of CSOs with content
addressing on and exactly one per bind with bit 63 of MOBILEGL_PIPE_PUSH set, while the
pixels do not move at all. csom == csob is the reading a dead switch cannot produce.
- The counters are read from the library's own "MGPipe stats:" line, because PipeStats is
internal and this module links the shipping library on Android. Each arm therefore gets
MOBILEGL_PIPE_STATS_PERIOD=1 and a private MOBILEGL_LOG_FILE_PATH, the same per-lane rule
the arming lane already follows, and the workload is bracketed by two swaps so the window
covers itself and nothing else.
- Both scenarios skip in the ambient entries, which configure none of the knobs their arms are
about, and both name what is missing when the package they depend on has not landed. What
decides that is the BUILD, not a hand-written guard: CMakeLists looks for
DirectGLES/SlotTables.h, MG_Impl/Pipe/Tracker.cpp and the two markers inside
VertexInputStateFactory.cpp, prints each verdict, and re-evaluates through CONFIGURE_DEPENDS -
so the arms arm themselves when packages B, C and D land.
- 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
- 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.
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself.
- The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs.
- PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct.
- AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers.
- Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green.
Android links this module against the SHIPPING libMobileGL.so on purpose, so the
on-device run validates the real artifact - and that library is -fvisibility=hidden.
Six symbols the scenarios reached for were therefore undefined and the executable
could not be linked at all: MG_Config::Features, SetAsyncShaderCompileSuspended,
ShaderCompilePool::Get/GetThreadCount/SetMaxConcurrency and
AsyncShaderCompileEnabled. All six are gone rather than exported.
CompilerThreadScope now restores the pool with glMaxShaderCompilerThreadsKHR
(0xFFFFFFFF), which MaxShaderCompilerThreadsKHR_State defines as exactly the two
steps it used to perform by hand, and AsyncAndSyncProgramsRenderIdenticalFrames
picks its two modes with the same entry point - a zero count compiles inline, a
nonzero one lifts that - so the switching is now itself under test.
ExtensionStringMatchesTheConfiguration stops deriving its expectation from
AsyncShaderCompileEnabled(), the very function the backends gate the extension
string on: it was asserting the implementation against itself and would have passed
however wrong both halves were. The expectation is now MOBILEGL_ASYNC_SHADER_COMPILE
as the process inherited it, and the case skips where that is unset because the
built-in default is a value only the implementation knows.
Both-mode coverage in one ctest run is preserved by registration rather than by
in-process forcing, and is wider than before. New entries, each APPENDING to the
common/Vulkan environment so the EGL-vendor and ICD pinning is not lost - a ctest
ENVIRONMENT property replaces the job environment rather than adding to it:
DirectGLES./DirectVulkan.AsyncOn. run all of AsyncCompileScenario with
MOBILEGL_ASYNC_SHADER_COMPILE=1; .AsyncOff. run the extension case with =0, which
asserts the withdrawn side that nothing covered before;
.OptimisticShaderStatus. run the Iris-shaped case with
MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1 (its own entries because the quirk is not
neutral for the rest of the scenario); DirectGLES.NoViewportArrayEmulation. runs the
emulation control with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0. Run from a device
shell with nothing set, the ambient configuration runs and the rest skip cleanly.
1836 -> 1853 ctest entries, all green.
Add a deterministic iterationRP Program 203 fixture that dispatches the original shader and compares every RG16F texel against fixed half-float golden bits. This catches both a wrong exposure result and collateral writes without retaining a serial reference shader.
Make MobileGLIntegrationTest runnable as a standalone Android executable by linking the shared MobileGL library and backing EGL with an AImageReader window; desktop keeps its static-library pbuffer path.
Validation: Adreno 830 passes with 0/262656 mismatches; lavapipe reproduces the current reduction defect with 1/262656 mismatches at the exposure texel.
iterationRP's Program 203 declares shared vec2 prefixSumCache[32] for a
512-invocation workgroup indexed by gl_SubgroupID; any device narrower
than 16 lanes partitions into more than 32 subgroups and the pack writes
shared memory out of bounds (heap corruption on lavapipe's CPU
rasterizer, ssim 0.028 on the CI retrace). Fix it where the fault lies -
in the fixture - and keep the GL contract sound everywhere else:
- FixIterationRPSubgroupScratchPass: fingerprint-gated SPIR-V pass that
grows exactly that array to ceil(invocations/width) entries on sub-16-lane devices; every other module passes through byte-identical.
- DeriveNumSubgroupsPass stays default-on for the Adreno topology bug
and is made spec-sound: pipelines request REQUIRE_FULL_SUBGROUPS
whenever the workgroup shape makes the flag legal (computeFullSubgroups
enabled, local_size_x a multiple of the native width, subgroup count
within maxComputeWorkgroupSubgroups).
- EmulateSubgroupsPass: 32-lane virtual-subgroup lowering kept in-tree
as a last resort, enabled only by MOBILEGL_MAGMA_EMULATE_SUBGROUP=1 on
devices with no native subgroup support; fails closed on extended
subgroup instructions and on modules whose added scratch would exceed
maxComputeSharedMemorySize.
- IterationRPFirstReductionScenario skips gracefully outside the pack's
16..256-lane source domain; the new IterationRPScratchFixScenario runs
the fixture-shaped reduction on any width and asserts the exact
width-independent total. DriverPost keeps reporting FAIL on
out-of-domain devices.
- Program203 -> IterationRP rename throughout; the per-trace
num_subgroups_quirk plumbing is removed from the trace replayer, JNI
chain, and CI workflows.