The Android retrace matrix gains a third lane, spawn-acceptance, beside
monolith-control and inproc-acceptance. Until now nothing in CI had ever
started a second MobileGL process on a device at all.
The apk.yml retrace job already carried a `transport` matrix dimension and
already branched on it, so the lane itself is three edits: a row in
github_transport_matrix, an elif in the retrace step, and a summary to render.
What needed thought was the gate.
THE SPAWN LANE IS APK-ONLY, and that is a statement about where evidence is
missing rather than about what spawn can do. Linux already has
test.yml's `retrace-split`, which is {backend, case, transport} over inproc AND
spawn with the two negative controls that make it mean anything. A spawn row in
the Linux transport matrix would pay for a second full sweep proving nothing -
and test.yml's `retrace` job stages no server image, so the lane would not
merely be redundant, it would be red. self_test_transport_matrices now asserts
the absence in both directions, because an accidental removal of the `if apk`
has to be caught in the self-test rather than in CI.
--require-spawn REQUIRES TWO SENTENCES, and the second is the one that matters.
ConfigLoader's marker proves the VALUE RESOLVED - it exists only in the Spawn
arm, which exists only under MOBILEGL_BUILD_DISAGGREGATED - but that is a
statement about a parser. `spawn ARMED - the server role runs in pid N` is
written only after the launch, the connect and the handshake have all
succeeded, and the pid in it is the SERVER's. It is the only line in the log a
same-process run cannot produce.
Both halves red once, measured:
* a monolith run demanded to prove spawn: REFUSED, transport_resolution_marker
false, server_pid null - while the retrace itself scored ssim=1.000000,
which is exactly the trap. OpenRA matches its golden under monolith too, so
the picture has never been the evidence.
* the same run's real spawn log with the one ARMED line deleted: REFUSED,
transport_resolution_marker TRUE and server_pid null. That is the
"resolved the transport and then never left the process" case, isolated.
THE PROOF USES `any` WHERE THE inproc GATE USES `all`, and the difference is
load-bearing rather than a relaxation. Under spawn two processes append to one
MOBILEGL_LOG_FILE_PATH, and the launcher deliberately scrubs every
MOBILEGL_IPC_* from the child's environment (anti-recursion catch (b)) - so the
server's own `Config: IPC` line reads strict=0 role-split-state=0 BY
CONSTRUCTION. Demanding every line match would red every spawn run for the one
thing the design requires.
--use-pbuffer is not optional on this lane: until P12 a spawned server cannot be
handed a window, because an ANativeWindow* is a pointer into the CLIENT's
process (Rule H). MOBILEGL_IPC_SERVER_PATH is deliberately NOT passed -
nativeLibraryDir carries an install-time hash that nothing off the device can
spell, and the Activity resolves it.
The local runner now passes the same knobs and the same --require-* flag CI
does, so "it works locally" can no longer mean "it ran monolith locally". That
change immediately caught its own omission: the first local run with the gate
armed failed on client_ipc_line_present=false because the runner had been
sending MOBILEGL_TRANSPORT alone.
Verified on device 2f7cbe2e: OpenRA.DirectGLES spawn, ssim=1.000000,
mismatchPixels=0, "real spawn transport, server role in pid 9885, and zero
Fatal records confirmed".
It was one case on one backend under one transport: OpenRA, DirectGLES,
inproc. It is now every CI case, both backends, inproc AND spawn - 154 CI
entries, 156 local ctest entries - because every defect the spawn work turned
up was a question about the SERVER's process and not about any one trace or
either backend.
THE DirectVulkan ARM. tools/trace_replay/CMakeLists.txt carried "a
DirectVulkan arm would be measuring a server nobody has written yet". True
when P5 wrote it; false since P5f/P7 landed Magma's server consumers and its
run-ahead readiness gate. Leaving it would have shipped a split shape whose
second backend nothing ever ran - the same class of hole as a lane that runs
monolith under a name that says split. Magma now brings up a server process
of its own and OpenRA retraces across it at ssim=1.000000, which is only
possible because this branch also taught the child to load its config: before
that, a DirectVulkan session brought up a DirectGLES server and the two
halves disagreed about which backend the capability mask describes.
`split` BECOMES AN OPT-OUT. It began as an opt-in naming one case, because
one case was the work. Marking 39 cases `"split": true` would carry no
information and would reinstate the hole the key was written to close, only
mirrored: a case added later would get the monolith arms and be silently
absent from the split ones, and absent is the one state nothing reds on. A CI
case is now in the subset unless it says `"split": false`, which is a
reviewable claim that a trace cannot cross a role boundary.
THE CI MATRIX GAINS A THIRD DIMENSION. The job took {backend, case} and
hard-coded MOBILEGL_TRANSPORT=inproc in its own run block, so `spawn` had no
way into CI at all; the .SPAWN ctest entries existed only for a local build.
It is {backend, case, transport} now, the backends are the case's own
ci_backends rather than a hard-coded DirectGLES, and the artifact names carry
the transport or the two arms of one {backend, case} would overwrite each
other's uploads.
inproc STAYS AS THE CONTROL. Same library, same codec, same applier, same
picture; the only difference is which process the server role runs in.
Running only spawn would leave "did the transport break it?" and "did the
phase break it?" answerable only together - which is the question every
failure in this phase has turned on, including the SIGSEGV that D1 explained.
BOTH NEGATIVE CONTROLS FOLLOW THE ARM. retrace_pull_library_control.sh and
retrace_drop_draw_control.sh each exported MOBILEGL_TRANSPORT=inproc
themselves, so on a spawn job they would have gone on proving something about
the other arm while spawn ran unguarded. They read SPLIT_TRANSPORT now, and
default to inproc so a caller that sets nothing behaves exactly as before.
scripts/ci/control_smoke_test.sh: 22 passed, 0 failed against the stubbed
ctest, including the two modes that matter most - a red whose SSIM never fell
and a knob that armed and dropped nothing.
The spawn arm needs a second binary and the inproc arm needs none, so the job
places libMobileGLServer.so only on the spawn arm and a missing one is a
named failure there rather than a LaunchServer refusal 78 jobs deep.
Measured locally as this lands: 39 of the 78 SPAWN entries complete, all
green. The rest are still running.
The step located the Android CMake identity by reading CMakeCache.txt next
to each compile_commands.json, but the trace flavor's build also produces
.cxx/tools/<flavor>/<abi>/compile_commands.json copies whose cache lives in
the real per-ABI build dir, so the proof failed on a file-layout assumption
rather than on missing evidence. Remove the step and its artifact upload.
5310ff62 generates the R11F_G11F_B10F chain with a real GPU shader blit and gives
F1WireScenario.GenerateMipmapPackedFloatPixels pixel assertions, so the entry now
passes under strict split state and `Magma:mipmap-native-format@P7` survives in no
source file. The old arm would therefore have failed its own `test rc -ne 0`.
Both halves of the arm are retired in one commit, because either alone is a lie:
CMakeLists' fm loop drops the special case that gave this slot the
`integration-magma-p7-known-red` label, so the entry is discovered by the ordinary
`integration-magma-split` lane - the same label, environment and pixels assertion
as GenerateMipmapPixels and GenerateMipmapDepthPixels next to it, and the same
label the dual-block census lane already re-runs.
The lane membership alone would not be the old gate's equal: a lane counts its
entries' exit codes, not their absence. So the CI step keeps its shape and inverts
its expectation - the same single named entry, discovered and run by name under
MOBILEGL_IPC_STRICT_ERRORS=1 and MOBILEGL_IPC_ROLE_SPLIT_STATE=1, now asserted
through split_log_paths.py's require_green, which rejects a skip, a missing entry
and a failure alike. No assertion is skipped away or weakened: the pixels are
checked by the test, and the named-refusal check the log carried is what the GPU
blit retired.
The remove-artifact-clutter jobs deleted trace fixtures and results while
reading job/artifact inventories through unchecked process substitution, so
a failed GitHub API page looked like 'no failed cases' and fixtures for a
red retrace could be deleted. test.yml cleanup also did not wait for the
retrace-split lane even though those jobs download the same fixtures.
Read both paginated inventories into files before any deletion so an API
failure fails the step, add retrace-split to the cleanup dependencies, and
retain fixtures plus result artifacts for failed cases in all three lanes.
- ID-133: escalation (iii) in MGPipeBarriered - a draw_vbo with NumDraws > 1 that is NOT
kDrawIsIndirect is barriered. Espryt's indirect multi-draw tiers reach
MultiDrawImpl::RunIndirect, whose apply reads the client's GL_DRAW_INDIRECT_BUFFER binding
(BoundDrawIndirectBufferId, MultiDraw.cpp:87) - GetBufferBindingSlot, retiring in P8 - and
unbarriered that is Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"} on 18 lane
entries. Backtrace of the aborting thread, verbatim frames: CountBarrierPull <-
MultiDrawImpl::RunIndirect <- MultiDrawImpl::DrawElementsBatch <- ServerVerbSink::OnDrawVbo
<- PipeWireDecoder::DecodeAndApply <- PipeApplier::ApplyOne.
- TWO PREMISE CORRECTIONS, both measured, both in the report and in the contract. (a) There is
no indirect multi-draw OP: all twenty draw entry points collapse onto draw_vbo, and the tier
is resolved on the server per batch (ResolveTierForBatch), so the escalation cannot be keyed
on the tier and its cost is NOT confined to the opt-in tier lanes - a plain glMultiDraw* now
waits on the auto -> ext default too. NumDraws > 1 is the narrowest wire fact that contains
the reaching set; kDrawIsIndirect is excluded so Sodium's indirect draw, which resolves by
handle and pulls nothing, keeps running ahead. (b) The read is a SAVE/RESTORE of a GL binding
name around the tier's own scratch buffer, not a data dependency - so it can be retired
outright by giving BoundDrawIndirectBufferId the handle arm its neighbour
ResolveBoundIndexBuffer already has, and this escalation withdrawn. That is three lines in
MultiDraw.cpp, which ID-113 gives to mv, so it is reported rather than edited here.
- The client's half is verb-keyed because the fill decision is asked before the record exists:
{MultiDrawArrays, MultiDrawElements, MultiDrawElementsBaseVertex} is exactly the set whose
record can carry NumDraws > 1 without kDrawIsIndirect. The inclusion goes that way round on
purpose - a record the server calls barriered whose fields the client did not fill would be
an ADMITTED pull of a stale value, which is worse than the abort.
- ID-134: the two ClientVertexArrayScenario entries get the label integration-clientarrays-split
and an expected-red CI step that asserts the MARKER, shaped like gl's Magma lane. ID-131 heeded
by COUNTING, not by assuming: -L integration-split 181 -> 179, -L integration-gpu 1359 -> 1357,
the new label selects exactly 2. The step branches on whether run-ahead is actually ARMED,
because kMGPipeP5eRunAheadReady is a BUILD constant: on a head where it is false the refusal is
unreachable and the entries are legitimately green, so an unconditional demand for red would
fail for the one reason that is not a defect. The client says which world it is in in its own
log line; delete the green branch when the flip lands for good.
- CONTRACT-P5E §3.5 amended to the code: "outside a barriered fill" rested on the caller's claim
about the FUTURE, and a guard whose exemption cannot be false on the class it exists for is not
a guard - it never fired on any of the 70 red entries. The landed rule is "not while the applier
is inside, barriered fill or not", with the fill sites taking §2.5's forced wait first, twice
per filling verb because the block is written either side of the emitters.
- AND §3.2 IS MARKED UNLANDED, which nobody had noticed: "the server's stamp is server-private
… never into the shared block" describes storage the applier owns, and
MGPipeServerStampVerbBoundary still writes m_filled / m_currentVerb / m_serverStampedVerb into
gPipeInputs itself. That gap is what made the race possible. Splitting the stamp is the P11
item - but per-role stamps WITHOUT versioning the BARRIER_PULLED values would be worse than
today, because it turns a loud Fatal into a silent stale read.
- §2.1 carries escalation (iii) as a row, per its own rule that a new escalation is a ruling and
a row in the contract rather than a condition at a call site.
- Gate on the flipped head: unit 2256/2256, isplit 179/179 HARD GREEN, gens rc=0, doc citations
rc=0 (the three remaining are CONTRACT-P5.md's and pre-date this branch).
- Both DirectVulkan.Split.NamedBlit entries carried LABELS "integration-split" and abort under
strict on GetFramebufferBindingSlot@Clear (VulkanRenderer.cpp:7648). P5e does not unbarrier that
field on Magma - it retires there in P7 - so the Espryt lane could not go hard green with them
in it.
- ARGUED RATHER THAN ASSUMED, because the brief allowed either reading: admitting the row instead
is the WORSE option. FieldOwnership.def carries ONE retiring-phase string for both backends
("P5e (Espryt unbarriered), P7 (Magma)"), so a rule that admitted the pair for Magma would admit
it for Espryt too - and an Espryt Clear reading the frontend's binding slot again is exactly the
regression fb's handle arm exists to prevent. The lane split costs two entries; the admission
would cost the gate.
- THE LABEL IS `integration-magma-split`, NOT `integration-split-magma`, and the spelling is
load-bearing: `ctest -L` takes a REGEX, so `-L integration-split` matches a label spelled
`integration-split-magma` as a substring. Measured with the obvious name first: both DirectVulkan
entries were still listed by `ctest -N -L integration-split`, i.e. the split looked done in every
listing and had not happened.
- Its CI step asserts the run is RED **and** that a private log carries the named marker, so
"Magma is expected red" cannot decay into "Magma did not run". Measured: 0/2 under strict with
two logs carrying Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}, and 2/2 green
without the knob. integration-split is 117 and integration-magma-split is 2.
- CONTRACT-P5E section 7 states the split and the reason it is not an allowlist entry.
Three measured defects in the strict lane, one commit each in spirit and one in fact because
they are the same step.
1. THE LOG SET CAME FROM A DIRECTORY. The CI step grepped MG_IntegrationTest/split-logs/, which
holds 90 of the lane's entries. The ones it could not see were the F1. readback block, both
NamedBlit pairs, the four Ct. entries and PersistentMapArm - precisely the readback population
the allowlist is ABOUT, plus the only DirectVulkan entries in the lane. split_log_paths.py
gains a `markers` mode that walks ctest's own {entry: MOBILEGL_LOG_FILE_PATH} map and
classifies every Fatal{ and Admitted{ line. Measured on this head: 113 of 113 entries read.
The lane is now a TWO-SIDED RATCHET - an unadmitted marker fails, an admitted pair not in
Harness/strict-expected-markers.txt fails, AND an expected pair that no longer appears fails,
so the lane cannot rot green by outliving its own debts.
2. THE `rsp` CHECK GREPPED FOR A COUNTER NO SPLIT ENTRY EMITS, and the pin behind it was vacuous:
CountBarrierPull's unbarriered arm is [[noreturn]] and runs BEFORE ++g_residualPulls, so
"rsp = 0 on unbarriered records" was true of every possible implementation. Replaced by an
assertion where the counter actually is - one entry, DirectGLES.Split.StrictArming., with
MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private log path of its own (a
whole-lane env flip races under -j: the library opens that path "w"). It asserts
rsp == 0 || rsp >= draws, which is the device evidence's shape - rsp ~= the draw count under
inproc, 0 under monolith - so the draw path's retirement is visible as rsp ceasing to scale.
3. THE LANE HAD NO POSITIVE CONTROL (ID-115). Of the seven entries that passed strict, three were
monolith transport - where nothing stamps a verb boundary, so the whole mechanism is
structurally unreachable - two self-skipped, one was a death test and one was a Python check.
Not one was a record-carrying split GL scenario, so "green" and "strict was never armed" were
the same observation. New push-only counter `vbs` (PipeStats::ServerVerbBoundaries), counted
at MGPipeServerStampVerbBoundary behind Enabled(), and the same entry asserts vbs > 0 on a
frame that drew. It is deliberately NOT rsp: rsp reaches zero when the phase SUCCEEDS, so a
control built on it would start failing on the day the debt is paid.
- Measured on this head (base 7c6f6886, before pa and mv): isplit 119/119; under strict the
census is Fatal{ GetProgramForDraw@DrawArrays 52, GetBoundVertexArray@DrawArrays 9,
GetProgramForDispatch@DispatchCompute 7 - exactly what pa and mv owe - and Admitted{ on ten
pairs, three of them ADMITTED-ESCALATED. The expected-marker file carries all ten and says in
its header that the wave-3 merge retires three of them.
- CONTRACT-P5E §7 rewritten around what is now checkable: the derived allowlist and its three
disjuncts, the two-sided ratchet, the restated rsp pin, and the positive control.
- ID-116's static rule failed on a row no package in this phase may touch. pa retired the
per-draw program pull, which unmasked GetTransformFeedbackProgram@DrawArrays (11 entries):
BARRIER_PULLED, on kWaitNone draw_vbo, and ruled out of P5e entirely by CONTRACT-P5E section
5.7, with a retiring phase of "P3b/P4b (Espryt), P7 (Magma)". The rule was wrong, not the code.
- A pull is now admitted if the field is BARRIER_PULLED, the verb has a stamp row, the field is
in the verb class's may-read mask, and EITHER the verb's op is statically barriered OR the
field's retiring phase does not name P5e. This table is only ever consulted on a record the
server already stamped BARRIERED - CountBarrierPull's unbarriered arm is [[noreturn]] and
fires first - so the pull is legal by construction and the only open question is whose debt it
is. The retiring-phase column is exactly that answer and was already in the table.
- A verb with NO stamp row still admits nothing under either disjunct: the server stamps only
MGP_VERB_OP_LIST's ops, so any other bit would be a row nothing can exercise. 111 -> 174 pairs.
- The phase column is matched as a TOKEN, not a substring, so a "P5" or a future "P5e2" cannot
answer for "P5e".
- All six of ID-125's survivors checked: GetFramebufferBindingSlot@ReadPixels and
GetTextureUnitObject@CopyTexImage2D by disjunct 1; GetTransformFeedbackProgram@DrawArrays
(P3b/P4b), GetTextureObject@CopyImageSubData (P7) and ValidateProgramName@ShaderStorageBlockBinding
(P9) by disjunct 2. GetTextureObject@CopyImageSubData holds by BOTH, asserted separately.
GetProgramForDraw@DrawArrays, GetBoundVertexArray@DrawArrays and
GetProgramForDispatch@DispatchCompute stay rejected.
- Magma's GetFramebufferBindingSlot@Clear is NOT admitted and cannot be: the retiring-phase
column is one string for both backends and it names P5e for Espryt. The DirectVulkan entries
get their own lane label instead (item 7), which keeps the Espryt allowlist about Espryt.
- StrictErrorsAlsoPromotesTheStickyForwards is RENAMED to
StrictErrorsReachTheStickyForwardsAndNameThemByField and restated. Not one of the seven sticky
forwards names P5e (P7, P7/P9, P7/P13, P9), so on a stamped verb they are all admitted now.
The claim that still has teeth is that the detector REACHES them - asserted by rsp moving and
by the marker naming the field and its phase, so "did not abort" cannot pass for "never
detected". The Fatal half keeps its own case next door on GetBoundVertexArray, a P5e row.
- Six new derivation controls, including one that makes GetTransformFeedbackProgram this phase's
debt and requires it to leave the unbarriered verb's allowlist - without which disjunct 2
could be a constant `true` and every other control would still hold.
- ra landed a hand-kept 13-row copy of CONTRACT-P5E section 7 in .github/workflows/test.yml, and
it was wrong in BOTH directions at once: it omitted GetFramebufferBindingSlot@ReadPixels (21
entries, the lane's largest survivor) and carried GetTextureObject@CopyImageSubData, which
resource_copy_region's kWaitNone makes a genuine unbarriered read of client memory. Neither
error is visible by reading the list. Both are obvious once the list is derived.
- gen_pipe_field_ownership.py now joins the three tables ID-84 already states the rule in -
kMGPipeFieldOwnership, kMGPipeWaitClasses (generated/PipeWire.inc) and kMGPipeClassFieldMask /
kMGPipeVerbClass (generated/PipeFillPoints.inc) - and emits kMGPipeAdmittedPullMask plus
MGPipeBarrierPullAdmitted(field, verb): 101 admitted pairs across 12 barriered verbs.
- The three inputs are READ, never restated, so the allowlist moves with the code: retiring a
verb's wait class narrows it with no edit anywhere, and a new BARRIER_PULLED row widens it only
on verbs that wait. A verb with no stamp row admits nothing - only MGP_VERB_OP_LIST's ops can
ever be the record a pull is reported against.
- --print-admitted gives the CI shell the SAME table the C++ answers from, which is what makes
the hand copy unnecessary rather than merely corrected.
- Three new negative controls (each source renamed away must stop the script - a missing source
yields a silently EMPTY allowlist, which reads as rigour and is blindness) and five derivation
controls that move an input and read the allowlist back.
- Red-once: dropping the wait-class conjunct from build_admitted fails the self-test by name -
"3 admission-derivation control(s) did not hold: unbarriering ReadPixels' op removes every
@ReadPixels pair; unbarriering ResourceCopyRegion removes every @CopyImageSubData pair; no pair
is admitted on an unbarriered verb (DrawArrays)" - and the new unit case fails with
"GetBoundVertexArray@DrawArrays is admitted on a verb no barriered op stamps".
- CONTRACT-P5E §7. integration-split-strict was an EXPECTED-RED control: every entry
aborts on its first BARRIER-PULLED read, so the step gated on the run failing for
that reason. After the flip it is a HARD GREEN GATE - an unbarriered record's pull is
Fatal whatever the knob says (§3.3), so strict adds only the barriered rows, and those
are a short named allowlist of <field>@<verb>. A revert of any handle arm in
vi/sb/pg/tx2/fb then goes red HERE, by field and verb, instead of in a picture.
- THE ARM IS CHOSEN BY THE TREE. The step greps kMGPipeP5eRunAheadReady out of
MG_Backend/Init.cpp, which is the one constant the integration commit flips and is
exactly the fact the lane's expectation depends on. A lane whose flip is a separate
human step is a lane that is wrong for as long as that step is outstanding - and the
pre-flip arm is unchanged, so this commit moves no CI verdict today.
- The allowlist names the package that retires each row: readbacks and the pack PBO
(P8/P9), the CopyTex endpoint and GenerateMipmap (tx2), set_storage_block_binding
(pg), XFB (P3b/P4b/P9), resource_copy_region's GL names (the declared P5b debt). A
row leaves it by deletion, never by widening. The lane also warns on a non-zero rsp,
which after P5e can only be a barriered row.
- The allowlist is built with printf rather than a heredoc: a quoted heredoc's
terminator has to start at column 0, which inside a YAML block scalar would end the
scalar instead of the heredoc.
- ARCHITECTURE §11.6: the wait rule is the generated WaitClass column now, not the call
class, and it REPLACES the BatchWaits skip; glFinish stops being a no-op under
run-ahead; the forced waits (EGL forwarders, the map/readback family) and glGetError's
named window are listed. §11.7: the P9 plan is code - the producer blocks, the drain
clears AND rings, the client's parks break on the latch, and two named refusals remain
for the impossible cases. §14: the present credit is client-minted, 1-based, one per
swap, paid before the encode. §17.7: what P5e is, in one section - the predicate, rule
F, gPipeInputs' ownership, the latch and the control arms.
- 附 A gains MOBILEGL_IPC_RUN_AHEAD and moves MOBILEGL_IPC_PRESENT_CREDIT out of the
"P6+" line: a knob listed as not-yet-effective while it paces the frame is how an A/B
stops being one.
Layer 1 arms the texture object surfaces at MipmapStorage (the one point every concrete
texture class and view forwards through): AllocateStorage, TruncateMipmapLevels,
UpdateMipmapSubData, MapMipmapData, MarkStorageDirty(Region), IsStorageDirty,
GetStorageDirtyRegion(s) take Fatal{RoleViolation, texture-legacy-arm} from the apply
thread with an active transport. What the lane then exposed is fixed, not exempted: the
completeness gate answers from the staged store's defined-ness, the two logging reads go
through the descriptor macro, and the remint replay loop keeps its named
Fatal{UnmigratedEmulation} ahead of the guard. Magma's texture sync read path is the one
named exemption (MGPipeTextureLegacyArmScope, P7). Layer 2 wires InBarrierWait as the
client half of the gPipeInputs single-writer rule (RefusePipeInputsTouchWhileApplierOwnsIt:
applier owns it AND this thread not in a barrier wait => Fatal{RoleViolation,
gPipeInputs}; the barrier-off negative control is deferred to its own Fatal), and stale
reads of RECORD-SUPPLIED / APPLIER-DERIVED fields under a server stamp now report
RoleViolation with the field named. The RemoteGuards suite pins all nine texture surfaces
plus HasDefinedContent by name, and a negative control for the fill window. CI gains the
integration-split-strict job: unit lane as the hard gate under MOBILEGL_IPC_STRICT_ERRORS=1,
the scenario lane expected-red with the marker asserted (rewritable to plain green once P7
retires the object-class rows). rsp is per-frame in the stats line and measured on the
locally available traces (bsl in-world 948.5/frame avg over 123 frames, complementary
1591.9, iterationrp 286 until its named remint-pull Fatal); value-class pulls are zero by
rv's pinned list, what remains is the object-class fifteen.
Evidence: unit 2187/2187 (also under STRICT_ERRORS=1); integration-split 111/111; red-once
per layer (the texture guard and the gPipeInputs guard each turned their named death test
red and restored green). CONTRACT-P5C sections 5.4, 6.
- The verified-profile guard read the process environment as well as the profile: the test ran
after the source, so PROFILE_VERIFIED=1 exported in an operator's shell re-opened the fail-open
hole for every profile that says nothing. Both scripts now set PROFILE_VERIFIED=0 immediately
before sourcing, so the file is the only thing that can answer.
- A --device path that cannot be sourced was diagnosed as an unverified profile, because both
scripts cd to their own directory first and neither checked readability. The path is now also
tried relative to the directory the script was invoked from (which is what a repo-root-relative
--device means), and an unreadable one is reported as unreadable, naming both places tried.
- Verified: exported PROFILE_VERIFIED=1 + an unverified profile -> rc 2; exported 1 + a profile
with no key -> rc 2; a repo-root-relative path -> resolved, then refused for its own reason;
a missing file -> "cannot read the device profile"; odinlite.env -> past the guard;
--allow-unverified-profile -> the three warnings, then proceeds.
- test.yml's new step described a mechanism the tree does not have. G6's and G10's entries are
registered in the pull build too - they must be, for G2's name-for-name comparison - and skip
inside their bodies. The step's value is unchanged and its comment now says the true thing: the
`test` job runs those names as a column of skips, and this is the first CI job that unpacks a
build which compiled the assertions.
- trace_benchmark takes the wall baseline before the CPU baseline, the order OnFrameBoundary
already reads them in, so frame 0 stops reporting a CPU delta biased upward against its own
wall delta; and it includes <time.h> rather than <ctime> for the POSIX names it uses.
- format_benchmark printed a p50 taken with the nearest-rank rule beside a medianFrameCpuMs the
device computes as the average of the two middle frames, and documented the two as one rule; on
an even window they differ (the pre-flight printed p50=8.261ms next to medianCpuMs=271.766).
p50 now goes through series_median, which is SummarizeSeries' rule transcribed; p95 and p99 stay
nearest rank, which is the device's rule for p95 and the honest extension of it for the p99 the
device does not compute at all
- require_verified_profile treated a profile that simply omits PROFILE_VERIFIED as verified, which
is the fail-open default a profile written by copying another one inherits - exactly the case the
guard exists for. It defaults to unverified now, odinlite.env carries PROFILE_VERIFIED=1
explicitly (it is the one profile that earned it), and the refusal says "says 0, or says nothing"
- the two new profiles claimed profile.sh refuses an unverified profile; it has no such check and
needs none - it records a simpleperf profile and pins nothing. The claim is corrected in both
profiles and in the README rather than a guard added where there is nothing to guard
- the handle-ABA / CSO control step in test.yml set only MOBILEGL_ITEST_REQUIRE_GPU while its
sibling verify step sets the three MOBILEGL_MAGMA_* fixes and arms core dumps. It runs the same
DirectVulkan binary on the same runner, so a crash there left no core; it now carries both
- pipe-gates stops printing gen_pipe_dirty_surface.py --summary and runs --check && --self-test.
--check fails both directions - a scanned mutator with no row in MG_Pipe/DirtySurface.def, and
a row naming a mutator the scan no longer finds - so a deleted mutator cannot leave a stale row
behind claiming coverage. --self-test is what keeps --check honest: a completeness check that
silently stopped checking is indistinguishable from a complete mapping, so two canned negative
controls must both trip. Same shape as gen_pipe.py --self-test next to it.
- integration-verify gains `ctest -L unit`. G6's chunk-table walk and G10's residual assertions
live in MG_Test/Pipe, compiled only under MOBILEGL_PIPE_PUSH, and the `test` job builds the
PULL library - so before this those tests ran in no CI job at all. The artifact already carries
them (the packaging step tars MobileGL/MG_Test whole), so the whole cost is the run: ~14 s for
~1490 entries, measured locally on this tree.
- integration-verify also runs the two always-on negative controls by name. They are labelled
integration-gpu rather than integration-verify - they are about the handle key and the CSO
switch, not the comparator - and this is the only CI job that unpacks a push build, which
CsoContentAddressingScenario needs because both counters and the cso[] bracket are
#if MOBILEGL_PIPE_PUSH.
- build-linux-verify's arming check accepts the per-verb entry point under either of its two
names. P2 renames MGPipeFillForVerb to MGPipeValidateForVerb, and a check that named only the
old one would go red on the rename for a reason unrelated to what it tests. What it tests is
unchanged: the artifact has a per-verb entry point, and it still fails when there is none.
- 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
- 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`
- 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.
- Includes.h defines VK_USE_PLATFORM_XLIB_KHR before vulkan.h, so the clang-mode probes and
the negative controls need X11/Xlib.h on the runner; run 34008829271 failed on exactly that