mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
df17c416c0a4da381d48d5a23de45abbc60da0b8
3491
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
df17c416c0 |
[MobileGL, MG_Backend, MG_Remote, scripts] (P7): the split wire comes up before the first frontend object, and the caps mirror stops deciding a family's fate from a placeholder
F1. `MG_State::Init()` runs before `MG_Backend::Init()`, and the second is what
starts the session - so `GLContext`'s `TextureState` constructor materialised its
eleven default texture objects, each of them emitted `resource_create` through
`FamilyIsLive`, and `CapsMirror::ServerConsumes(0x400)` answered every one of them
from a generation-0 placeholder with `callMask=0`. All eleven records were dropped
and the whole event was one WARN line:
MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
caps generation 0
It is NOT a race. The line reproduces byte-for-byte on lavapipe and on the Adreno
830, because the two Init calls are two statements on one thread. And "the legacy
pull path runs for it" is false on the client under split: there is no pull path
on this side of the wire, so withholding is not a fallback, it is a drop that only
a later respecify's self-healing create ever undoes.
Two of the three options the brief offered do not apply, and both facts are worth
recording. (b) "extend the P6.5 fix to inproc" is already the tree: `FinishStartup`
step 7 is the one funnel every transport reaches and it will not return without a
real snapshot. (a) "a bounded wait in Consumes()" cannot work alone and would
deadlock: at `MG_State::Init()` no handshake is in flight, and the one that will
land is started later BY THE SAME THREAD.
So the fix is the ORDER, with the rule behind it as the gate:
* `MobileGL::Initialize` calls `MG_Backend::InitSplitRolesBeforeState()` before
`MG_State::Init()`. `Init()`'s split arm becomes one body, `InitSplitBackend()`,
shared with the direct callers that still reach `Init()`; the monolith arm keeps
its place after `MG_State::Init()`, because moving it would move pull `.text`.
* `CapsMirror::ServerConsumes()` refuses to answer from a placeholder. It waits,
bounded, when a bring-up is genuinely in flight on ANOTHER thread, and otherwise
dies as `Fatal{CapsBeforeFirstSnapshot, "<family>"}` through `SessionFail` -
a new family word, one row in `FatalFamilies.def`, no new abort site.
* Two arms are exempt and say why in place: the server arm (`MGPipeServerArm()` -
the apply thread and the spawn server, where the client's mirror is the wrong
object rather than a late one), and a process whose own configuration never
asked for a split transport (`MG_Config::SplitTransportRequestedByConfig`, set
by `MG_ConfigLoader::InitTransport`, which is what tells a real run apart from a
fixture that assigns `MG_Config::Transport` by hand).
The read accessors keep P5's ruling untouched: `LogBackendInfo` reads
`GetRendererInfo()` during `MG_Backend::Init()` and an abort there is a process that
cannot start. Only the DECISION changed. The warning was not made quieter and the
callMask was not defaulted to all-ones.
`MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` (server side, disaggregated builds, inert unless
set) holds back the FIRST CapsSnapshot. On spawn/tcp that is a genuinely late
snapshot in another process and it exercises the client's step-7 wait; on inproc it
only makes `Start` slower, which is itself the proof that the only window a client
can read a placeholder in is the one before `Start` is called.
G1: `.text` 0xa52203 and the 30570-symbol list identical to p7-before/pull-syms.txt.
Census: 79 abort sites unchanged, 44 family words (was 43). Link ratchet: 186,
unchanged.
|
||
|
|
206abdfa44 |
[docs] (P7): the gate-5 CTS base reading is on record - monolith x DirectVulkan x KHR-GL46 on the Redmi, five complete blocks
shader-image 63/56/6, ssbo 124/0/0, dsa 368/0/2, texture 815/221/18, packed-pixels 4732/0/0 (Pass/NotSupported/Fail); every block complete (no unrun, hung or crashed); library p7w1-d260f110, glcts from VK-GL-CTS 0b04c470e with the FBO colour-texture patch, API probe confirms GL 4.6. The AFTER reading compares inproc against this at 0.5 pp per block with new crashes as a separate hard red. |
||
|
|
5c4235d7ba | [docs, MG_Remote] (P7): ID-P7-12..17 - gate 3 per case against monolith, photon excluded, wave-0 naming ratified, X1 on the critical path, gate 5 is five blocks at GL46, E2/E0b folded into the F1 re-test | ||
|
|
ec3ba00af3 |
[tools] (P7): the CTS skip list the runbook already passes exists
run_cts.py silently tolerates a missing --skip-file, so the known device-killers from the skill (clip_distance.functional, the multisampled_to_singlesampled blit family, the rgb565 repeat-mode case) would each have cost a reboot in the gate-5 window. |
||
|
|
4cbf78c269 |
[MG_Remote, MG_Test, docs] (P7): the persistent-map tracker answers faults in the kernel's address space, and a map hands back a range it can write
Wave 2 package X1. Three CI split-subset traces died on the Redmi under inproc and
passed under monolith on the same device and APK, on the client thread, in the
replayer's memcpy into a pointer MobileGL had handed it: SIGSEGV / SEGV_ACCERR on the
destination's FIRST byte, destination page-aligned.
The tombstone names the same address twice, in two different address spaces:
x0 b4000073ac9a5000 ... x2 0000000001800000
signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x00000073ac9a5000
x0 is the pointer the application writes through, carrying bionic's 0xb4 heap tag
(the same tombstone reports tagged_addr_ctrl PR_TAGGED_ADDR_ENABLE). fault addr is
si_addr, which the kernel reports UNTAGGED.
TrackWriteMap took shadowBase from MappedData() - a tagged heap pointer - kept the tag
through the page masking, mprotect()ed the mapped range PROT_READ and published the
TAGGED base/end into the slot. The handler compared si_addr against it:
if (base <= kSlotSettingUp || address < base) continue;
0x73ac9a5000 < 0xb4000073ac9a5000 is true for every fault, forever. Ownership test (2)
never matched, and every fault on a page the tracker had itself protected was chained
to debuggerd. mprotect was fine throughout - do_mprotect_pkey untags its own argument -
so the right pages really were read-only; only the recognition was broken. x86-64 has
no top-byte tag, which is why every host lane has always been green.
The fix is two rows and one ordering:
* UntagAddress(), applied to the shadow base at registration, to the shadow base in
PushBlocksFor (so trackedBase - shadowBase stays in one space) and to si_addr in
the handler. Written WITHOUT an #if defined(__aarch64__): masking bits 56-63 is
the identity for every valid userspace address off arm64, and an arch guard would
have put the only gate that can catch this on the one platform CI never runs.
Dereferences keep the tagged pointer they came from.
* SlotOwnsFault(), the ownership row the handler and the unit case both read.
* REGISTRATION ARMS NOTHING. AcquireMemoryRange registered and only then returned
Bytes() + range.start, so glMapBufferRange handed the application a PROT_READ
pointer whose writability depended on a handler reaching the writing thread - and
the application is free to write a mapped arena from a worker that blocks every
signal. Nothing is lost by waiting: every page bit is already set at registration,
so the first push ships the whole interior regardless, and it is that push which
arms the pages. 6144 pointless faults per 24 MB arena go with it, and the
invariant the file rests on (bit set <=> page writable) is now true from
registration onward instead of only from the first push.
Diagnostic, independent of the fix: one MGLOG_I_ONCE at the first registration naming
the mapping the way the device run needed and did not have (allocator, pointer, ITS
TAG, extent, range, published span, pages, alignment arm), and a decline record in the
handler - relaxed atomic stores plus one async-signal-safe write(2) - readable through
DeclinedFaults(). A non-zero decline count whose si_addr lies inside nearest-tracked
once the top byte is masked off is this defect's signature and nothing else's.
Not the mechanism, and checked: libsigchain (the tombstone's "F libc : Fatal signal 11"
is bionic's own handler, so the chain ran), the 16 KiB-page refusal (the device is a
4 KiB kernel and the protection demonstrably took), RefaultOfANonWriteAccess (matched
is false long before it is consulted), and the P5d shadow allocator (correct - it is
what makes the destination page-aligned in the tombstone).
Red-once, two and orthogonal (MG_Test/Wire/PersistentMapFaultTest.cpp, 6 cases):
dropping the two UntagAddress calls in SlotOwnsFault reds only
AKernelsUntaggedFaultAddressIsOwnedByATaggedShadowsSlot, which runs the device's own
two addresses through the real predicate on an x86-64 host; restoring the mprotect at
the end of TrackWriteMap reds only AMapHandsBackARangeWritableWithoutTheFaultHandler
(the child dies by SIGSEGV) and RegistrationLeavesEveryPageOfTheRangeReadWrite.
AnArmedPageIsStillAnsweredByTheHandlerAndMadeWritable is the anti-vacuity case.
Gates: ctest -L unit 2408/2408; integration-split 186/186; integration-spawn 102/102;
integration-magma-split 73/73 (PersistentCoherentMapScenario green in the Split, Spawn,
PersistentMapArm and SmallRing variants); fatal_census.py 0 unmarked. G1 holds - pull
.text 10822147 = 0xa52203 unchanged, symbol report 0 added / 0 removed / 0 resized /
0 renamed, nm --defined-only identical at 30570 and nm -D at 11933. Every line is under
MOBILEGL_BUILD_DISAGGREGATED (CMakeLists.txt:592-659), so the pull build sees none of it.
Evidence and the device experiment: docs/Disaggregated/notes/p7/x1-persistent-map-segv.md
|
||
|
|
4f2d4c4e83 |
[MG_Remote, docs] (P7): CONTRACT-P7 carries wave 0's numbers, and device window #1a is on record
CONTRACT-P7: the lane counts (magma-split 73 / spawn 52 / tcp 54; full-suite 510 x 3 with one StreamLink x Magma red), the census baseline (79 abort sites over 20 files, 43 family words, 0 unmarked), the ratchet baseline (186 = p7-magma 101 / espryt 14 / both 60 / core 11; espryt's clearable count is 9) and the @P7 count (15 sites / 4 files) replace the 待填 markers. Device window #1a (Redmi 2f7cbe2e, APK p7w1-d260f110, reboot-clean): E0a monolith 39/39 deterministic, three below threshold on monolith itself (create-indirect, 1.21.11 main-menu, iris-photon - Adreno refuses its pipelines with VK_ERROR_UNKNOWN); E0a inproc: OpenRA 0.976494 (run-ahead) / E1 lockstep 0.988998 - both historical numbers were OpenRA, and its client log shows a TextureResources verb answered from caps generation 0 (package F1); 26.3 / create-* SIGSEGV in the replayer's memcpy on BOTH backends and with the persistent-map push off (package X1); iterationt x2 die by name on Magma:mipmap-shader-format-or-shape (package B); bsl-esc-menu hits scudo OOM under inproc. Gate 3's denominator is 36; run-ahead and present credit are off the suspect list. |
||
|
|
801f8edaa6 |
[scripts] (P7): the parity self-test patches the seam the Magma tiers read through
Package D added a test that patches lane_cases and hands spawn_lane_parity.py a placeholder build dir; package L then made the three Magma tiers read through a new lane_names, so the placeholder reached a real ctest and the unit label went red on the integrated tree. Patching lane_names to an empty set on every arm makes those tiers report "nothing to compare" and leaves the assertion about lane_cases intact. |
||
|
|
8bb6309a1c |
[scripts, CI, docs] (P7): the 184-symbol link-closure ratchet lands as a gate
CONTRACT-P6.md 12.1 has carried "a ratchet recomputes the 184 in CI and asserts it only
ever falls" as an obligation since P6. The roadmap handed it to P6.5; the P6.5 line never
received it, and scripts/ci/ has never had anything that computes this number. ID-P7-7
gives it to P7 wave 0. This is it.
scripts/link_ratchet.py mechanises a6's hand experiment: partition the per-object output
of the single MobileGL target into SERVER / FRONTEND / SHARED, then compute
(SERVER undefined) - (SERVER defined) - (SHARED defined) intersect (FRONTEND defined),
which is the undefined-symbol list a real link of a server-only image would print. The
partition is an explicit table with a rationale per row, because it IS the module boundary
the build system does not have (SOURCE_FILES is one flat list; boundaries are P13's work).
An object matching no row is a hard failure, as is a build whose CMakeCache lacks the three
disaggregated options, or an empty role set - every way this gate could go green without
having measured anything is an explicit exit 2.
Measured on
|
||
|
|
1de8f02ebe |
[MG_IntegrationTest, docs] (P7): the whole integration binary runs under Magma split, and it is one red
Tier 2 replays the split-arm registrations - ~100 scenarios, the set Espryt's disaggregation work
has carried since P5 - and it measured green on Magma across all three transports. That is a real
result but it is NOT exit gate 1. The gate says the INTEGRATION BINARY is green under Magma split,
and the set that names is the one the monolith DirectVulkan. arm runs: the whole suite minus
MGL_ITEST_MONOLITH_MECHANISM_FILTER. 510 cases against tier 2's 102, and the readback matrices,
the same-texture copy shapes and the odd-offset uniform ranges are in it and nowhere else in the
disaggregated registry.
integration-magma-full-{split,spawn,tcp}, in no gating step, private log per entry through a list
of its own. THE RESULT: 453/0/57 on inproc, 453/0/57 on spawn, 454/1/57 on tcp - and not one of
the 1530 entries reaches any of the tree's fifteen @P7 named refusals. The single red is
IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput on the TCP arm:
one texel of 262656, texel 0, which is the case's own exposure value
(IterationRPProgram203Scenario.cpp:371, actual half bits (0x357a, 0x4b65) against golden
(0x3a74, 0x4821)). Deterministic 5/5 on tcp; 5/5 GREEN on inproc, on spawn and on the monolith
control. spawn and tcp differ only in MOBILEGL_IPC_DATA, so the suspicion is the STREAM data path
and not "two processes"; the GLES tcp lane is green in the same run, so it is not the fixture.
Not fixed here - it is what the census is for.
TWO ENVIRONMENT DIFFERENCES FROM THE GATED TIERS, both measured rather than assumed:
* NO MGITEST_SPLIT_LANE=1. That marker arms ScenarioFixture's destructor assertion - a case in
an armed split lane must have moved the client encoder's record ordinal - which is right for
a curated lane and wrong for a whole-binary census. With it set, the first run produced 13
reds, all `EmitSeq was 20 at SetUp and is 20 now`: AdvertisedLimitsScenario (8),
SpirvShaderBinaryScenario (4) and ProgramPipelineScenario.AReservedName - a limits query is
answered from the client's CapsMirror and a shader-binary error surface never reaches a verb.
Those 13 would red on a DirectGLES census too. What it gives up is stated in the comment: this
tier does not prove its records crossed, and the tcp topology proof is inert in it.
* DualBlockScenario.* filtered out. Its green REQUIRES MOBILEGL_IPC_ROLE_SPLIT_STATE=1 and a
census must not set that knob; without it the case is red by name, which is its documented
negative-control behaviour. The monolith arm skips it for the same class of reason.
CtWireScenario gets MOBILEGL_TEST_SERVER_COUNTERS=1 in this tier - a switch that makes a counter
readable, not an oracle that asserts - because without it the spawn arm's ApplierReset case reds
on "server control-record telemetry missing", which says nothing about Magma. The strict-arming,
pmap and persistent-map-arm markers stay out.
The inproc-only exclusion in spawn_lane_parity.py is now PER TIER: in the gated tier the two
scenarios are registered on the inproc arm alone, in the census they are on every arm and simply
skip, so excluding them there would manufacture a spurious "extra".
Measured after the change: every pre-existing lane unchanged and green (magma-split 73,
spawn 52, tcp 54, all-* 102/102/104, GLES 186/102/104, buffers 19, runahead 14, caches 7), the
dual-block census 257 green / 2 skipped / 0 red with an empty ratchet, both new CI steps green,
parity RC=0 across all three tiers, G14 4155 -> 6095 with removed=0.
The note now carries the grouped red list, the 57 named skips by reason, the four pieces of
evidence that the tcp red is a defect and not plumbing, and six findings - the first being that
wave 2's clusters cannot be scheduled from this lane's red list, because on lavapipe there is
essentially none.
|
||
|
|
78e10ccf80 |
[CI] (P7): retrace-split exports every Magma knob the monolith retrace job exports
Integrator ruling, completing the alignment package L started. The split retrace job now also exports MOBILEGL_MAGMA_R11G11B10F_FALLBACK for every DirectVulkan row and MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE for improved-transparency-minecraft-26.3, copied from the monolith job (:2180 / :2195) with the same guards. The point is comparability, not the knobs: a split retrace whose monolith control ran a different repair set cannot say whether a divergence is a transport fact. With the three iterationRP repairs from the previous commit, every DirectVulkan row of the 154-cell split matrix now runs the quirk configuration its control runs. retrace-verify is left alone, as the ruling says. |
||
|
|
a7425d95e0 |
[MG_IntegrationTest] (P7): every Magma lane pins the ICD the monolith arm has always pinned
Integrator ruling on the asymmetry package L reported. The six hand-laid Magma lanes (Fm,
Buffers, RunAhead, Caches, NamedBlit, P5fRsp) appended ${MGL_ITEST_COMMON_ENV}, so the monolith
DirectVulkan. arm ran with VK_ICD_FILENAMES pinned and the three lavapipe iterationRP repairs
forced on, while the 73 integration-magma-split entries ran against whatever ICD the loader
found first with those repairs at their device auto-detection default.
That is a latent flake rather than a semantic: the day a runner or a workstation grows a second
ICD, the lane measures a different driver from the gate it mirrors and nothing in its output
says so. Consistency wins - all six now append ${MGL_ITEST_VULKAN_ENV}, the same list the new
MGL_ITEST_VULKAN_{SPLIT,SPAWN,TCP}_ENVIRONMENT use.
The ICD pin follows the BACKEND, not the lane, in the two blocks that register both: NamedBlit
and P5fRsp loop over DirectGLES and DirectVulkan, and handing VK_ICD_FILENAMES to the DirectGLES
half would pin an ICD for a lane that never opens Vulkan. Verified in the discovery: all 73
integration-magma-split entries carry VK_ICD_FILENAMES and all three repair variables, and the
two DirectGLES.Split.NamedBlit. entries carry neither.
Re-run after the change, as the ruling asks: integration-magma-split 71 PASS / 0 FAIL / 2 SKIP
out of 73, unchanged - the two skips are the same ClipDistance pair with the same reason.
|
||
|
|
faca09d4ad |
[CI, docs] (P7): retrace-split exports the iterationrp knobs, and the two new Magma lanes gate
Three changes, one theme - making the two halves comparable, then gating what measured green.
retrace-split's "Retrace and validate under MOBILEGL_TRANSPORT" step exported no Magma knob at
all, while the monolith retrace job (:2183) exports three iterationRP repairs for
minecraft-1.21.4-fabric-iris-iterationrp-in-world x DirectVulkan. Those repairs are tri-state
quirks defaulting to device auto-detection and lavapipe is on no auto list, so that cell of the
split matrix (154 cells, and it contains that case x DirectVulkan x {inproc, spawn}) was being
compared against a monolith control running a different repair set. The same backend+case guard
is now copied verbatim into the split job.
Two gating steps for the lanes this wave added, and ONLY because they measured green on the
first run: integration-magma-{spawn,tcp} run like the existing magma-split step, and the
nineteen application-buffer cases get require_green BY NAME on both new arms - the tcp arm's
required list includes TcpServer.Start / Stop on purpose, because ctest lists the fixture
entries under the label and a lane whose supervisor failed to start has nothing to say about the
nineteen. The informational integration-magma-all-* tier is deliberately in no gating step.
Both steps were executed locally, not merely linted: spawn 19/19 and tcp 19/19 with
"require-green: N PASS, 0 skip, 0 failed, complete discovery". All 99 run: blocks in the
workflow pass bash -n.
The note records the first run in full: per-lane PASS/FAIL/SKIP, the two blocker groups the
first registration attempt produced (both lane plumbing, neither a server abort, wrong pixels
nor a hang), the arming evidence, both red-once demonstrations, the parity and G14 numbers,
what was NOT verified (no device, no retrace run, no G1) and six findings for the integrator -
the first of which is that P7 exit gate 1 has no host-side red list at all, which the wave 2
plan is built on top of.
|
||
|
|
e7e3265c47 |
[MG_Remote] (P7): the wire client's death notice is installed for any backend, not just Espryt
WireTables.cpp:740 installed the client-side object_death emitter only when
ActiveBackendType == DirectGLES. P6.5 wrote that because DirectGLES was the only backend with a
two-process lane, so the condition read as "the case this can happen in" rather than as a policy
- and under a Magma spawn or tcp client it is a silent hole of exactly the shape P6.5 closed for
Espryt: no notice installed, NotifyAndFree drops every object_death before freeing the slot, and
CtWireScenario's two death cases report ObjectDeaths=0 and prove nothing.
The two remaining conditions carry P6.5's intent, both halves of it: Transport == Spawn (this is
the remote client, the one with no local twin table; under inproc the backend's own dispatcher
is the right one) and GetStateObjectDeathOps() == nullptr (whoever installed first keeps the
notice).
RED-ONCE: with the install site short-circuited, both DirectGLES spawn death cases red on
CtWireScenario.cpp:187/:246 with
Expected: (afterCounters.deaths) > (deathsBefore), actual: 0 vs 0
which is the silent zero this closes. Restored and green.
WHAT IT DOES NOT DO is give Magma a death table - that is wave 2 package C. But it already does
more than the plan predicted: with CtWireScenario's own BackendName() != "DirectGLES" skip guard
temporarily lifted (a probe, not committed), both DirectVulkan SPAWN death cases PASS, while the
DirectVulkan inproc arm still fails. So the two-process Magma death path works today and the
remaining gap is the inproc arm alone. Recorded in the package note; narrowing that skip guard
is package C's call and its file.
|
||
|
|
3d9a38a979 |
[MG_IntegrationTest, CI] (P7): the split arms take a backend, so Magma has two-process lanes at last
P7 exit gate 1 asks for integration green "under Magma push and split". Until now it was not
merely unmet, it was UNMEASURABLE: every arm of mgl_itest_register_split_arms hardcoded
MOBILEGL_BACKEND_TYPE=DirectGLES (:2002/:2025/:2031) and MGL_ITEST_MAGMA_WIRE_ENVIRONMENT
hardcoded MOBILEGL_TRANSPORT=inproc (:2456), so the shared integration-split / -spawn / -tcp
labels held ZERO DirectVulkan entries and the 73 integration-magma-split cases only ever ran in
one process.
The registration macro now takes the backend as its first argument; the old name is a thin
wrapper, so all 32 call sites are untouched and every DirectGLES name, label and private log
path is byte-for-byte what it was. Two tiers come out of it, and the CMake comment states both:
* GATED - integration-magma-{split,spawn,tcp}. The six hand-laid Magma lanes (Fm, Buffers,
RunAhead, Caches, NamedBlit, P5fRsp) grow arms through one shared shape macro; the inproc
arm keeps its names, its labels and its log paths, so the CI steps and the two magma_*
checker scripts that name them by hand keep working unchanged.
* INFORMATIONAL - integration-magma-all-{split,spawn,tcp}, in no gating step. Every scenario
the split arms run, on Magma, on all three transports, generated by REPLAYING the macro's
recorded call list - so a scenario added to the split arms tomorrow joins the Magma census
the same day. Its job is to be able to go red while CI stays green; a case moves up into the
gated tier with the commit that retires its blocker.
The label spellings are chosen against ctest -L's regex (ID-131 again): the tier word goes in
the MIDDLE, because integration-magma-spawn-all WOULD have been selected by
-L integration-magma-spawn and pulled ~100 expected-red entries into a gated lane. Sub-labels
mirror it: the inproc arm keeps integration-magma-buffers, the others put the arm word first.
MagmaRunAheadScenario and MagmaWireCacheScenario stay on the inproc arm alone, and that is
MEASURED rather than assumed: with all three arms registered, all 21 entries red on spawn and
tcp with no Fatal in either role log - the first arms its hold through
ServerLoopInstance().SetBeforeRetireHookForTesting, a hook on THIS process's server loop, and
the second asserts transportName == "inproc" in its SetUp. Both are named in the CMakeLists and
in spawn_lane_parity.py rather than allowed to widen a tolerance.
split_log_paths.py's is_split prefixes gain DirectVulkan.Spawn. / .Tcp. - on those arms two
processes append to one path, so a shared log does not merely blur attribution, it interleaves
two sessions. spawn_lane_parity.py gains the Magma dimension, normalising only the ARM segment:
RunAhead.Credit1. and .Credit3. are two entries of one case and a tail-stripping normaliser
would fold them together and then report an equal-sized set that had lost one.
Measured on this tree (lavapipe): magma-split 73 unchanged and green, magma-spawn 52/52,
magma-tcp 52/52, the three informational lanes 102 each, GLES split 186 / spawn 102 / tcp 104
all unchanged and green. G14 against ~/w7/p7-before: 4155 to 4565, removed 0, added 410.
|
||
|
|
9e8fdacd21 |
[MG_Remote] (P7): gate 3 is judged per case against the monolith arm, and its denominator names what monolith already fails
Device window #1 E0a: monolith x DirectVulkan on the Redmi is 0.996-1.000 against the desktop goldens (only OpenRA is 1.0), so "monolith 1.0" was an OpenRA fact, not a rule. Gate 3 now reads: per case, inproc and spawn bit-identical over three repeats, within the case threshold, and within 0.0005 of the same-session monolith reading. The denominator excludes what monolith itself fails - create-indirect (OQ-17), the 1.21.11 main-menu trace artifact, and iris-photon, whose pipelines Adreno 830 refuses with VK_ERROR_UNKNOWN (PipelineFactory.cpp:660) - which leaves 36. Gate 5 records the real block sources: UBO lives only in the GTF module, SSBO joins, gl33 is the fallback. |
||
|
|
8b02feb245 |
[android-plugin] (P7): the result verdict hands native python a host path whatever the caller set MSYS_NO_PATHCONV to
On Git Bash the script protects its own adb calls with MSYS_NO_PATHCONV=1, but the two python invocations relied on the caller NOT having exported it; a session that did (for the adb-side /data/... arguments the runbook mentions) got json.load failing on /c/... for a result.json that existed - every case reported "trace replay failed" over a passed result. host_path_for_adb already exists for exactly this; use it. |
||
|
|
c890f0ad1a |
[docs] (P7): the runbook spells the side-by-side package the way the flavour builds it, and its two shell traps are removed
The application id is top.mobilegl.plugin.p7w1.trace (flavour suffix after the id suffix), pgrep -f dex2oat matched the shell running the loop and never exited, and adb.exe cannot stat an MSYS /c/ path once MSYS_NO_PATHCONV is set. All three were hit on the first execution of section 3. |
||
|
|
9be62cbc21 |
[tools] (P7): the UBO caselist comes from the GTF module, and the SSBO block joins gate 5
KHR-GL46 has no uniform_buffer_object group; the 90 UBO cases are GTF-GL46.gtf31.GL3Tests.uniform_buffer_object*, present only when the deployed glcts was built with GTF - the device probe decides. KHR-GL46.shader_storage_buffer_object (124) is the block that exercises what cluster A actually refuses today: unaligned and over-range SSBO bindings, not only UBO byte tails. |
||
|
|
4e7989f1c2 |
[tools] (P7): the five CTS caselists for exit gate 5 are committed with their provenance
Cut from VK-GL-CTS 0b04c470ed78569b0b778ece1262d88455ab2b00 (main; the deployed glcts build's source) mustpass gl46-main.txt and gl33-main.txt: texture_*, shader_image*, packed_pixels, direct_state_access, uniform_buffer*. The gl33 variants are the fallback the runbook names for an Android runner that cannot open a 4.6 core context; whichever version the probe selects, BASE and AFTER read the same list. A 0.5 pp gate needs a committed denominator, not a glob over someone's checkout. |
||
|
|
484a56a5ae |
[scripts, MG_Pipe, MG_Backend, MG_Test] (P7): the census docstring's triage follows what the same wave retired, and the comments stop joining the @P7 census
Two corrections to the two commits before this one. The widening's docstring was written against the 83 sites it found, and the commit that routed five of them through Session::Fail did not revisit the prose. It now describes the 79-site floor the baseline actually records, says which five went and where, and states that the enforceable artefact is the baseline's per-file map rather than the counts in the prose - so a reader who finds the two disagreeing knows which one to believe. The three buckets are restated too: `(ii)` is no longer "owned by a Ph / P7 row", because with the Magma wire arms routed what is left there is the peer-reachable deaths in the applier and the two backends, which is exactly the set ID-P7-1 scopes to Ph's fuzz arm 2 AFTER P7. And the routing commit's own comments spelled the at-sign refusal marker while explaining what routes through them, which put SEVEN comment lines into the retirement census: `grep @P7` went from 15 sites in 4 files to 22 in 6, and that grep is how wave 2 will measure which refusals it has retired. The prose says the word instead, and the count is 15/4 again. |
||
|
|
a5dcbb444f |
[MG_Remote, scripts, MG_IntegrationTest] (P7): the supervisor names a faulted session, and the smoke that proves it is finally a ctest entry
Ph slice (1) (ID-P7-1). Three `::waitpid(active, nullptr, WNOHANG)` calls in the `--serve` loop threw the status away, so a session child that died of SIGABRT inside SessionFail and one that returned from its control loop and _exit(0)'d were the SAME EVENT to the supervisor: `active = -1`, no line, nothing to count. ID-P7-1 declines the literal 98-site latch translation because P6.5's fork-per-session already delivers "the next connection is served as usual"; what was missing is that anyone could TELL a faulted session from a finished one, which is what an operator, a fuzz arm and a flake triage all actually need. One `ReapActiveSession()` where there were three call sites, asking WIFEXITED / WIFSIGNALED / WTERMSIG - the same three questions ServerSpawn.cpp:289 asks of the same kind of child - and logging `pid=<n> reaped exit=<c>` or `signal=<n>` with the supervisor's lifetime `sessionsFaulted` beside it. A refused handshake exits 0 and is NOT a fault, which is right: protocol.fbs:86 says a rejection is a normal session outcome. The running total is on every reap line as well as on the summary, because the fixtures end a supervisor with SIGTERM and a figure only printed at a chosen exit would never be seen. THE IDENTIFIER ROW'S PREMISE IS WRONG, MEASURED. The plan asks for CtrlEnvelopeBufferHasIdentifier at ServerMain.cpp:193 because ServerSession::ParseEnvelope asks it and this loop did not - but the generated VerifyCtrlEnvelopeBuffer already IS `VerifyBuffer<CtrlEnvelope>(CtrlEnvelopeIdentifier())` (protocol_generated.h:2378), so a wrong identifier was refused here and ParseEnvelope's second call is redundant too. Recorded for the integrator. What was real is that both refusals were SILENT: a peer whose post-Welcome frame was garbage had its session ended with no line in the log, and a fuzz arm that cannot tell "refused, by name" from "the child went away" measures nothing. So the identifier is now asked first - it is the specific diagnosis where the verifier's is the general one - with the length guard that asking it first requires, and both say so. scripts/ci/tcp_supervisor_smoke.py has been in the tree since P6.5 asserting four supervisor controls and was referenced by NOTHING - no ctest entry, no workflow step - so nobody could have said whether those four were green. It is now `TcpLane.SupervisorProtocolControls` beside the TCP lane's own fixtures, labelled integration-tcp, under the lane's resource lock (a second supervisor's child would answer the other's peers with Busy). It needs flatc, which is deliberately not in the build graph, so it resolves one gen_protocol.py's way and exits 77 - ctest SKIP - when there is none, rather than passing on a run that did not happen. Two cases added to it: a session child killed with SIGABRT must be named by pid and `signal=6` with `sessionsFaulted` one higher than before, and the next Hello must still be welcomed; and a steady-state frame carrying the wrong file identifier must be named rather than silently dropped. Red-once, both ways, against the real image: making the reap discard the status again reds with "the supervisor did not name session pid=N as killed by signal=6", and deleting the identifier branch reds with "read a frame with the wrong file identifier without saying so". |
||
|
|
e1ec69341a |
[MG_Pipe, MG_Backend, MG_Remote, MG_Test, scripts] (P7): the Magma wire fatals and the two Staged aborts go through Session::Fail
Five deaths in the split server image raised their own std::abort(). Three of them are funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), WireDescriptorFatal (Renderer/UniformManager.cpp) and WireBufferLegacyFatal (Renderer/WireDraw.inc) - and the fifteen `@P7` refusals on the tree (8 in UniformManager.cpp, 4 in WireFramebuffer.inc, 2 in WireDraw.inc, 1 in VulkanRenderer.cpp; plan section 1.3's count confirmed) all die through two of them. Every one of those deaths published NO SessionFault frame, so the peer read a bare EOF; bumped NO SessionFaultCount(), so exit gate S8 could neither confirm nor falsify it; and carried a hand-typed family word with no path to FatalFamilies.def's projection. MG_Backend may not name an MG_Remote symbol on its own account, so the route is a seam in MG_Pipe with MGP_TRIP_WIRE_REPORT's shape (PipeApply.cpp:66, and the new code sits beside it): MG_Pipe/PipeSessionFail.h declares MGPipeSessionFail, PipeApply.cpp defines it inside `#if MOBILEGL_BUILD_DISAGGREGATED`, the DEFAULT behaviour is byte-identical to what the three funnels did on their own, and MG_Remote::InstallPipeSessionFailHook() points it at SessionFail. The install is in InitServerRoleCommon (MG_Backend/Init.cpp), step 0, because that is the one function BOTH server roles run - the inproc role and the spawn/TCP session child - so there is no per-transport install to get wrong. MGPipeFatalFamily is a two-value boundary enum, NOT MGFatalFamily cast to an integer: the .def's enumerators are positional and a row inserted in its middle would silently re-point every hook call at a different family. FatalFunnel.cpp's adapter is the one place the two vocabularies meet, as a switch with a named default. The family word stays UnmigratedVerb rather than becoming a new MagmaWireRefusal. Three reasons: the message strings are required to stay byte-identical (5.2), and the retrace refusal censuses recorded since P5b count these deaths by that word - a rename invalidates every `@P7` measurement taken against them; `Magma:` in the detail already separates the Magma arm from the GLES UnmigratedVerb deaths, so a new word adds no discrimination a grep does not have; and its wire projection would be ProtocolCorruption, exactly UnmigratedVerb's. Recorded for the integrator. Ph slice (2) in the same commit: Server/StagedShadow.h's RequireCovered and Server/StagedTextureStore.h's RequireLevelBytes were MGLOG_F + bare abort. Both are inside MG_Remote already, so they call SessionFail directly, message verbatim; check_include_closure.py's four probes do not reach either header and the gate is green. Census: 83 abort sites -> 79 over 20 files (five sites retired, one gained for the seam's no-hook default, which is argued into FUNNEL_SITES), baseline rewritten in this commit - the downward ratchet's notice is what asked for it. Two gate bugs the widening exposed and this commit fixes: the SessionFail call regex also matched `MGPipeSessionFail(` (a lookbehind now), and a funnel's anchor needed a wider window than a marker's (kFunnelAnchorWindow). Red-once, three ways. Pointing MagmaWireFatal back at its own abort reds the census naming WireFramebuffer.inc 0 -> 1. Making the seam skip the hook reds both new death cases - `TheMagmaWireVerbDeathReachesSessionFailAndBumpsTheFaultCount` on the exit code, because it reads SessionFaultCount() out of a SIGABRT handler and gets 0 instead of 1, and `TheSeamHandsTheInstalledHookItsFamilyAndItsLineVerbatim` on the line the hook echoed. |
||
|
|
f141918aa1 |
[scripts, MG_Remote, MG_Impl, MG_Test] (P7): the fatal census gate widened to the split server image
`scripts/ci/fatal_census.py` scanned `MG_Remote/` and saw four abort sites. CONTRACT-P6
§12.1 already says what the server image is - "P6's server image is the whole
libMobileGL.so" - and the server process was executing seventy-nine more deaths the gate
could not see: 15 in DirectGLES.cpp, 12 in Managers.cpp, 11 in VkBufferManager.cpp, 7 in
PipeRoute.cpp. SCAN_ROOTS is now MG_Remote / MG_Pipe / MG_Backend / MG_Impl/Pipe /
MG_State, `.inc` joins `.cpp`/`.h` (the Magma wire arms and the generated wire records are
.inc files, and 20 sites are reachable only through one), and MG_Benchmark /
MG_IntegrationTest join MG_Test in the exclusion.
Four rules now, where there were two:
1. an abort names its family within twelve lines, OR it is argued into FUNNEL_FILES /
the new FUNNEL_SITES table. FUNNEL_SITES exempts an individual funnel inside a file
that has ordinary named deaths around it, and only when one of the file's anchors
appears in the same window - so the exemption follows the funnel if it moves, expires
if it is deleted, and does not cover an unrelated abort added to the same file. Two
entries, one line of reason each: PipeApply.cpp's MGP_TRIP_WIRE_REPORT and
PipeWire.inc's MGPipeWireProtocolFatal.
2. unchanged: a WireLogFatal / SessionFail call carries a `Fatal{` in its string.
3. NEW: every family word found in the perimeter has a row in FatalFamilies.def. Eleven
words did not, and now do (see below).
4. NEW: every `Refuse{Word}` names a RefuseCode enumerator. This is what catches
`Refuse{AuthenticationRequired}` (SocketTransport.cpp:161), a word protocol.fbs has
never had - PH-7 (2). Renamed to the enum's own `Authentication`, and
SocketTransportTest pins the spelling to `EnumNameRefuseCode` so the two cannot
drift again.
`abort_sites` was recorded and never compared. It is now a DOWNWARD ratchet with a
per-file map beside it, so "83 became 84" reports as "VkBufferManager.cpp went 11 -> 12,
here are its lines"; fewer sites - the direction `@P7` retirement moves the number - asks
for a re-baseline rather than blocking one.
Eleven new `.def` rows for the eleven rowless words. Ten are ProtocolCorruption for the
reason that bucket already states. ResourceUnavailable is ServerCrashed, not SegmentMismatch:
SegmentMismatch means a shared SEGMENT was the wrong size, and telling a peer that when a
VkBuffer allocation failed would be a lie about whose geometry was wrong. A twelfth word was
collapsed rather than given a row - `InitialBytesNotCarried` (PipeFill.cpp) said the same
thing as `UncarriedInitialBytes` (WireTables.cpp) about the same record, and two words for
one family is the drift a6 censused.
Red-once, four ways: an unnamed `std::abort()` appended under MG_State reds naming the file
and the line; deleting the `Fatal{` from PipeRoute.cpp's ResidualBlockSize reds; putting
`Refuse{AuthenticationRequired}` back reds rule 4; a typo'd family word reds rule 3 and the
vocabulary ratchet together.
|
||
|
|
1e2d076633 |
[docs] (P7): the wave-1 device window runbook
One reboot-clean session on Redmi 2f7cbe2e, copy-pasteable, with the shell each block runs in marked: pin and wake/Doze, APK install identity, P7-7 E0-E3, the P6.5 residual 39-case DirectGLES TCP golden matrix, the CTS $BASE for monolith x DirectVulkan, and the temp-environment restoration. Every step states in one line which result supports which hypothesis, and where the evidence is archived. Four things the runbook had to settle rather than restate: E1's proof. `running lockstep` is NOT the line that proves lockstep took effect: ClientSession.cpp:1873 guards it with `RunAhead != 0 && m_barrierArmed`, so it is the "the knob is set but the server lacks kCapRunAheadApply" warning and MOBILEGL_IPC_RUN_AHEAD=0 prints nothing at all from LatchRunAheadFromCaps. The proof is a line present and a line absent: `Config: IPC ... run-ahead=0 ...` (ConfigLoader.cpp:459) with zero `run-ahead ARMED` - the shape p5f/magma-runahead.md already recorded for the RA=0 arm. E1 will exit non-zero, by design. --transport inproc adds --require-inproc, whose gate demands run-ahead=1 in the Config: IPC line (trace-replay-ci.sh:601), and the proof runs AFTER result.json is pulled and checked. So the SSIM and the archive are usable and the arm is verified by hand against the table above; dropping the --env to make it green would delete the experiment. E0's budget. 39 cases x 2 transports x 3 repeats is 234 replays, roughly 3-4 h, which does not fit beside the TCP matrix and CTS in one window. It is split into a one-pass census that produces the divergent-case list and a three-repeat pass over that list plus two controls - the determinism question only has meaning for cases that already diverge. CTS is flagged, not promised. No caselist is in-tree, and the local VK-GL-CTS checkout has an empty external/openglcts/data with no mustpass file anywhere, so the five blocks cannot be cut in-tree; the exact pre-window procedure to fetch a tagged checkout and commit tools/cts/caselists/p7-*.txt is written out instead. And KHR-GL46 is unproven on the Android runner - every device reference result in the skill is KHR-GL33, the 4.5 numbers are desktop lavapipe, and the platform hands the package's version straight to eglCreateContext - so the runbook carries a five-minute pre-window probe and an explicit integrator decision: if 4.6 is unreachable the baseline is taken on KHR-GL33 and the AFTER reading must use the same version, because 0.5 pp between two different API versions means nothing. |
||
|
|
3ba150d021 |
[tools, android-plugin, CI] (P7): the device matrix runner selects DirectVulkan, repeats are archived, and the spawn env parse is entry-aware
wave 0 item 5. Three instruments P7's device window needs, and the CI steps that
can make each of them red.
run_tcp_matrix.py --backend DirectVulkan end to end. --backend already existed and
already filtered by each case's ci_backends; what did not exist is a per-backend
GOLDEN. The two backends shared `golden` plus an optional `alternate_golden`, and
alternate_golden is an OR across backends, not a per-backend key - so a
DirectVulkan run that matched the DirectGLES picture passed, which is the "went
green on the wrong arm" shape the exit gate exists to catch. trace_cases.json
gains `backend_overrides.<backend>` ({golden, alternate_golden, ssim_threshold}
only: restating target_call or a crop would stop the two backends retracing the
same frame through the same window). A per-backend golden REPLACES the shared
pair, matching another backend's declared image is refused BY NAME, and a --case
named explicitly that does not run a requested backend is an error instead of one
plan fewer and no message. The CMake emitter and the APK matrix refuse a case that
declares the key rather than silently emitting one golden for both arms.
DirectGLES is unchanged and that is asserted, not asserted-in-prose: a case with no
override resolves to itself, and the key is dropped from the resolved case even for
the backend that overrides nothing - so giving DirectVulkan its own golden does not
move the DirectGLES identity fingerprint and does not invalidate its checkpoints.
Each results.json row now records the golden, alternate golden and threshold it was
scored against.
run_android_retrace_local.py gets --matrix (the CI split subset, 39 cases, each
running only its own ci_backends - not --all, which adds the rd12 workload ID-P7-4
excludes), --repeat N, and --archive-dir. The archive is the point: the result root
is keyed by case and backend and by NOTHING else, so repeat 2 overwrites repeat 1
and the only artefact that survives a three-repeat run is the last one, which makes
"were the three repeats the same picture?" unanswerable after the fact - in the one
place where re-running to find out is expensive. It also refuses --benchmark with a
non-monolith --transport, which has always produced a monolith run under a spawn
label (run_benchmark_case passes args.env alone; p6/README.md records the pitfall).
compare_actuals.py computes the pair result.json cannot: SSIM between two actual
PNGs, plus a case x repeat x ssim-vs-golden x ssim-vs-first-repeat table over an
archive. It is the retrace gate's OWN SSIM - a transcription of ComputeChannelSsim
/ ComputeRgbSsim (trace_replay_core.cpp:630/:671), the only SSIM in the tree - and
the crop comes from each repeat's own result.json, so the two columns describe the
same rectangle. PNG decode is stdlib zlib, because an unavailable Pillow in the
device window would turn "the repeats diverged" into "the tool did not run".
TraceReplayActivity: resolveSpawnServerPath's two decisions were String.contains()
on the whole joined K=V;K=V list and neither was entry-aware. The one that bit:
`--env MOBILEGL_IPC_SERVER_PATH` with no '=' is the documented UNSET spelling
(trace_env_overrides_test.cpp:92) and the only way to ask for "spawn with no image"
and watch LaunchServer refuse by name - it did not match "MOBILEGL_IPC_SERVER_PATH="
so a resolved path was appended AFTER it and, because ApplyEnvOverrides applies in
order with setenv(...,1), the deliberate unset was silently undone. That is exactly
the "silently replacing it would make that knob untestable" the method's own comment
forbids. Two more: the key inside another entry's VALUE flipped both decisions the
wrong way, and the transport match was case-sensitive while ConfigLoader lower-cases
the value before matching (ConfigLoader.cpp:322). The parse moves to
SpawnServerPath, a class with no android.* import, so test_android_lifecycle.py
compiles and runs it with plain javac - the parse had no host-side coverage at all,
and its on-device failure mode is "the knob had no effect".
Only spawn gets a path, deliberately: it is the only transport that execs an image.
Red-once, all four: a stub case whose result names the DirectGLES golden fails a
--backend DirectVulkan run with "matched a golden another backend declares"; a
synthetic PNG pair with a closed-form SSIM (identical = 1.0 exactly, constant
black vs white = C1/(255^2+C1), inverted halves = (C2-2v)/(2v+C2)); the pre-fix
resolveSpawnServerPath body fails eight of SpawnServerPathTest's checks; and the
DirectVulkan TCP proof battery is the DirectGLES one under the other prefix,
because an empty prefix selection raises nothing and reads as a clean pass.
CI: test_run_tcp_matrix.py and test_compare_actuals.py join the R-16 step. The
matrix driver's stub regressions had NO CI step - they were run by hand and
recorded in a note - and they guard the tool that decides whether a device golden
run passed.
Counts in WSL: test_run_tcp_matrix 12 -> 18, tcp_lane_tools 4 -> 6,
test_compare_actuals 12 new, test_android_lifecycle green with the new Java unit.
|
||
|
|
e19095269b |
[docs, MG_Remote] (P7): the roadmap rows say what the tree says, and CONTRACT-P7 is drafted
ROADMAP.md: the Ph / P3b-P4b / P7 rows carry the audit's verdicts and the factual
corrections it found - the @P7 refusal list is 15 sites in 4 files (the row printed 8),
the 29-commit DirectVulkan shape wave was never recorded, the device divergence is two
observations with no per-case attribution and no lockstep control, SetupDrawSnapshot /
content-addressed CSO are declined for this stage, "XFB scatter to client" is retired
in favour of the two real holes, readback/pack state splits into closed and open
halves, the NDK size pin is obsolete, PACK-PBO readback landed in
|
||
|
|
e8b2c4bd05 |
[docs, MG_Test] (P7): the Ph / P3b-P4b / P7 program plan, and the pull flavour builds its tests again
Plan for driving the roadmap to P7 close-out, from a per-item audit of the three
rows against
|
||
|
|
b95f5f1a20 | [docs] (P6.5): the first wave recorded as landed - review + fixes + representative re-validation, residual to CI | ||
|
|
fe28bdb5f3 | [MG_Remote, MG_Pipe, MG_Test, CI, tools, docs] (P6.5): the first wave completed and reviewed - archive v2, publication boundary, half-close, AcceptPair/role fixes, device re-validation | ||
|
|
018fab0f5b | [WIP] (MG_Remote): implement P6.5 TCP and stream transport | ||
|
|
242fd45cd7 | [Fix] (CI): read role logs and retry device disconnects | ||
|
|
9043442454 |
[MG_Test, CI] (P6): the last two readers the role split left behind
build-linux-verify could not compile PipeStatsTest: RoleDerivedJsonDumpPathForTesting is DISAGGREGATED-only and the verify flavour is deliberately monolith. The case name stays registered and GTEST_SKIPs there, matching the PIPE_PUSH pattern, so ctest -N still matches between flavours. runtime_mode_proof.py logs opened MOBILEGL_LOG_FILE_PATH as a file. After the split that name is a base and the sink writes <base>.client.log / <base>.server.log, so the inproc proof died on FileNotFoundError after a green lane. It now reads through split_log_paths' role helpers and names the paths it looked for when none exist. |
||
|
|
35b3283bd9 |
[docs] (P6): the closeout - gates 7 and 8 are measured and P6 is closed
The device gate: Redmi 2f7cbe2e, reboot-clean, paired A/B in one thermal window (arms interleaved, best-of-3 then best-of-2), rd12 + DirectGLES. inproc vs spawn client CPU p50 7.880/7.889 vs 7.876/7.869 (-0.15%) and 7.987/7.989 vs 7.992/8.009 (+0.16%) - a tie inside the 0.2-4.2% within- arm spread, sign flipping with arm order, recorded and not gated. monolith is ~30% dearer. 22/22 runner exits clean, 44/44 PINNED, zero Fatal. The three mandatory numbers: socket doorbell vs condvar is a tie too - client 121.1 waits/frame identical across transports, server 10 866-11 963 (inproc) vs 10 415-10 416 (spawn) waits/frame, and the contract's 'converting that into ~4 blocking reads may be cheaper' did not pay off; the only column where the socket is worse is clipark/f (6.07 vs 5.60/5.75). SEG_STAGE bytes/frame: 130.8 KB median / 8.01 MB steady (OpenRA), 816.2 KB / 1.53 MB (rd12). Records/frame post-chunking: 211/206 and 7 497/7 504. maxrec stays 1808 - the 8 MiB chunk budget is never reached. Both framings reported throughout (the startup window runs ~7x the steady state) rather than the flattering one. MEASUREMENTS.md gains section 12 with the full protocol and tables; the 6.3 'no post-chunking distribution exists' gap now points there. CURRENT_STAGE_PROGRESS.md marks gates 7/8 met and records the closeout work plus two structural limits (spawn client windowed fields are zero by construction - OnPresent runs in the server process - though its run totals are valid stores; the stats dump is role-derived). ROADMAP marks P6 closed in the stage table, the header and the milestone. Evidence: notes/p6/gate7-device-ab.md, gate8-doorbell-device.md, gate8-wire-counters.md, git-worktree-fix.md. check_doc_citations: 82 citations in the three documents, 0 problems. |
||
|
|
06b61bd84e |
[MG_Util, MG_Remote, tools] (P6): the wire counters and the spawn server's stats latch
Gate 8's last two numbers (SEG_STAGE bytes/frame, records/frame post- chunking) had no producer at all: the summary line's byte classes are backend-to-driver bytes and maxrec is a max, not a per-frame. Two push- guarded counters answer them at their single choke points - ByteClass:: StageSegmentBytes at PipeWireCodec's StageAllocate (the only allocation behind StageBytes, so every buffer walk, texture slab and CSO archive lands there and emitters cannot double-count) and CallClass::WireRecords on EncodeRecord's committed path (post-chunking an emitter no longer knows how many records its one call produced). A staged-blob histogram rides along. G1 holds: symbols 0/0/0/0, .text byte-identical, pull build carries no MG_Remote symbols. The spawn server never counted anything: PipeStats::Init() is a step of MobileGL::Initialize(), which ServerMain never reaches, so every Enabled() site in that process - including ServerLoop's own ServerWaits/ServerParks publish - stayed false and the server log carried no summary line at all (a zero that means 'another process' printed in the shape of one that means 'never waited' is the milder misreading; the line was simply absent). ServerMain now arms and closes PipeStats at its role boundary - after MG_ConfigLoader::Init (Init latches from the parsed config), after the server-role latch (the log sink's role), and before ::_exit(0). Red-once: 0 server summary lines before, 29 after, SSIM 1.0 unchanged. MOBILEGL_PIPE_STATS_FILE now derives per role through the log sink's exported RoleLogPath rule - run.client.json / run.server.json, the bare name resolves to no file - because both roles reach FormatJson() and ofstream trunc made the second writer replace the first. G1 caught three separate leaks on the way (a string temporary in the monolith body, one resized Shutdown() from unequal #if arms, two rewritten MGLOG_W literals in .rodata); the pull arm is statement-for-statement the original. Device tooling for the paired A/B lands with it: pin_device.sh gains a measured 2f7cbe2e entry (this board clamps the GPU at 1050 MHz - thermal_pwrlevel writes 0 and reads back 1, max_gpuclk is read-only; absolute times are not comparable with the 1100 MHz-era rows), and tools/device_bench/p6/ carries the session collector and renderer beside run_android_retrace_local.py's MOBILEGL_TRACE_PACKAGE override. Unit 2367/2367 (PipeStats 25/25), spawn integration 102/102. |
||
|
|
4e7bf0fe65 |
[docs] (P6.5): the design for the first wave - all-TCP transport, WSL client to adb-device server
ROADMAP gains the section "the immediate next item": a design, not an implementation, for running the frontend on an x86_64 WSL process and the backend on the Redmi over TCP, with the integration lane and the CI retrace subset as the acceptance. The P6.5 row now points at it. What the design rests on, verified in the tree: the control plane already treats the rendezvous as a NAME with two ordered connections (SocketTransport), ClientSession::StartOverSocket already separates "how the socket came" from the handshake, MGPBlobRef addresses kSegStage only through one ResolveOrFatal, the reply header and the event heads are already wire-shaped, LogLine exists in CtrlMsg with no producer, goldens are shared between desktop and device, and the trace APK can already exec libMobileGLServer.so from nativeLibraryDir. What it does not rest on, stated rather than assumed: ILink is declared and unused beyond LinkProgress, ShmLink does not exist, both sessions hold SessionSegments directly - the lk seam body from the end-state review was never built and is this wave's first package (lk2). The manifest has no INTERNET permission. The mechanism: StreamLink as mirrored private segments replicated by messages - same sizes both ends, same offsets, so the codec, the watermark rules, the apply thread and the encoder do not change; RingControl becomes Progress frames sent on flush-on-idle (the recorded amendment to "appliedSeq is never lazy": late by at most one flush, never early); replies become seq-stamped messages; the event ring is mirrored with the P5e blocking rule intact. Cross-architecture makes the PipeFields.def layout digest mandatory (wf). Server logs on another machine make LogLine forwarding mandatory (lf) so the 34 server-log readers stay unchanged. A --serve supervisor forks one child per session on the device, started by a foreground Service in the trace APK. Packages lk2 -> wf -> ct -> sl -> lf -> sv -> t65, each with red-once; the gate list, the mandatory numbers (kWaitReply per frame x RTT, SEG_STAGE bytes/frame against the link, PRESENT_CREDIT 1..3), the two-sided configuration runbook and the risks (RTT per reply row, single large uploads, AP isolation) are in the section. check_doc_citations: 74 citations, 0 problems. |
||
|
|
872370ba9d |
[docs, MG_Remote] (P6->P6.5): the end state is TCP across machines, and the transport stack has two axes
The end state is re-ruled: client and server may sit on different machines, OSes and architectures, joined over TCP. The AVF pVM / AF_VSOCK end state the P6 end-state review assumed is withdrawn (retraction at its head, form D added to its table, the vsock probe and both external unknowns struck). The transport stack becomes two independently selectable axes, negotiated in the handshake and mixable: control plane (ITransport: fork / unix:<path> / tcp://host:port) and data plane (ILink: ShmLink / StreamLink). On one machine control over TCP with data over shared segments is a legal pair; ShmLink brings its own AF_UNIX aux rendezvous for SCM_RIGHTS, so ITransport narrows to control frames. `auto` is decided by a successful segment delivery, never by inspecting addresses, and a fall-back is named. Nothing above Transport/ may branch on the link kind. ARCHITECTURE.md gains 11.9 for this; CONTRACT-P6.md gains 10.3. ROADMAP: P6.5 and Ph get rows (both were referenced by the contract and absent from the stage table); the IPC track becomes P6 -> P6.5 -> Ph -> P12 -> P9 -> P10 -> P11(same-machine arm). Every later row is reconciled against the tree: items already landed are struck (client vertex arrays, client-minted query handles, credit default 1, SEG_EVENT overflow mechanism), the unreachable P7 gate (`nm | grep glslang` on a server image that IS libMobileGL.so) is replaced by the 184-symbol ratchet share, fixed-width wire form moves from P7 to P6.5, P12 is cut down per the review (server-owned display, no Surface passing), and the debt table's stale claims are corrected: class C is down to two slots, not 15 and not 511; PACK-PBO and the 2 MiB reply cap are P9's, not P6's. Verified against the tree and recorded honestly: the contract's three hs rows (LinkTerms, Refuse, the fingerprint split) never landed - protocol.fbs has neither - so CURRENT_STAGE_PROGRESS.md's claim that S6 ran red is withdrawn and the rows move to P6.5; the 184-symbol ratchet gate does not exist in CI; the server process never initialises PipeStats, so gate 8's `srv=0 srvpark=0` is a structural zero (fix in progress in the tree, not in this commit). check_doc_citations.py: 159 citations, one pre-existing basename ambiguity. |
||
|
|
94c130d639 |
[MG_Util, MG_IntegrationTest, CI] (P6): the readers the role split left behind
The per-role log split renamed BOTH halves so that an un-updated reader would fail loudly instead of silently reading half a session. It worked: CI found four readers this way. This is them. - MG_Util/Debug/Log.h: the role vocabulary now exists in the PULL build too, header-only. It was wholly inside #if MOBILEGL_BUILD_DISAGGREGATED, so PipeInputsTest - which builds in the verify flavour - could not name ReadRoleLogs and took the whole build-linux-verify job down at compile time. Inline rather than out-of-line because of G1: nothing in the library calls these, so an inline definition emits code into the readers and NOTHING into libMobileGL.so. Verified: pull symbols 30570 -> 30570, 0 added / 0 removed, .text still 0xa52203, and zero role-log symbols in the pull library. - PipeStatsWindow.h: the harness copy of the naming rule is now flavour-aware. It derived `.client` in BOTH flavours, so in the verify build - one role, MOBILEGL_LOG_FILE_PATH written unchanged - it opened a file that never existed. The copy has to stay a copy: MG_Util's RoleLogPath is `t` (local) in the shipping .so this module links, so calling it does not link. Added LogMark/MarkLaneLog/ReadLaneLogSince: one offset PER ROLE, because a single scalar over the concatenation slides by whatever the other role wrote. - The four arming scenarios: they asserted on lines the BACKEND emits, and under inproc the backend runs on the apply thread - the server role - so the line lands in the server's log. Reading only the client's half reported the emulation unarmed, i.e. accused the product of a defect the reader invented. Four copies of the same private helper pair now delegate to the harness. - scripts/ci/magma_runahead_checks.py: it unlinked and read the base name as if it were a file, so the negative arm saw an empty log and could not prove the knob took. It now imports split_log_paths' derivation - the authoritative one - rather than becoming a third copy. Local verification (WSL, lk-split + c6-pull): - the 4 CI-red entries: 10/10 PASS under inproc, 10/10 under monolith - unit role-log readers: 510/510 PASS - magma_runahead_checks negative: run_ahead_0 EXPECTED_RED, restored GREEN - the 5 affected TUs compile clean with MOBILEGL_BUILD_DISAGGREGATED=0 - G1: 0/0/0/0, .text 0xa52203 Red-once is the CI run itself (35677991453), which failed for each of these reasons before the change. |
||
|
|
75dd0cb1ca |
[docs] (P6): the roadmap and the current-stage doc record P6 as landed-not-closed
All ten P6 packages (a6/c6/lk/so/sm/cp/hs/dl/st/t6) are on feat/disaggregated
through
|
||
|
|
77cbd17636 |
[MG_Remote] (P6/dl): record the Session::Fail funnel as landed in the contract
§5.2 gains a LANDED note: the three steps (the FatalFamilies.def vocabulary, the SessionFail funnel that byte-identically routes ~90 aborts through one place, and the SessionFault frame that names the family to the peer), and the one deliberate deviation from the prose - seq/op are not threaded through the 90 sites because the message already names the op, so the frame carries code, family and message rather than a field that is always zero. |
||
|
|
b46da7002c |
[MG_Remote] (P6/dl): a dying session names itself to the peer
Step three of CONTRACT-P6 5.2, the funnel's whole point: instead of the peer
reading a bare EOF and guessing, the session publishes a SessionFault frame
that names the family before it aborts.
THE WIRE CHANGE IS D2's, exactly: `table Fatal` appends `family: string`,
append-only, so an older peer still reads `code` and `message`. `code` stays
the coarse seven-value projection; `family` carries the full word.
SessionFail publishes it before the abort, on whichever session's control
plane is active - the client has a ClientSession, the server a ServerSession,
so under spawn the frame reaches the far side whichever role is dying. Under
inproc both exist and it goes to an in-process transport the aborting process
will never read, which is harmless and cheaper than a special case that would
then be the untested one. Best-effort by nature: this thread is about to die,
so a send that blocks or fails changes nothing.
seq/op from the prose are NOT threaded through the 90 sites: the message
already names the op where it matters, so the frame carries what it can name
honestly (code, family, message) rather than a field that is always zero.
The client recognizes it: RunRemoteSurfaceControlFrame's reply loop, which
already demuxes CapsSnapshot from SurfaceReply, gains a Fatal arm that logs the
family and detail, latches device-lost from it, and returns transport-closed.
A server that DETECTS a fatal condition and aborts itself now tells the client
why; a kill -9 still bypasses abort entirely and is caught by dl's hangup
witness instead - the two cover the two ways a server can die.
The census learns SessionFail: a SessionFail call, like a WireLogFatal call,
must carry a Fatal{ word in its string, so the enum and the string cannot
disagree. FatalFunnel.{h,cpp} join the funnel exemption. Baseline is 4 aborts.
FatalFamilyTest gains the wire round-trip: every family's frame carries its
word and its projected code across encode/verify/decode.
G1 re-measured after the schema change: 0 symbol diff, .text unchanged at
0xa52203. Unit 2359 (one pre-existing environmental failure), integration-split
186/186, integration-spawn 102/102, retrace 6/6, census 0 unmarked.
|
||
|
|
4bb4d4fa4f |
[MG_Remote] (P6/dl): every session death goes through one funnel
Step two of CONTRACT-P6 5.2. The ~90 scattered `MGLOG_F("...Fatal{...}"); std::abort();`
pairs under MG_Remote/ become one call each to SessionFail(MGFatalFamily, fmt, ...),
which logs, echoes to stderr, counts, and aborts in one place. a6's census
found no single chokepoint existed; now there is one, and step three hangs the
SessionFault-to-peer publish off it.
THE MESSAGE STRING IS PASSED VERBATIM. The contract requires the log lines, the
CI greps and every red-once byte-identical, so SessionFail does not compose the
line from the family - it takes the SAME string the site handed MGLOG_F and
forwards it unchanged. The family enum rides alongside, redundantly, so the
funnel has the coarse wire FatalCode without parsing the string. The census now
checks that redundancy the way it checks WireLogFatal: every SessionFail call's
string must carry a Fatal{ word, so the enum and the string cannot silently
disagree.
The conversion was mechanical - 89 sites by a transform that injects the family
as MGLOG_F's new first argument and drops the trailing abort - with three sites
fixed by hand that the transform would have broken: a stub macro, and two
if/else-then-shared-abort blocks (PersistentMapTracker's adopt-tier and
SurfaceOpCodec's three surface-error branches) where a naive rewrite stole the
abort from the earlier branches and would have let them fall through. The
death tests are the safety net for byte-identity, and every one stayed green.
Four std::abort() remain under MG_Remote/, all inside the two funnels
(WireLog.cpp, FatalFunnel.cpp); the census exempts both files because their
callers carry the family word, and its baseline drops from 93 aborts to 4.
SessionFaultCount() is the one telemetry point S8 will assert zero over a good
run.
G1 re-measured: 0 symbol diff, .text unchanged at 0xa52203 (FatalFunnel and
FatalFamily live under MG_Remote/, compiled only in the disaggregated build).
Unit 2358 (one pre-existing environmental failure), integration-split 186/186,
integration-spawn 102/102, retrace 6/6, census 0 unmarked.
|
||
|
|
3a12a48add |
[MG_Remote] (P6/dl): the Fatal-family vocabulary as a table, projected onto the wire's seven codes
CONTRACT-P6 5.2's D2 ruling, step one of the Session::Fail funnel: the internal vocabulary before the funnel that routes through it. a6 censused thirty family words against a seven-value wire FatalCode, so Session::Fail(FatalCode, detail) cannot carry the vocabulary in its first argument - the word would collapse to one of seven and the log would say ProtocolCorruption where the code said UnmigratedVerb. FatalFamilies.def is the internal enum; the wire keeps FatalCode; each row carries the coarse code the PEER is told when that family ends the session. THE PROJECTION IS A TOTAL TABLE, NOT A SWITCH WITH A DEFAULT. This build has no -Werror anywhere, so a default arm would silently swallow a family added without a code. A row IS the arm; a family with no row is not a value of the enum. FatalFamily.h generates the enum, the projection and the name from the one .def, so a family, its word and its code cannot drift apart. The buckets are documented per row in the .def: ProtocolCorruption for malformed records / unimplemented verbs / role invariants / reply-contract breaks; RingOverrun for a ring or event channel overrun; SegmentMismatch for a segment too small for what a role must place in it; AbiMismatch for layout or version disagreement; ServerCrashed for the liveness ends a WAITING peer attributes to the other side. DeviceLost gets no row on purpose - it is armed from the doorbell (5.3), never raised as a Fatal, and a test asserts no family projects onto it so an abort can never masquerade as a device loss. Five tests, all walking the table so a new row is covered without editing them: count agreement, every family projects to a non-None code, none projects onto DeviceLost, names distinct and non-empty, and a handful of anchors pinned by meaning so a bulk-edit drift is caught. No call site changes yet; the funnel that uses this is the next step. |
||
|
|
5fb54f03fe |
[MG_Util, MG_Test, MG_IntegrationTest, CI] (P6): one log per role, and every reader moved with it
The spawn work had the two roles append to one MOBILEGL_LOG_FILE_PATH under O_APPEND. It was correct - two processes at one offset would corrupt the file, O_APPEND made each write atomic - but it put two sessions' lines in one place and made every per-side assertion a search. This splits the sink: the client role writes <base>.client.log and the server role writes <base>.server.log. THE BASE NAME NAMES NO FILE ANY MORE, AND THAT IS THE POINT. Leaving the client at the old unsuffixed path would have been the compatible choice and the wrong one: a reader that was not updated would go on finding a file, go on passing, and go on seeing only half the session - and a refusal census that reports a confident zero for the half it cannot see is worse than one that fails. With both names moved, an un-updated reader gets "file not found" and says so. There were 34 such readers; every one is moved here. THE ROLE IS PER-THREAD, not per-process, because under inproc both roles are threads of one process. Log.cpp keys the sink on a thread-local role: the apply thread declares itself server (ServerLoop.cpp), a spawned image is server for its whole life (MOBILEGL_IPC_ROLE=server, read once), and everything else is the client. The naming rule (RoleLogPath) and the two whole-log operations (ReadRoleLogs / TruncateRoleLogs) are EXPORTED from the library, so no reader reimplements the derivation - a second copy is the copy that falls behind. Readers, by how they read: - unit mains set MOBILEGL_LOG_FILE_PATH and read a role file: they keep the base in the env and derive the client path from the library helper. - the death-test child runners (ReadLog) now concatenate BOTH roles: a death test asserts the CHILD said something, and the applier's refusals - the very diagnostics these check - are written under the server role. - the offset-based Emit readers derive the client path once after setenv. - PipeStatsWindow reads both, because draws/vbs increment in the backend's PrepareForDraw - the apply thread, the server role - so the summary line is in the server file even under inproc. - run_trace_case.cmake's split census reads both files; the client keeps the transport markers. - split_log_paths.py (and its self-test, and the control stub) derive both role files from the declared base. - the Android script pulls both role files off the device and concatenates them, so every downstream check is unchanged - and it judges a role file by CONTENT, because `run-as test -f` through `exec-out` returned 0 for a missing file on the device and `exec-out` folds cat's own not-found line into stdout. The monolith arm legitimately has no server file, and that is a silent skip rather than a one-line server log. Guarded so a pull build is untouched: RoleLogPath and the role sink are wholly inside MOBILEGL_BUILD_DISAGGREGATED; a non-disaggregated build writes the base path exactly as before. G1 re-measured - 0 symbol diff, .text unchanged at 0xa52203. Verified on device 2f7cbe2e: monolith writes only the client log (no phantom server log), inproc and spawn each write both and pass their transport proofs (spawn: server role in pid 16125). Unit 2353 (one pre-existing environmental failure), integration-split 186/186, integration-spawn 102/102, retrace 6/6, control smoke 12/12, split-log-paths self-test 9/9. |
||
|
|
15f5b54bbd |
[MG_IntegrationTest, CI] (P6/t6): integration-spawn, and the set identity made structural instead of maintained
Exit gates 9.2 and 9.3. Until now nothing ran the integration suite across a
process boundary at all - the spawn shape was proven only by the retrace lane,
which exercises one trace per case and no scenario assertions.
integration-spawn: 102 cases, 102 green.
THE SET IDENTITY IS THE POINT, AND IT IS NOT MAINTAINED BY HAND. 9.2 asks that
the spawn lane's case-name set equal the split lane's. Registering the spawn
arm as a second copy of each block would satisfy that today and drift the first
time somebody adds a scenario to one of them - and a spawn lane covering LESS
goes green while proving less, with nothing about a smaller green lane looking
wrong. The eleven split-environment blocks become eleven calls to one macro
that emits BOTH arms from one filter, so a case added to either is added to
both or to neither.
scripts/ci/spawn_lane_parity.py asserts it anyway, because a macro is one edit
from a hand-written block. It compares SETS rather than counts - a count can
agree while the membership does not - and carries exactly one sanctioned
asymmetry, named in the file: integration-split also labels the PUSH-MONOLITH
arm, whose scenario is monolith-only by construction. R-16: introduce a
split-only block and it reds naming the case and the macro to use.
THREE CASES SKIP UNDER SPAWN, BY NAME. CtWireScenario reads PipeApplier's
in-process tallies through ServerVerbs(); under spawn those live in the SERVER's
process and this one's copy is 0 forever, so the assertions were failing about
somebody else's memory. That is a harness limit, not a product defect - the
records themselves cross, which the retrace lane proves. They are SKIPPED and
not deregistered: a case dropped from the registration would make the two sets
differ silently, which is the drift the macro exists to prevent. What retires
the skip is cross-process telemetry, which is also what S8's SessionFaultCount
needs.
TWO REAL REGRESSIONS THE EXISTING GATES CAUGHT, both from this refactor:
* the macro dropped the per-block TEST_LIST variables, which were what
SplitLogPaths fed to give every entry a PRIVATE MOBILEGL_LOG_FILE_PATH.
SplitLogPaths.PrivateAndDistinct went red naming the first orphan. The
macro accumulates list NAMES now, so a block added later is covered without
anyone remembering to extend a list - and the names have to be unique,
because gtest_discover_tests' TEST_LIST is populated when CTEST runs, not
at configure time, so one shared variable held only the last block's
entries. That matters more on the spawn arm: there TWO PROCESSES append to
the path, so a shared file interleaves two sessions rather than merely
being ambiguous.
* the configure_file that substitutes those names ran BEFORE the F1 and Ct
blocks registered, so it captured neither. Same gate, naming
F1WireScenario.ClearBufferfiPixels. It now runs last.
junit_tally gains --require-spawn-ran beside --require-split-ran: the prefix IS
the arm, and a shared flag would let one lane tally the other's entries and call
itself run. Verified to discriminate - the spawn junit reads 96/0/5 under its
own flag and 0/0/0 under the split flag, which correctly fails.
Two death tests follow the ADOPT_TIER message the census commit retagged with
Fatal{UnimplementedAdoptTier}; their regexes asserted the old wording.
Unit 2353 (one pre-existing environmental failure), integration-split 186/186,
integration-spawn 102/102, retrace 6/6, fatal census 0 unmarked, G1 0 symbols
and .text unchanged at 0xa52203.
|
||
|
|
3399b8791c |
[MG_Remote, CI] (P6/dl): the Fatal census gate, and the one death under MG_Remote that named nothing
Exit gate S7, and the precondition for CONTRACT-P6 5.2's funnel rather than
its replacement.
S7 says "a bare abort() added outside Session::Fail -> the census gate goes
red". Session::Fail does not exist: it is the 93-site refactor 5.2 describes,
and it is not in this commit. What IS checkable today is the property that
funnel exists to guarantee and that every consumer already depends on - every
death under MG_Remote/ names a FAMILY, in a Fatal{Word} marker a grep can
find. run_trace_case.cmake reds a split retrace on any `Fatal{` line, the
retrace lane's refusal census counts them by name, and MEASUREMENTS quotes
the distinct words. A death with no word is invisible to all three.
TWO RULES, BECAUSE ONE WOULD LAUNDER THE OTHER. An abort must carry a marker
within twelve lines; and every WireLogFatal call must carry one in its format
string. WireLog.cpp's own abort is exempt from the first rule ONLY because its
five callers satisfy the second - a funnel that accepted an unmarked string
would be a hole shaped exactly like the rule.
THE BASELINE BOUNDS SILENCE, NOT DEATHS. Abort sites may grow freely; a new
family word may not appear unrecorded. Freezing the total would make every new
guard a gate change, which is how a gate stops being run. The wire's FatalCode
has seven values and the internal vocabulary is thirty-two, which is the
divergence D2 exists to bound - so growing it has to be an act, not an
accident.
THE CENSUS ALSO CORRECTS ITSELF. Its first run reported WireLog.h's own PROSE
as an unmarked death: the comment explaining why the stderr echo exists
contains the words std::abort(). So the scanner strips comments before
matching, with a character-level pass rather than a regex - a `"http://"`
inside a string literal is not a comment - while KEEPING string literals,
because the markers it looks for live in them. Real numbers after that fix:
93 abort sites, 32 family words, 0 unmarked. a6 had counted 92 and 30 from a
scan that did not make the distinction.
AND THE ONE REAL FINDING. a6 named two aborts with no marker at all;
WireLog.cpp's is the sanctioned funnel, so the genuine one is
PersistentMapTracker's invalid-MOBILEGL_IPC_ADOPT_TIER death. It now carries
Fatal{UnimplementedAdoptTier}, which is what makes the census zero.
R-16 both ways, run: a bare std::abort() reds it naming the file and line, and
a new family word reds it naming the word and how to record it.
|
||
|
|
e3bb6d8b82 |
[MG_Pipe, MG_Backend, MG_Impl] (P6/sm, P6/st): four guards that were compiled in and could never arm, and the crash behind one of them
CONTRACT-P6 3.2 (D1c), 3.3 (D13) and 3.4 (D10).
Every guard below asked a question a SERVER PROCESS answers wrongly - "is a
ClientSession active" (never, there) or "am I on the apply thread" (every
thread is, there) - so all four were present, compiled, and permanently
disarmed in the one shape they exist for. That is worse than absent: it reads
as coverage.
D13 FIRST, THEN THE WIDENING. MGPipeRoleSplitActive() meant "the REHEARSAL
knob is on AND the transport is real", so widening it in place would have left
a narrow name describing a wide predicate. It is renamed to
MGPipeRoleSplitRehearsalActive() across its 12 sites - including the
DualBlockScenario assertion message that embeds it - and the wider predicate
is introduced BESIDE it.
MGPipeBlocksAreDistinct() IS TRUE FOR TWO DIFFERENT REASONS, which is the
whole point: under the rehearsal the two blocks are distinct objects in one
process; under SPAWN they are distinct because they are in different ADDRESS
SPACES, and no knob is involved. The spawn retrace runs with
role-split-state=0 - measured off the lane's own log - so every guard gated on
the rehearsal was off there.
THE CRASH. MGPipeServerBlockNoteIdentity early-returned on the rehearsal, so a
spawn server's gPipeInputs.ContextIdentity() stayed nullptr for the life of the
process. DirectGLES' framebuffer binding-slot memo compares that identity FIRST
and with NO generation, against a cache variable that also starts nullptr - so
nullptr == nullptr reads as a CACHE HIT and the function returns a slot pointer
nothing ever filled. `sm` widens the gate; `st` lands the named Fatal
PipeInputs.h had already asked for, so a reopening says which memo and which
identity instead of corrupting memory three frames later. Guarded on
MOBILEGL_PIPE_PUSH: in a pull build that identity is the live GLContext's
address and is never null, so the test would be dead code and G1 measures that.
DISJUNCTS, NOT SUBSTITUTIONS - and this was measured the hard way. The first
attempt REPLACED OnApplyThread() with MGPipeServerArm() and
ClientSession::Active() with MGPipeSessionLive(). 14 RemoteGuards and
RemoteClientControls cases went green by omission: a ServerLoop fixture with no
client session has only the original facts, so swapping them out disarmed every
test that drives the apply thread directly. The originals stay; the new
predicates are added beside them.
AND THE PROBE HAD TO BE THE RIGHT ONE. Installing
ClientSession::ApplyThreadIsInsideApplier as the apply-thread probe made every
GL-thread call a role violation whenever the applier happened to be busy - the
inproc retrace died on Fatal{RoleViolation, "MGPipeSlots"} raised from the GL
thread. "Am I the apply thread" and "is the apply thread inside the applier"
are different questions; the second is R-1's mutual-exclusion probe and is
about another thread entirely. ServerLoop::OnApplyThread is the one the guards
mean.
Three of D1c's six predicates land here, the three the four guards need. The
other three (MGPipeSplitActive, MGPipeShouldDial, MGPipeBackendIsLocal) have no
D10 site and are not added as dead code; their call sites are ContextEpoch and
ActiveXfbState, which this commit does not touch.
D14 holds: the non-disaggregated build gets constexpr false definitions, not
runtime-false predicates, so every consult folds at compile time. G1
re-measured - 0 symbol diff, .text unchanged at 0xa52203.
Two test names added, both falsifiable by construction: the spawn case asserts
the blocks are distinct with the rehearsal knob OFF, and the monolith case
asserts the predicate is not simply a constant. Unit suite 2353 tests, one
pre-existing environmental failure. All six OpenRA arms green.
|
||
|
|
90f9a79058 |
[MG_Remote] (P6/hs): the build stamp that was silently empty, and two asymmetries the handshake never checked
CONTRACT-P6 4.2 and 4.3. Three defects, all of them things the handshake
believed it was checking and was not.
1. THE BUILD STAMP WAS "" AND NOTHING SAID SO. CMakeLists took the git hash
through an execute_process with NO RESULT_VARIABLE, so a failed
`git rev-parse` yielded the empty string. Verified live rather than
inferred: this worktree's `.git` is a file holding a WINDOWS path, WSL's
git answers `fatal: not a git repository`, result 128 - and every WSL build
mixed "" into its ABI fingerprint. Two builds from different commits then
agreed exactly, which is the one thing the stamp exists to prevent. The
already-failing SessionHandshakeTest was reporting this the whole time.
The fix may NOT touch GIT_COMMIT_HASH_SHORT: it is baked into
glGetString(GL_VERSION), so redefining it changes an application-visible GL
string and the pull build's .rodata - a behaviour leak and a G1 break at
once. MGGitHash.h.in gains a SECOND pair instead, consumed only by the
fingerprint, plus -DMOBILEGL_BUILD_STAMP for trees git cannot read (CI has
${GITHUB_SHA} in hand). A stampless build is now a configure-time WARNING
naming the consequence, not silence.
MOBILEGL_BUILD_STAMP_PRESENT IS MIXED AS ITS OWN FIELD. "this build cannot
name its commit" and "this build's commit is the empty string" are different
claims, and collapsing them is how every stampless build in the world agreed
with every other. The test asserts the flag moves the fingerprint in EITHER
build state, and the `BuildStamp = ""` perturbation is now guarded - in a
stampless build production's value already IS "", so that assertion was
passing while proving nothing.
2. THE CLIENT NEVER READ abiMajor/abiMinor. Hello has carried them since P5
and the SERVER checks them; the client compared only the fingerprint. The
fingerprint mixes AbiVersion so a mismatch would fire anyway - but as
"struct shapes", which sends the reader looking for a layout change that is
not there. Under spawn the peer is a SEPARATE IMAGE the operator chose with
MOBILEGL_IPC_SERVER_PATH, so this is exactly the failure that needs its own
sentence. Both handshake sites call one helper; there are two sites, and
only one of them was reachable before `sm`, which is how the gap survived.
3. Hello::pid WAS 0 AND Welcome::serverPid ECHOED IT. So both were always 0 and
neither named anything, while §9.5's arm-proof gate wants exactly the child
pid. Both now carry real ids, and the pair is the cheapest arm proof in the
protocol: under spawn they MUST differ, and a same-process session cannot
produce two different values. The client keeps what the server STATED -
local launcher knowledge is a different fact from one that crossed the wire,
and only the second can be asserted from the server's own mouth.
R-16 both ways: revert serverPid to echoing hello->pid() and the new test reds
on both halves, naming 4.3. G1 re-measured after the CMake and header changes:
0 symbol diff, .text unchanged at 0xa52203.
Unit suite goes from two long-standing failures to one - the remaining
PipeCatalogue.FrontendNeverTakesAnApplierAddress is an environmental path
problem in this tree, unrelated. All six OpenRA arms still green.
|
||
|
|
8b13d8a84c |
[MG_Remote, MG_Impl] (P6/dl): the device-lost latch, taken from a descriptor and never from a deadline
Before this, CONTRACT-P6 5.3's own sentence held: "a dead server leaves the
client silently DECLINE-ing every verb forever with glGetError clean".
FatalCode::DeviceLost had zero producers and zero consumers and
glGetGraphicsResetStatus returned GL_NO_ERROR unconditionally, so an
application had no way to learn that the process holding its GPU objects was
gone. Measured cost of that: a server killed on its first Clear left the
client waiting out the full 120 s barrier and reporting Fatal{BarrierTimeout}
- the wrong diagnosis, two minutes late. It now notices in 0.01 s.
THE MACHINERY WAS ALREADY THERE. Doorbell::Wait breaks out on Dead(),
SessionProducer::Park turns that into SessionWait::ShutDown, and ClientSession
already had the named DECLINED path. What did not exist was the death FACT:
D5c's finding is that the client parks on clientBell[0] and rings ITSELF
through clientBell[1], holding both ends, so EOF can never arrive on the one
descriptor it watches. A bell that can wake itself cannot hear a death.
SetDeathWitness(fd) fixes that with no new descriptor: the control socket's
far end is held only by the server. The subtlety is that the bell must watch a
socket it must NEVER read - the transport's reassembler owns those bytes and a
control reply is not a death. So the witness enters the poll set with
`events = POLLRDHUP` and nothing else: POLLHUP/POLLERR/POLLNVAL arrive in
revents unrequested, so the hangup is seen while POLLIN never is. Nothing is
recv'd from it and its ownership stays with SocketTransport.
Dead() AND PeerHungUp() ARE TWO FACTS. Park sets m_dead for POLLERR, POLLNVAL
and unrecognised revents too - each a fault in THIS process's descriptor - and
CondVarDoorbell::Kill() sets it on every orderly Stop(). A latch taken from
Dead() would report a lost GPU for a local fd bug and a context reset for
every clean exit, so only PeerHungUp() arms it.
GL_UNKNOWN_CONTEXT_RESET, not GUILTY or INNOCENT: those assign blame, and the
server died for a reason that never crossed the wire.
D5b, in the control-reply wait, which had the right comment and no code: EOF
on the control stream is device loss; a TIMEOUT with the socket still open is
"ALIVE BUT SILENT" and says so by name. 5.4 forbids calling a frozen or merely
slow peer dead, and under P5e run-ahead a server one frame behind is the
INTENDED steady state - a latch armed by a deadline would fire on a healthy
session under load.
MOBILEGL_IPC_RESPAWN is now a NAMED REFUSAL rather than a variable nothing
reads. The latch is one-way on purpose: every handle this client minted names
an object inside the server process, so recovery means re-pushing the world.
THREE TEST NAMES ADDED, none renamed (G14), and R-16 run both ways:
* remove the witness -> AKilledServerLatchesDeviceLostFromTheHangupAndNot-
ADeadline goes red, takes 2 s instead of 0.01 s, and names D5c.
* collapse PeerHungUp() into Dead() -> ADeadBellIsNotAlwaysAHungUpPeer goes
red naming the local-fd-bug consequence.
* AnOrderlyStopIsNotADeviceLoss stays GREEN under that collapse, and its
comment now says so. The first draft claimed it was the control for it;
running the falsification proved otherwise - under spawn the self bell is a
SocketDoorbell and Stop() never kills one. The comment was corrected rather
than the claim quietly kept.
G1 RE-MEASURED, NOT ASSUMED: 0 symbol diff against the c6 baseline and .text
unchanged at 0xa52203. That measurement earned its keep - it caught a 16-byte
drift that had nothing to do with dl: P6's O_APPEND log-sink fix was compiling
into the PULL build, where a second process can never exist. Now guarded, and
.text is back to the baseline exactly.
Full unit suite: 2350 tests, the same two pre-existing environmental failures
and no others. All six OpenRA arms (2 backends x monolith/inproc/spawn) green.
|
||
|
|
d83fc6643a |
[CI, android-plugin] (P6): CI starts a second MobileGL process on a device
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".
|
||
|
|
7c8120cc49 |
[MG_Remote, android-plugin] (P6): a real phone retraces a real trace across two processes
OpenRA.DirectGLES, transport=spawn, on a Redmi: ssim=1.000000,
mismatchPixels=0, zero Fatal{ lines, three runs for three. The server is a
process the client launched out of the APK's lib/arm64/, and the client's own
log names its pid.
Four things stood between the desktop green and the device green, and every
one of them was invisible on desktop for the same reason: a build tree is not
a deployment, and Linux is not Android.
1. THE SERVER COULD NOT LINK. It links libMobileGL.so and is EXEC'd, not
dlopen'd, so it gets a fresh linker namespace with the DEFAULT search path
- and an APK's lib/<abi>/ is not on it. `CANNOT LINK EXECUTABLE ...:
library "libMobileGL.so" not found: needed by main executable`, before a
single line of MobileGL ran. On desktop CMake had been giving the target an
absolute build-tree RPATH, so the WSL server resolved its dependency
through a path no phone has. $ORIGIN is asked for via BUILD_RPATH (a raw
-Wl,-rpath,$ORIGIN reaches the generator unquoted and the SHELL eats it -
the device linker said `normalize_path - invalid input: "$"`), and because
the NDK drops RPATH from the link anyway, the LAUNCHER states
LD_LIBRARY_PATH: it is the one component that has already resolved where
the image lives, and a pair that is not co-located is not a pair.
2. THE RENDEZVOUS NEEDED A WRITABLE DIRECTORY. An Android app has no /tmp, so
bind() in the child had nowhere to put the node and the client refused by
name 20 s later. AF_UNIX endpoints now accept the ABSTRACT namespace with a
leading '@' - the systemd/D-Bus convention - which has no filesystem
presence at all: no directory, no mode bits, and no stale node to unlink
after a crash, which is the bind() failure Listen() has been working
around. Its reach is the network namespace, which for an app is the app.
StartSpawned mints '@' names; an explicit path still works everywhere, and
that is what P12's "connect to something somebody else is listening on"
will use.
3. PBUFFER WAS A DirectGLES-ONLY KNOB. Twice over: trace-replay-ci.sh gated
`--ez use_pbuffer true` on the backend name, and the JNI demanded a native
window for DirectVulkan unconditionally. A surface SHAPE is not a backend
property, and the coupling mattered because the Android GLWS makes a window
surface whenever gNativeWindow is non-null - so handing Magma a window was
the same as asking for one. Until P12 a spawned server cannot be given a
window at all: an ANativeWindow* is a pointer into the CLIENT's process and
SetWindowHandle is refused on the way across with Fatal{UnmigratedSurface,
"AndroidNativeWindow@P12"} (Rule H). Which is exactly what happened, one op
after the server had come up green.
4. THE RUNNER LOOKED IN ONE OF THE TWO OUTPUT DIRECTORIES. assembleTraceRelease
writes apk/trace/release; CI has staged the same artifact under
apk/trace/debug. A tree that has built locally has both, and picking the
wrong one retraces last month's APK and says nothing about it. Newest of
either, now.
MEASURED AND NOT FIXED: DirectVulkan on this device diverges under BOTH split
transports and does so nondeterministically - inproc scored ssim=0.988998 /
9836 px, spawn scored 0.976494 / 14658 px and then 0.988998 / 9836 px on a
repeat, while monolith with the same pbuffer surface scores 1.000000 / 0 px.
The two transports draw from the same set of outcomes, so this is the split
path on Magma against a real GPU, not the process boundary: P6 neither caused
it nor can fix it. DirectVulkan spawn IS green on desktop (lavapipe, 1.000000).
G14: one test name added, AnAbstractEndpointNeedsNoWritableDirectoryAndLeaves-
NoFile, which pins both halves - the handshake crosses an abstract name, and
nothing is left on the filesystem. A '@' mistaken for a path would have bound
a file of that literal name, passed here, and failed on the one platform this
exists for.
|