[Docs] (.github/workflows, docs/Disaggregated, MG_IntegrationTest): shorten the long comments in the integration lanes and test workflow

This commit is contained in:
2026-10-15 23:16:45 -04:00
parent 1161d489e2
commit 7bb5a9ccbe
4 changed files with 455 additions and 1757 deletions
+101 -377
View File
@@ -780,78 +780,22 @@ jobs:
ctest --output-on-failure -L integration-verify --no-tests=error --output-junit "${RUNNER_TEMP}/verify.xml"
fi
# The push-only unit tests, on the verify runtime.
#
# WHY HERE AND NOT IN `test`. The entries themselves are registered in EVERY build - they
# have to be, or `ctest -N` would stop matching name-for-name between the pull and the push
# build (gate G2). What is push-only is what they assert about: MGPipeRenderStateSpans.cpp
# and PipeApply.cpp are appended to SOURCE_FILES inside the `if (MOBILEGL_PIPE_PUSH)` block,
# which is exactly how the pull build stays symbol-identical, so in a pull build each case
# opens with `#if !MOBILEGL_PIPE_PUSH GTEST_SKIP() << "push not compiled in"`. The `test`
# job therefore runs G6's chunk-table walk and G10's residual assertions as a column of
# skips: CI executes the NAMES and never one of the assertions. This job unpacks a build
# that compiled them, so it is the first place in CI where they actually run.
#
# This artifact already carries them - the packaging step above tars
# ${BUILD_DIR}/MobileGL/MG_Test whole - so the whole cost is the run, which is ~14 s for
# ~1490 entries. --no-tests=error, because a packaging change that stopped shipping the
# unit binaries would otherwise report a green run of nothing.
# The unit suite again, on the verify library: cases compiled only under MOBILEGL_PIPE_VERIFY
# skip in the `test` job's build and assert only here, as do poison-gated cases on branches
# where that build is a monolith. The packaging step above ships MG_Test whole, so the cost
# is the run itself. --no-tests=error, so a packaging change that dropped the unit binaries
# fails here instead of passing with nothing run.
- name: Unit tests on the verify runtime (G6, G10)
working-directory: build-verify
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-verify.xml"
# The always-on P2 and P3a negative controls (G8, G10, G12), which are labelled
# integration-gpu and not integration-verify - they are about the handle key, the subsystem
# switch and the map-persistent counter, not about the comparator - so the lane above does
# not reach them. They are run HERE because this is the only CI job that unpacks a
# MOBILEGL_PIPE_PUSH build: every one of them reads a counter out of the library's summary
# line, and both the counters and their brackets are #if MOBILEGL_PIPE_PUSH, so in the pull
# `integration` job the entries exist (gate G2 requires the same names in both builds) but
# have nothing to assert.
#
# An arm whose subsystem has not landed on this tree SKIPS with the reason (never absent,
# never a green that asserted nothing), so this step is green through the P2 and P3a landing
# orders and starts asserting as each package arrives.
#
# The environment is the sibling step's, deliberately and in full: these entries run the
# same DirectVulkan binary through the same runner, so the three MOBILEGL_MAGMA_* fixes it
# needs apply here too, and a crash here has to leave a core for the same black-box flow.
# The step above is the only reason those lines exist in this job; a control that crashed
# without one would be the hardest failure in the job to diagnose.
#
# THE -R ALTERNATIVES ARE TEST-NAME PREFIXES, NOT LANE LABELS, and each one is deliberately
# the SHORTEST string that still selects only what it means to. `ResourceSubsystem` (not
# `ResourceSubsystemControl`) is what reaches the eight
# DirectGLES.ResourceSubsystemOn./Off.LargeArenaAdoptionScenario.* entries - the A/B lanes
# whose entire purpose is that the handle path and the legacy BufferBackendOps path must
# agree about an adopted store - as well as the two ResourceSubsystemControl. entries.
# `MapPersistentRoundtrip` is singular because LargeArenaAdoptionScenario's case is
# `AnAdoptionCostsExactlyOneMapPersistentRoundtrip`; the plural matched only the lane PREFIX
# of the other one. Both mistakes were silent: this is the only CI job that unpacks a push
# build, so an entry the filter misses is either never run under the P3a bits at all or runs
# only in the pull `integration` job, where a MOBILEGL_PIPE_PUSH value steers nothing
# (Config.h declares the field inside the push guard) and both arms are the same legacy path.
# A lane that cannot go red where it is installed is not a gate (ARCHITECTURE.md §13.2).
# P4a ADDS THREE ALTERNATIVES, and each one is here because this is the only CI job that
# unpacks a push build:
# * `ObjectSubsystem` reaches the three DirectGLES.ObjectSubsystemControl. entries - the
# 0x1fff-vs-0x1ff A/B and the 0x9ff dependency refusal (G12). `ResourceSubsystem` does
# NOT match it: the two families are named apart on purpose, because they are different
# phases' switches and a filter that merged them would hide one behind the other.
# * `TextureParamsWithoutASamplerView` reaches G9's four cases, the scenario the phase's acceptance gate
# names by hand. It runs in the ambient lanes, which the label already selects - but this
# step is where those cases run against a PUSH library, and G9's whole claim is about the
# push path. All four cases are green on the contract commit - including the one D-E3
# expected to be red, for the reason the scenario's header records - so this row is green
# from the day it lands and goes red only if a reachability path stops syncing texture
# parameters at all, which is the coupling ARCHITECTURE.md §3.2 exists to remove.
# * `TextureUploadShape` is RECORDED, not gated (D-D4): it asserts that the two upload-shape
# counters could be read and that they agree, and prints the shape for MEASUREMENTS.md. It
# is in the filter so that the number is actually collected on every run - an unmeasured
# shape is not a recorded one - and because its own assertions can go red.
# As with the four before them, each alternative is the SHORTEST string that selects only what
# it means to.
# Handle-ABA, CSO, subsystem and texture-parameter controls (G8, G8b, G9, G10, G12).
# Handle-ABA, CSO, subsystem-mask and texture-parameter controls, rerun on the verify library.
# They carry integration-gpu, not integration-verify, so the verify lane misses them.
# Each -R alternative is a test-name substring, the shortest that selects only its family:
# `ResourceSubsystem` also reaches the LargeArenaAdoption lanes and the mask refusals,
# `MapPersistentRoundtrip` is singular so it matches the case name as well as the lane prefix,
# and `ObjectSubsystem` and `ResourceSubsystem` stay distinct families. TextureUploadShape
# records the upload shape; it fails only when its two counters are unreadable or disagree.
- name: Push-build controls (G8, G8b, G9, G10, G12)
working-directory: build-verify
env:
@@ -1143,34 +1087,14 @@ jobs:
fi
echo "the corrupted field turned ${matched} selected split entries red, as it must"
# NEGATIVE CONTROL A ON THE OTHER COMPARATOR (P7 wave 3, V1). The step above proves the
# ENTRY compare - client thread, verb boundary, `where=entry`. The compare-at-read hook runs
# on the SERVER's apply thread at every backend read and is the only comparator that arm ever
# runs, and until MG_Impl/Pipe/PipeFill.cpp's hook honoured MOBILEGL_PIPE_VERIFY_CORRUPT it
# had no control at all: a hook that had stopped comparing was indistinguishable from a hook
# with nothing to report.
#
# NOT THE "ctest MUST FAIL" SHAPE, and the reason is mechanical rather than a preference:
# GetPixelStoreParameters is in kReadback's fill mask, so with MOBILEGL_PIPE_VERIFY_FATAL at
# its default the client's entry compare aborts the process at the ReadPixels boundary and
# the server never reaches the read. The entries therefore pin FATAL=0, survive their own
# divergence, and each asserts from inside the process that the SERVER role's half of its
# private log carries the report. This step re-reads those logs from outside, so the claim
# does not rest only on the assertion that produced them.
#
# AND IT COUNTS, NEEDING TWO (V1 fix round 2). The server reads the pack field twice inside
# the ReadPixels window - PipeApplier::read_pixels saves the application's pack before it
# installs the neutral one, the backend's ReadPixels reads it after - and the hook perturbs
# its oracle once per read, because the ID-49 overwrite inside the window wipes the first
# perturbation before the second read's compare. A step satisfied by ONE line was satisfied
# by either perturbation alone and falsified neither. Measured at landing: 3 (DirectGLES,
# whose backend reads the field twice after the install) / 2 (DirectVulkan); without the
# re-applied perturbation both drop to 1, without the first one DirectVulkan drops to 1
# while DirectGLES stays at 2 - so BOTH backends are required, and the first perturbation's
# falsifier is the DirectVulkan entry.
#
# THE GREEN HALF is the lane run above: the same hook, the same window, with no corruption
# armed, across 1070 entries at zero divergences.
# Negative control for the compare-at-read hook, which the server's apply thread runs at each
# backend read; the step above covers only the client's entry compare. The entries pin
# MOBILEGL_PIPE_VERIFY_FATAL=0, or the client's entry compare would abort at the ReadPixels
# boundary before the server reads. This step re-reads each server log and needs two
# `where=read` reports per backend: the server reads the pack field twice in the ReadPixels
# window (the saved pack, then the backend's read under the neutral pack), and each read must
# catch its own perturbation. PipeVerifyArmingScenario.CorruptedFieldIsReportedOnTheServerRead
# has the details, including why both backends are needed.
- name: Negative control A (server-side read)
working-directory: build-verify-split
env:
@@ -1408,26 +1332,13 @@ jobs:
fi
echo "libMobileGL.so defines ${remote} MG_Remote symbol(s) and the MGPipe applier (${defined} defined symbols)"
# The link-closure ratchet (CONTRACT-P6.md 12.1, ID-P7-7): the set of frontend symbols a
# server-only image would still have to link, recomputed from the per-object partition in
# scripts/link_ratchet.py and compared against the stored LIST.
#
# THIS job and not `build-linux`: build-linux passes the same three options from
# ${MOBILEGL_CI_DISAGGREGATED}, which is ON only on feat/disaggregated, cleanup and
# codex/fix-github-ci, so on every other branch it builds a monolith - MG_Remote would not
# be compiled at all, the SERVER half of the partition would be empty, and the gate would
# go green having measured nothing. This job turns the three options on unconditionally,
# and the nm step immediately above has just proved they took. The script re-reads them
# from CMakeCache.txt anyway and refuses the run if they are not all ON.
#
# Only NEW symbols fail. A symbol that has DISAPPEARED is the ratchet turning and is
# printed with an instruction to re-baseline in the same commit - the baseline stores the
# list rather than a count precisely so that clearing one symbol while adding another does
# not walk through at an unchanged total (ID-P7-7).
#
# It reads the per-object output of the MobileGL target, so it must run before anything
# that cleans the build tree; the packaging step below excludes *.o from the tarball but
# does not delete them.
# Server link-closure ratchet (ARCHITECTURE.md §13.2): scripts/link_ratchet.py recomputes
# from the per-object partition the frontend symbols a server-only image still has to link
# and compares the list with the stored baseline. Only new symbols fail; a vanished one is
# printed with a request to re-baseline in the same commit. The baseline is a list, not a
# count, so swapping one symbol for another fails. It runs here, not in build-linux, because
# this job always builds MG_Remote (the script re-checks CMakeCache.txt), and before anything
# deletes the *.o files it reads.
- name: Server link-closure ratchet
run: |
mkdir -p ci-artifacts
@@ -1567,23 +1478,11 @@ jobs:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml"
# P7 wave 2-F, PH-7 (1). THE TCP SUPERVISOR LANE NEEDS flatc, AND CI HAD NONE.
#
# `TcpLane.SupervisorProtocolControls` carries LABELS integration-tcp, so it has been in the
# selection below since wave 0 - as a SKIP, every run. The script builds the protocol's
# Python bindings with flatc, flatc is deliberately absent from the build graph
# (gen_protocol.py's header says why), and with none found it exits 77 and ctest reports
# the entry skipped. Skipped is what the entry looked like for the whole time it did not
# exist, which is the failure mode the plan's own row named ("referenced by NOTHING").
#
# So the job builds the SAME pinned flatc that flatc-check builds, from the same submodule,
# and the lane step below names the entry to junit_tally.py --require-entry-passed. If the
# build ever stops producing one, the lane reds by name rather than going green having run
# the supervisor's Busy / token / listen / version / build-policy / reap controls zero times.
# --check is passed so this step cannot quietly regenerate a different committed header;
# keeping that honest is flatc-check's job and this step must not overlap with it.
# P7 F2: `TcpLane.ControlFrameFuzz` (Ph fuzz arm 1, scripts/ci/ph_fuzz_control_frames.py)
# needs the same flatc and is required PASSED by name on the same junit_tally line.
# TcpLane.SupervisorProtocolControls and TcpLane.ControlFrameFuzz build the protocol's Python
# bindings with flatc, and without one they exit 77 and ctest reports a skip. flatc is not in
# the build graph (see gen_protocol.py), so this builds the same pinned flatc as flatc-check,
# and the lane step requires both entries PASSED by name through junit_tally.py. --check keeps
# this step from regenerating the committed header; checking that header is flatc-check's job.
- name: Check out FlatBuffers (TCP lane)
run: git submodule update --init 3rdparty/flatbuffers
@@ -1692,36 +1591,12 @@ jobs:
fi
echo "the split lane resolved MOBILEGL_TRANSPORT=inproc"
# EXIT GATE E1's NEGATIVE CONTROL and EXIT GATE E3(a)'s, in one step because they have the
# same two-state shape and the same reason for it.
#
# Both controls turn a knob that MUST make a split scenario red: MOBILEGL_IPC_VERB_BARRIER=0
# removes the lockstep fence R-1 rests on, and MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 turns the
# persistent-map push off. A control of the verify lane's shape - "this step passes when
# ctest FAILS" - cannot be written yet, because while packages c1/s1/v1 are landing the
# Split entries SKIP and ctest reports green whatever the knob says, so an unconditional
# control would be red for the whole of P5 for a reason that is not a defect.
#
# So the expected state is DERIVED FROM BEHAVIOUR rather than assumed. The first version read
# MGITEST_REMOTE_CLIENT_PRESENT out of the generated *_tests.cmake, which was a restatement of
# the CMake source probe review finding M-1 falsified; the arming condition is a runtime fact
# inside each test process (MG_Config::Transport, ClientSession::Active() and
# ImplementedVerbCount(), read by Harness/SplitRuntimePeek), so the only honest way to ask it
# from a shell is to look at what the entries DID. When entries passed, the controls MUST
# fire; when every one of them skipped, the step says so loudly and does not pretend.
#
# THE BODY OF THIS STEP IS scripts/ci/split_negative_controls.sh, and the move is the point
# rather than tidiness. A `run:` block executes nowhere but on a runner, so these lines were
# unreviewable and untestable: when the wave-1 cross-family review said they were broken,
# CONFIRMING it needed a hand-made copy of them (wave1-codex-verify.md 8), and a copy is not
# the thing. scripts/ci/control_smoke_test.sh now drives the very file this step runs.
#
# What that smoke test pins, and what ID-46 finding 8 found missing: each control asserts its
# OWN failure reason. A non-zero ctest exit used to be enough, so a timeout, a setup abort or
# any unrelated assertion printed "turned N selected entries red, as it must" and this step
# went green. The arming run's `|| true` had the matching defect - it counted a case that ran
# and FAILED as evidence the lane was live, so the controls could be measured against a
# baseline that was already red.
# Negative controls for the verb barrier (MOBILEGL_IPC_VERB_BARRIER=0) and the persistent-map
# push (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0): each knob must turn its selected entries red.
# The body is scripts/ci/split_negative_controls.sh, so scripts/ci/control_smoke_test.sh can
# run the same lines off a runner. It needs a green baseline with at least one passed entry,
# and accepts a red only when it carries the control's own diagnostic, so a timeout, a setup
# abort or an unrelated failure never counts.
- name: Negative controls (barrier, map push)
if: ${{ !cancelled() }}
working-directory: build-split
@@ -1821,23 +1696,14 @@ jobs:
MOBILEGL_IPC_STRICT_ERRORS: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-strict.xml"
# ---- P5e (ra), docs/archive/contracts/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
# ---- Strict split lane: residual pulls under MOBILEGL_IPC_STRICT_ERRORS=1 ----
#
# This lane was written as an EXPECTED-RED control: under strict every integration-split
# entry aborts on its first BARRIER-PULLED read, so the step gated on the run failing and
# on the strict marker being the reason. P5e turns it into a HARD GREEN GATE - once the
# client runs ahead, an unbarriered record's pull is a Fatal whatever this knob says
# (§3.3), so what strict adds is only the BARRIERED rows' pulls, and those are a short,
# named list. 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 nobody diffs.
#
# THE ARM IS CHOSEN BY THE TREE, NOT BY A SECOND EDIT. kMGPipeP5eRunAheadReady in
# MG_Backend/Init.cpp is the one constant the integration commit flips, and it is exactly
# the fact this lane's expectation depends on: with it false no server publishes
# kCapRunAheadApply, every record is barriered, every scenario still aborts on its first
# pull and the expected-red control is the honest gate. Reading it here means the lane
# changes meaning in the same commit the runtime does - a lane whose flip is a separate
# human step is a lane that is wrong for as long as that step is outstanding.
# An unbarriered pull is always fatal, and strict also stops any barriered pull that is not
# admitted (ARCHITECTURE.md §9.2.1). The arm comes from kMGPipeP5eRunAheadReady in
# MG_Backend/Init.cpp, so the expectation changes in the commit that changes the runtime. When
# true (run-ahead), the lane must be green and its markers must match the admitted list and
# strict-expected-markers.txt; when false, every record is barriered, the lane must be red
# (strict aborts every drawing entry), and only the marker census is checked.
- name: Strict split lane
working-directory: build-split
env:
@@ -1912,64 +1778,23 @@ jobs:
exit 1
fi
# THE ADMITTED MARKERS COME FROM THE GENERATOR, NOT FROM THIS FILE (ID-116).
#
# A hand-kept copy of §7 lived here for one round and was wrong in BOTH directions at
# once: it omitted GetFramebufferBindingSlot@ReadPixels - 21 entries, the largest
# survivor in the lane - and it carried GetTextureObject@CopyImageSubData, which
# nothing made safe, because resource_copy_region is kWaitNone. Neither error is
# visible by reading the list; both are obvious the moment the list is DERIVED.
#
# The rule is ID-84 said in tables that already exist (ID-116, refined by ID-125): a
# <field>@<verb> pull is admitted iff the field's ownership row is BARRIER_PULLED, the
# verb has a stamp row, the field is inside the verb class's may-read mask, and EITHER
# the verb's wire op is statically barriered (kMGPipeWaitClasses != kWaitNone, i.e. the
# client really is parked behind the record) OR the field's retiring phase does not
# name P5e. The second disjunct is the honest reading of ID-84: this comparison only
# ever sees pulls on records the server stamped BARRIERED - an unbarriered one aborts
# before any of this - so the pull is legal by construction and the only question left
# is whether THIS phase still owes the migration. Without it the lane would go red on
# GetTransformFeedbackProgram@DrawArrays, a row CONTRACT-P5E §5.7 rules out of P5e
# entirely and no package here is allowed to touch.
# `--print-admitted` prints exactly the table MGPipeBarrierPullAdmitted() answers
# from, so this shell and the C++ cannot drift: retiring a verb's wait class narrows
# the lane with no edit here, and a new BARRIER_PULLED row widens it only on verbs
# that wait.
# THE RATCHET, BOTH SIDES (ID-119). `no-fatal` is what makes this arm the hard gate:
# - any Fatal marker fails;
# - an ADMITTED marker outside the generated allowlist fails, which can only mean the
# committed PipeFieldOwnership.inc and the generator disagree;
# - a pair that appears and is not in strict-expected-markers.txt fails: it is a debt
# somebody now owes and it gets written down with the phase that retires it;
# - AND a pair in that file that no longer appears fails, so the lane CANNOT ROT
# GREEN. Without that half, a retired debt leaves its row behind as a permanent
# licence and the next regression to re-introduce the read reads as "expected".
# ADMITTED-ESCALATED markers are checked against the expected set but NOT against the
# allowlist: they are a runtime fact about the record's payload (an open XFB span, a
# draw carrying client vertex arrays) that no table indexed by (field, verb) can carry,
# and widening the static list to hold them would forgive the ordinary draw path too.
# The admitted pairs come from gen_pipe_field_ownership.py --print-admitted, the table
# MGPipeBarrierPullAdmitted() reads, so this list and the C++ cannot drift. A pair is
# admitted when its verb has a stamp row, the field is BARRIER_PULLED and in the verb's
# may-read mask, and either every wire op stamping the verb waits or the field's retiring
# phase is not the generator's THIS_PHASE. The census fails on an admitted marker outside
# that list and on any difference from strict-expected-markers.txt in either direction;
# `no-fatal` adds any Fatal marker. Escalated markers depend on the record's payload (an
# open XFB span, client vertex arrays), so they are checked only against the expected set.
python3 "${helper}" markers "${RUNNER_TEMP}/lane.json" '.' \
"${RUNNER_TEMP}/allowed.txt" "${expected}" no-fatal
# `rsp`, AND THE PIN THAT REPLACED A VACUOUS ONE (ID-119). What stood here was a grep
# for `rsp=` over split-logs/ - a counter no entry in that directory emits, because
# PipeStats is off unless a lane asks for it - testing a sentence that was true of every
# possible implementation: CountBarrierPull's unbarriered arm is [[noreturn]] and runs
# BEFORE ++g_residualPulls, so an unbarriered pull can never reach the counter whatever
# the code does. It has been replaced by an assertion inside a lane entry that actually
# counts: DirectGLES.Split.StrictArming., which runs 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, since the library opens that path "w"), and asserts
# vbs > 0 - the server stamped a verb boundary on a DRAWING inproc entry, which is
# ID-115's positive control: everything strict checks is downstream of
# that stamp, and before it existed "the lane is green" and "strict was
# never armed" were the same observation;
# rsp == 0 or rsp >= draws - the checkable shape of the debt. While the draw path's
# pull is unretired every draw pulls, so rsp scales with draws; the
# retirement is visible as rsp ceasing to scale. That is what the device
# measured (rsp ~= the draw count under inproc, 0 under monolith).
# So the assertion lives where the counter is, and this step only has to insist the
# entry RAN - a positive control that can be skipped away is not one.
# Arming proof: DirectGLES.Split.StrictArming. runs with MOBILEGL_PIPE_STATS=1 and a
# private log and asserts, over one drawing frame, that the server stamped a verb
# boundary (vbs > 0) and that no residual pull happened (rsp == 0). Only that entry
# enables PipeStats, so the counters are checked there, not grepped from these logs; rsp
# counts only barriered pulls, as an unbarriered one aborts before CountBarrierPull.
# This step only requires that the entry ran: a skipped positive control proves nothing.
if ! grep -q 'DirectGLES\.Split\.StrictArming\.' "${RUNNER_TEMP}/strict-split.txt"; then
echo "::error::the strict lane did not run DirectGLES.Split.StrictArming., so nothing in this run proves the server ever stamped a verb boundary. A green lane without it is indistinguishable from a lane where strict was never armed (ID-115)."
exit 1
@@ -2091,26 +1916,12 @@ jobs:
run: |
ctest -L integration-magma-split --no-tests=error -j 2 --output-on-failure
# ---- P7 PACKAGE L: THE SAME MAGMA CASES IN TWO PROCESSES -----------------------------
#
# EXIT GATE 1's FIRST HALF, and until this package there was nothing to run: every arm of
# the split registration macro hardcoded MOBILEGL_BACKEND_TYPE=DirectGLES, so the shared
# `integration-split` / `-spawn` / `-tcp` labels held ZERO DirectVulkan entries and
# `integration-magma-split` was inproc by construction. These two lanes are the same
# scenarios the step above runs, with the transport as their one difference - so a red
# here that the step above did not see is a TWO-PROCESS fact and not a Magma one.
#
# THEY ARE GATED BECAUSE THEY MEASURED GREEN, and only for that reason: 52 of 52 entries
# on each arm, with the two ClipDistance cases skipping for the reason the inproc arm's
# allowlist already names. The 21 entries the inproc arm has and these do not are
# MagmaRunAheadScenario and MagmaWireCacheScenario, which are inproc-only BY
# CONSTRUCTION (they hold and peek THIS process's server loop); the CMakeLists and
# spawn_lane_parity.py both name them rather than widening a tolerance.
#
# THE INFORMATIONAL TIER - `integration-magma-all-{split,spawn,tcp}`, every scenario the
# split arms run, on Magma - IS DELIBERATELY NOT HERE. Its job is to be able to go red
# while CI stays green; a case moves into a gating step with the commit that retires its
# blocker.
# The Magma scenarios of the step above over the spawn and TCP transports, so a failure here
# that the inproc step did not show is a two-process fact, not a Magma one. Entries that can
# run only inproc (such as those that peek this process's server loop) are listed in
# spawn_lane_parity.py's MAGMA_INPROC_ONLY. The informational tier,
# integration-magma-all-{split,spawn,tcp}, is not gated: it may go red while CI stays green,
# and a case moves into a gating step once its blocker is fixed.
- name: Magma spawn and TCP lanes
working-directory: build-split
env:
@@ -3190,29 +3001,13 @@ jobs:
build-retrace/tools/trace_replay/${{ matrix.case }}/${{ matrix.backend }}/output/**
if-no-files-found: warn
# The retrace arm of the split shape: retrace-verify with MOBILEGL_PIPE_VERIFY=1 swapped for
# MOBILEGL_TRANSPORT and the comparator's symbol swapped for an MG_Remote one. Exit gate E2 is
# one row - OpenRA at SSIM >= 0.99 - but the lane is no longer one row: as of P6 it is
# {backend, case, transport} over the whole CI matrix, both backends, inproc AND spawn.
#
# inproc IS THE CONTROL AND SPAWN IS THE CLAIM. 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 turned out to hinge on.
#
# IT RUNS THE UNCHANGED CTEST NAMES with the transport exported in the JOB environment, rather
# than the SPLIT-suffixed variant entries. build-retrace configures without
# -DMOBILEGL_BUILD_DISAGGREGATED, so the variant entries are deliberately not registered there
# (a name that says SPLIT in a build that cannot be one is worse than no name), and the
# unchanged names are what retrace-verify already proves this shape works with. The variant
# entries exist for a build that DOES configure the option - a local build-split with trace
# replay on - where `ctest -L retrace-split` is self-describing and needs no environment ritual.
#
# WHAT MAKES IT FALSIFIABLE is not the SSIM. A monolith run of OpenRA also scores 1.000: measured
# on this branch, a pull library under MOBILEGL_TRANSPORT=inproc produced ssim=1.0 and was caught
# only by run_trace_case.cmake's transport-resolution assertion. So there are two guards, and the
# SSIM is neither of them: the nm check below (this library carries MG_Remote) and the
# transport-resolution line in the library's own log (this PROCESS resolved inproc).
# Trace retrace on the split library: the retrace-verify shape with MOBILEGL_TRANSPORT set
# instead of MOBILEGL_PIPE_VERIFY, over {backend, case, transport} with inproc and spawn.
# inproc is the control and spawn the claim: same library and picture, only the server role's
# process differs, so a spawn-only failure is a transport fact. It runs the unchanged ctest
# names with MOBILEGL_TRANSPORT exported around ctest, since build-retrace does not register the
# split variant entries. SSIM cannot catch a monolith run (OpenRA scores 1.000 either way); the
# guards are the nm check for MG_Remote and run_trace_case.cmake's transport-resolution check.
retrace-split:
name: retrace split (${{ matrix.backend }} / ${{ matrix.transport }}, ${{ matrix.case }})
runs-on: ubuntu-latest
@@ -3400,39 +3195,13 @@ jobs:
ctest -V --no-tests=error --timeout 10800 \
-R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
# THE RETRACE LANE'S OWN ALWAYS-ON NEGATIVE CONTROL, and its absence was review finding M-6:
# this job was a clone of retrace-verify with the one step removed that makes the lane mean
# anything. retrace-verify's "Negative control (corrupted snapshot)" step requires a corrupted
# snapshot field to red the retrace, so that "79 traces, zero divergences" is not a statement
# about a comparator nobody watched; the same reasoning applies here word for word.
#
# The control swaps the PULL library into the frozen path and requires the SAME replay to
# fail. It is the sharpest one available, because OpenRA scores ssim 1.000000 either way -
# measured - so this step fails if and only if run_trace_case.cmake's transport-resolution
# assertion has stopped working, which is the single thing standing between this job and a
# green that ran monolith end to end.
#
# NOT the control BRIEF 7 E2 names ("patch the Clear emitter to drop one emission; SSIM must
# fall below threshold"). That one needs an emitter, i.e. package c1, and it is carried as an
# explicit debt in t1-v1.md rather than silently substituted - which is what the first
# version of this package did.
#
# The rerun replays into the same case directory, so the good run's images are put aside and
# restored whichever way the control goes; "Upload actual image" below runs `if: always()`
# and would otherwise ship the deliberately-wrong run's output under the good run's name.
# THE BODY OF THIS STEP IS scripts/ci/retrace_transport_control.sh, for the reason the
# split lane's control gives: a `run:` block cannot be executed off a runner, so these lines
# could not be tested until they ran in CI. scripts/ci/control_smoke_test.sh drives that file.
#
# Two holes ID-46 finding 8(b) found in this block, both CONFIRMED against the REAL ctest in a
# REAL build tree, both closed in the script: it had NO selection guard at all - unlike the
# split lane's run_control - so a case/backend regex matching nothing exited 8 through
# `--no-tests=error` and was read as "the pull library turned it red"; and only "non-zero
# ctest" was checked after the nm identity check, so a loader failure, a missing fixture or a
# timeout passed it. The red must now carry run_trace_case.cmake's own sentence.
# P13 W2: THE LIBRARY IS THE PUSH MONOLITH WITHOUT MG_Remote (the shape FCL embeds); until W2 it
# was the pull build, run side by side with this one for a wave (W1) to show both red here for
# the same sentence. What makes the library red is that it has no transport, not that it pulls.
# Negative control: copy a library without MG_Remote (the push monolith FCL embeds) over the
# frozen path and require the same replay to fail. OpenRA scores ssim 1.000 either way, so
# the picture cannot catch the swap; this proves that run_trace_case.cmake's
# transport-resolution check still does. The body is scripts/ci/retrace_transport_control.sh,
# which control_smoke_test.sh runs off a runner. It counts the selection first, requires the
# red to carry run_trace_case.cmake's own sentence, and restores the split library and the
# good run's images, since "Upload actual image" runs `if: always()`.
- name: Negative control (library without a transport)
working-directory: build-retrace/tools/trace_replay
env:
@@ -3447,23 +3216,12 @@ jobs:
bash "${GITHUB_WORKSPACE}/scripts/ci/retrace_transport_control.sh"
'${{ matrix.case }}' '${{ matrix.backend }}'
# EXIT GATE E2's OTHER HALF, and the one BRIEF-P5 §7 E2 actually names: "drop an emission
# and the SSIM must fall below the threshold". The control above proves the lane runs a
# SPLIT library; it cannot prove the PICTURE came through the wire, because OpenRA scores
# ssim 1.000000 against a monolith library too and the transport assertion is what reds it.
#
# It drops the DRAWS and not the clears, and that is a measurement rather than a
# preference: with MOBILEGL_IPC_E2_DROP_CLEAR=1 armed, read, and all 29 of OpenRA's Clear
# records dropped, the retrace still scored ssim=1.000000 / mismatchPixels=0 - OpenRA
# overdraws every pixel it clears before the snapshot (joint-v1.md §3 found the green;
# scripts/ci/retrace_drop_draw_control.sh's header has the trace census that explains it).
# With the DrawVbo records dropped instead: 758 dropped, ssim=0.000036,
# mismatchPixels=295296.
#
# THE BODY IS A SCRIPT for the reason the two steps above give: a `run:` block cannot be
# executed off a runner, and scripts/ci/control_smoke_test.sh drives this file against a
# stubbed ctest in seven modes - including the two that matter most, a red whose SSIM never
# fell and a knob that armed and dropped nothing.
# Negative control for the picture: the step above cannot show that the image came through
# the wire, because OpenRA also scores ssim 1.000 on a monolith library. This drops the
# DrawVbo records and requires the SSIM to fall. Dropping clears does not work: OpenRA
# overdraws every pixel it clears (retrace_drop_draw_control.sh's header has the census).
# The body is a script so control_smoke_test.sh can run it against a stubbed ctest, including
# a red whose SSIM never fell and a knob that armed but dropped nothing.
- name: Negative control (dropped draws)
working-directory: build-retrace/tools/trace_replay
env:
@@ -3682,66 +3440,32 @@ jobs:
fi
echo "no fprintf(stderr/stdout / printf( / puts( / std::cout|cerr under MobileGL/MG_Backend or MobileGL/MG_State"
# A GATE as of P2, which is when MG_Pipe/DirtySurface.def exists to diff the scan against
# (the first mapping round was part of that phase's deliverable). --check fails BOTH directions: a
# mutator the scanner finds with no row in the 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 the half that keeps --check honest, and it is not optional. A completeness
# check that silently stopped checking produces exactly the same green as a complete
# mapping; the self-test feeds it two canned negative controls (a mutator withheld from the
# def, a row naming a function that does not exist) and fails if either fails to trip. Same
# shape as gen_pipe.py --self-test and check_include_closure.py above.
#
# What this gate does NOT cover is written into DirtySurface.def's header rather than left
# implicit: the scanner attributes a mutation inside a lambda to the enclosing function,
# reads a mutation published through a helper as deferred, and scans only MG_Impl/GLImpl -
# so the four MGP_NOTE_MUTATION sites in MG_State are outside it entirely. This is a
# completeness gate over what the scanner can see; the semantic proof is the verify lane.
# Dirty-surface completeness (ARCHITECTURE.md §5.2.1). --check fails in both directions: a
# mutator the scanner finds with no row in MG_Pipe/DirtySurface.def, and a row naming a
# mutator the scan no longer finds. --self-test feeds canned negative controls and fails if
# one does not trip, so a check that stopped checking cannot stay green. The scanner matches
# text; the semantic proof is the verify lane.
- name: Dirty-surface mapping complete (G9)
run: |
python3 scripts/gen_pipe_dirty_surface.py --check
python3 scripts/gen_pipe_dirty_surface.py --self-test
# A GATE as of P5 (R-7.1, CONTRACT-P5.md table 2). The eighth generator, and the only one
# whose output the step above cannot cover: `gen_pipe.py` does not write
# PipeFieldOwnership.inc, so regenerating the seven and diffing MG_Pipe/generated leaves a
# drifted ownership table green. The build-level static_assert only catches an
# UNCLASSIFIED row; a hand edit that CHANGES a class compiles clean, and --check is the
# only thing in the tree that catches it.
#
# NO BRANCH GUARD, deliberately, unlike the two G5 steps below: this asks "does the
# committed table still follow from the .def files", which is a question every branch can
# answer and every branch wants answered. The table outlives feat/disaggregated.
#
# --self-test is not optional, for the reason the two gates above give and for one this
# generator learned the hard way: its first version counted eleven trips when one control
# was a silent duplicate of another, because the harness asked "did something exit" rather
# than "did THIS exit". Every control now asserts its own message, and a harness control
# asserts that the harness still rejects someone else's exit.
# PipeFieldOwnership.inc is written by gen_pipe_field_ownership.py, not gen_pipe.py, so the
# gen_pipe.py regenerate-and-diff step above does not cover it. The build's static_assert
# catches only an unclassified row; --check also catches a changed class. No branch guard:
# every branch needs the committed table to follow from the .def files. Each --self-test
# control asserts its own message, and a harness control checks that one control's exit is
# never credited to another.
- name: Field-ownership table complete (R-7.1)
run: |
python3 scripts/gen_pipe_field_ownership.py --check
python3 scripts/gen_pipe_field_ownership.py --self-test
# R-16 APPLIED TO THE NEGATIVE CONTROLS THEMSELVES. The split lane's E1/E3(a) controls and the
# retrace lane's pull-library control are gates, and until ID-46 finding 8 neither could be
# made red by anyone: their bodies were `run:` blocks, which execute only on a runner. Both
# bodies now live in scripts/ci/, and this step runs them against a stubbed ctest that
# reproduces the finding - a NON-EMPTY selection failing with UNRELATED_CONTROL_FAILURE, and a
# case/backend regex matching no tests - and requires each control to report FAILED. The same
# stub, failing with the diagnostics the scenarios really emit, must make them report PASSED.
#
# NO BRANCH GUARD: this asks "do the negative controls still reject a red that is not theirs",
# which is a question every branch can answer and none of which depends on the TEMPORARY
# feat/disaggregated trigger at the top of this file. It costs a couple of seconds and needs
# no build.
# P7 adds the last two. THE DEVICE MATRIX DRIVER HAD NO CI STEP AT ALL: its 12 stub
# regressions (now 18, with the DirectVulkan half) were run by hand in WSL and recorded in
# a note, which is the same "a gate nobody can make red" shape the paragraph above is
# about - and it guards the tool that decides whether a device golden run PASSED.
# compare_actuals is the new half of that evidence (actual-vs-actual SSIM between
# repeats); both are stub/synthetic-only, need no build, no GPU and no device.
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2).
# control_smoke_test.sh runs the split and retrace control scripts against a stubbed ctest:
# an unrelated failure or an empty selection must make each report FAILED, and the
# scenarios' real diagnostics must make it report PASSED. The Python tests run the trace and
# TCP lane tooling on stubs and synthetic data. No branch guard; no build, GPU or device.
- name: Control and trace-tool smoke tests (R-16)
run: |
bash scripts/ci/control_smoke_test.sh
File diff suppressed because it is too large Load Diff
+6
View File
@@ -107,6 +107,12 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与
- `RegionCount == 0` 只表示整 level 写;其余写入一律带盒子(只有一个也带),所以 applier 的累积不会因为某一侧「只有 box」而把精确贡献放宽成并集盒。
- 例外:足迹超过一条记录能带的 `kMGPipeMaxPendingUploadRegions` 个盒子时,退回存储的 rect 列表(只保证覆盖,日志记一次);该列表又被扣下(rect 填满并集盒的 3/4)时,0 是一个**不精确**的部分盒。把「只有 box」读作「client 拥有盒内每个纹素」的读者(`AdoptDriverLevelIntoShadow` 的覆盖盒规则)只在这种情况下不精确。
#### 6.1.2 纹理视图的上传记在存储属主上
`glTextureView` 与源纹理共用纹素,所以 client 把经视图或经属主名字做的每次上传都记在**存储属主**上:`TextureObjectView` 把 dirty 标记换算成属主坐标(`ToOwnerLevel` / `ToOwnerUploadTarget` / `ToOwnerRegionOffset`)转给属主,`TextureEmit.h` 的排空列表按(存储属主, upload target, level)键控,每条纹理 `resource_subdata` 都指向属主句柄;applier 因此只推进属主记录的 `Serial` 与 `PendingUploads`。
- Espryt 的逐纹理 clean 门 `BackendTextureObject::IsDrawSyncCleanByRecord` 对两个存储条款读 `Desc.ViewOf` 指向的属主记录(`PipeTextureStorageRecordForRecord`),三个参数条款仍读视图自己的记录:视图有自己的纹理参数与内建 sampler。这与前端 `TextureObjectView::GetContentVersion()` 转发给属主是同一条规则。
- 若存储条款读视图自己的记录,视图第一次被采样之后,经属主名字的上传在视图上永不可见。门是 `TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary`,monolith 与三个传输臂都跑。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 过线的是逐 stage SPIR-V + 反射归档,**不是源码**:glslang 全在 client、SPIRV-Cross 全在 server,文件级切割;server 侧无编译池。归档序列化器 `ProgramArtifactsCodec`,monolith 只在 verify 构建里调它(D-H3)。
@@ -25,6 +25,19 @@ MobileGL/MG_Remote/ 仅 MOBILEGL_BUILD_DISAGGREGATED:CONTRACT-*.md、
- 测试接线:ctest `ENVIRONMENT` 用 `mgl_itest_join_environment(...)` 构造;每条 split / spawn / tcp 条目带独立日志路径,日志按角色分文件(`<base>.client.log` / `<base>.server.log`);spawn、tcp 车道与 split 名集合一致(`scripts/ci/spawn_lane_parity.py`),每条目记 arm 证明。
- CI(`.github/workflows/test.yml`、`apk.yml`):`pipe-gates`(G1–G8、生成器 self-test、符号报告、dirty-surface、字段归属、G5、文档引用 lint)、`flatc-check`、include 闭包、各 flavour 的 build / integration / retrace 车道、APK + AVD。两份 workflow 里的 `feat/disaggregated` 触发器是临时的,合入 `dev` 前移除。
### 16.1 集成测试车道的接线规则
`MG_IntegrationTest/CMakeLists.txt` 的每条车道都要能因它存在的理由变红(§13.2),接线规则:
- 读库日志的用例必须独占日志:库以 `fopen(path, "w")` 打开日志,同一路径上的每个进程启动时都截断它,`ctest -j` 下读者会读到邻居截断后的文件,症状与「计数从未发出」相同。条目因此带私有 `MOBILEGL_LOG_FILE_PATH`,过滤器只选这一个用例;车道必须跑整个 scenario(重新过滤会改测试名)时,加以日志命名的 `RESOURCE_LOCK`,ctest 不会同时运行持同一把锁的条目。
- 选臂的每个变量都写成条目自己的 ctest `ENVIRONMENT` 属性:属性只覆盖它写出的变量,其余从 job 环境漏进来(job 级 `MOBILEGL_TRANSPORT=inproc` 会把没钉 `monolith` 的 monolith 条目悄悄变成又一条 split)。
- ctest 不把 SKIP 算失败,跳光的车道和全过的车道一样绿:必须真跑的车道钉 `MOBILEGL_ITEST_REQUIRE_GPU=1`,让不可用的 harness 变成失败;功能 opt-in(如 `MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1`)必须到达每个臂里读它的进程,tcp 臂的 server 由 `TcpServer.Start` fixture 预先启动、不继承 client 条目的环境,所以也要设在 fixture 上。
- `ctest -L` 按正则子串匹配:新标签不得包含任何被不加锚点选择的已有标签,所以限定词放在中间(`integration-magma-all-spawn` 不含 `integration-magma-spawn`,`integration-magma-spawn-buffers` 不含 `integration-magma-buffers`);只选一个标签时写 `'^label$'`。
- 门控车道带一个私有的第二标签并以 `--no-tests=error` 运行:构建缺少该选项时匹配为空即失败,而不是零条目的绿。
- 三条传输臂:`Split.` 为 inproc,server 是测试进程里的 apply 线程;`Spawn.` 的 server 子进程继承 `::environ`(`ServerSpawn.cpp` 只去掉 `MOBILEGL_TRANSPORT` 与非 server 自有的 `MOBILEGL_IPC_*`);`Tcp.` 的 server 是整条车道共享的 `TcpServer.Start` fixture,由 `scripts/ci/tcp_server_fixture.py` 以它自己的环境启动一次,每个 Hello fork 一个 session 子进程。
- 条目 ctest `ENVIRONMENT` 里由 server 读取的旋钮只到达 inproc 与 spawn 的 server;tcp 副本会以默认臂运行却顶着声称旋钮的名字,永远不会红。这类条目只注册 split + spawn(Magma 的列入 `scripts/ci/spawn_lane_parity.py` 的 `MAGMA_SERVER_ENV_KNOB_NO_TCP`),旋钮取默认值的孪生条目保留三条臂;要 tcp 臂就得让 fixture 带上旋钮:对整条车道无害的追加到 `TcpServer.Start` 的 `ENVIRONMENT`,否则另起一个带旋钮的 supervisor(`P8Coverage.cmake`)。只能在本进程读取的探针(描述符池普查、server loop 挂钩、server 缓存窥视、验证层输出)只注册 inproc(`MAGMA_INPROC_ONLY`)。
- Magma 分三层:门控的 `integration-magma-{split,spawn,tcp}` 是手工布置、各带 flavour(strict + 双块、adopt 档、run-ahead credit、cache 钉定)的车道,测绿后才进 CI 门;`integration-magma-all-*` 重放每次 `mgl_itest_register_split_arms` 调用;`integration-magma-full-*` 跑整个集成二进制减去 `MGL_ITEST_MONOLITH_MECHANISM_FILTER` 与 `DualBlockScenario`。后两层不进任何 CI 门,用例随修复其阻塞点的提交升入门控层。
- `MGITEST_SPLIT_LANE=1` 武装 `ScenarioFixture` 的析构断言:case 必须推进 client 编码器的记录序号,tcp 臂还要证明没有本地 server 子进程。它只适合每条都绘制的精选车道;整二进制集合(`integration-magma-full-*`、`integration-verify-split`)不设它,因为纯查询 case 由 `CapsMirror` 回答、不产生记录,代价是这些集合不证明记录过了线。
## 附 A:开关
CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_INPROC`(OFF,隐含前者)、`MOBILEGL_PIPE_VERIFY`(OFF,永不出货)、`MOBILEGL_BUILD_SERVER_SPIKE`(仅 Android spike)、`MOBILEGL_FLATC_EXECUTABLE`(只服务 `flatc-check`)。`MOBILEGL_PIPE_PUSH` 与 `MOBILEGL_PIPE_LEGACY_MEMOS` 在 P13 删除(旧缓存里的 OFF 只告警并忽略)。