ILink::ReadReply takes a SEQ (ILink.h:285) and ShmLink implements it against a
ReplySlotPool, so any answer the pool still holds is readable. StreamLink kept ONE
reply - replySeq/replyStatus/reply - and then asked the wrong question of it:
x.replyReady.wait(lock, [&]{ return closed || x.replySeq >= seq || appliedSeq >= seq; });
if (x.replySeq != seq) return ... MOBILEGL_ERR_PROTOCOL_MISMATCH;
The wait is written as "at least this seq"; the check is "exactly this seq". Every
answer older than the newest was therefore unreadable, which nothing noticed because
every caller read the answer it had just waited for - i.e. always the newest one. It
is the transport half of the P12 create window's failure: the window defers up to
four answers and collects them in order, and this link could not hand one back.
THE RING, AT THE POOL'S OWN DEPTH. kReplyRing = kDefaultReplySlotCount (8), because
that is the pool this link stands in for: an answer a reader has not taken by the time
eight newer ones have arrived is one the shm arm has already overwritten too, so the
two transports now lose the same answers and no more. Vectors are sized per reply, so
an idle link pays eight empty ones.
This does NOT make the create window work - on the device it changed nothing, because
the pool itself is 8 slots against 59,673 replies in one frame (see the next commit).
It is kept because the two transports disagreeing about ILink's contract is a bug in
its own right, and because the single slot silently failed any read that raced a newer
reply.
Gate: StreamLinkTest.AnOlderReplyIsStillReadableByItsSeq posts three replies and reads
them oldest-first - the order the window's drain takes them in.
- The predicate is one function again: MGPipeResourceRespecifyWantsItsReply (PipeRoute.h) is read
by the wire emitter, so the two halves of this row cannot disagree about whether an answer is
owed. The buffer half never owes one (both call sites discard the Bool, PipeFill.cpp:1105/:1142);
the texture half owes one exactly until MGPipeHandleIsPublished, and that latch is set only by an
ACCEPTED create - which EmitResourceRespecify already sends, blocking, when it is clear
(TextureEmit.h:869-879). So the object pays once, at first use, and every record after it is
fire-and-forget.
- MEASURED on the device, same frame, same 101,421,396 B staged, stock power policy: waits
25,154 -> 4,543 and wall 130.9 s -> 31.3 s. The op split is the proof of what changed and what
did not: waits by_op 3=20611 -> absent (only 2=4536, 5=6, 9=1 remain), while the RECORDS sent
are unchanged - frame-sent still shows 3=20633 and 103,094 records total. The round trips went
away; the work did not.
- It also lands on an independent prediction: an audit run beside this work (mgl-device/scratch/
scheme-review.md) computed 4,543 remaining waits and a 24-37 s credible range (midpoint ~30 s)
from the archived logs, a host-inproc round trip of 35.4 us and a measured 43.1 MiB/s link.
Measured: 31.3 s, at the midpoint.
- Cumulative on that frame since this work started: 43,232 waits / 221.5 s -> 4,543 / 31.3 s (7.1x).
What is left is the create row (4,536 waits, ~23 s) - the confirmation itself, not waste - plus
hard floors of ~13.4 s client CPU and ~2.2 s of transfer. Going below that means batching the
create row, which keeps real answers.
- TWO DEBTS, STATED: (1) the invalidation guard for the one silent-loss case is NOT wired; the
audit confirmed independently that no production path can drop a texture record today, and if
one appears the notice it needs is protocol-surface (a tenth reverse callback). (2) No SSIM or
visual check has been taken since these rows stopped asking - frame-sent agreement is strong
evidence but it is not a picture.
- Two stale controls in older cases were fixed rather than left: the item-B gate now names the
CREATE row (its old total-based control only moved because a respecify was still waiting), and
the by-op measurement asserts records-sent for the rows that deliberately stopped waiting.
RemoteClientTest 169/169; red-once for the predicate is forcing it back to "always wait".
- The two halves of this row are not the same row. PipeFill.cpp:1105 and :1142 both call
MGPipeRouteResourceRespecify(desc, nullptr) and drop the Bool, so for a buffer the wait bought a
value that was thrown away. It goes fire-and-forget with the provisional-accept shape
resource_subdata has had since item B.
- The texture half keeps its answer ON PURPOSE: there the Bool is the self-heal trigger AND the
gate on the acceptance mirror, and TextureEmit.h:885-888 (ID-18 M3) says advancing that mirror on
an unconfirmed answer is how the next identical call gets suppressed "against a record that was
never stored". That half needs the provisional/really-accepted distinction plumbed through the
reply mailbox before it can stop waiting; it is not a flag flip.
- MEASURED, same frame, same 101,421,396 B staged: waits 25,176 -> 25,154 and by_op 3=20633 ->
3=20611. So the buffer half is worth 22 waits (0.1%) and the remaining 20,611 are texture
respecifies - which is what says the next step is the mirror design, not another flag. Wall
134.4 s -> 130.9 s.
- The ERROR-is-not-a-decline case moves to the texture half: a row that reads no answer cannot fold
ERROR into a decision, so the rule it exists for belongs to the half that still waits (the
transport fault is caught at the next row that does). New case
ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes pins both directions; red-once with the
branch removed is exit 155. RemoteClientTest 169/169.
- The device run put 18,056 of one atlas frame's 43,232 round trips on this row, at ~5 ms each, for
records carrying a few hundred bytes. The acceptance it waited for says exactly one thing - "the
applier holds a record for this handle" - and the client already keeps its own copy of that fact
in MGPipeHandleIsPublished, which is set when the create was ACCEPTED and is never cleared while
the record lives (the applier keeps texture records across a make-current, PipeFill.cpp:3756-3759).
The wait was the client asking the server a question it had already answered.
- SO THE CONFIRMATION MOVES TO THE FIRST USE. EmitTextureParams now checks the latch BEFORE it
sends: an unpublished object gets HealUnpublishedRecord - the same create-with-no-storage, the
same storage respecify, the same one re-send the refusal branch has always run, extracted so both
callers share it - and that create is the ONE blocking record the object owes. Every parameter
record after it is emitted fire-and-forget and the caller reads a provisional accept, exactly as
resource_subdata has done since item B. Wire_SetTextureParams reads the same latch to decide, so
the two halves cannot disagree about whether a wait is owed.
- THE FALLBACK IS THE OLD PATH, unchanged: an object whose create was refused stays unpublished, so
its next parameter record still takes the real answer and the existing one-retry heal still runs.
That is also the red-once: with the unconditional EmitAndWait put back, the new case fails -
RemoteClientControls.AParameterChangeForAConfirmedObjectTakesNoReply exits 153, having asserted
that the eight parameter records still go OUT while none of them takes a reply.
- WHAT IS GIVEN UP, stated rather than implied: a loss the client believes is published (a bug, or a
served-context teardown whose only caller today is a test) is now silent on the CLIENT - it stays
counted and named on the server, which is the same residue item B accepted for the upload row.
- The measurement case's "the counter is live" control moves to create/respecify, because this row's
zero is now the fix rather than a dead counter. 168/168 in RemoteClientTest; the unit lane is
2617/2617. Tracks: docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md.
- 2.9 established that a record's ~5 ms is mostly the DEVICE waking out of a deep idle state.
That points at a knob that is already wired: how long the server's apply thread spins before
parking. Raised from 50 us to 2000 us (via the Activity's existing `env` extra, which
ServerEnvironment.apply parses as KEY=VALUE;KEY=VALUE and which does not scrub
MOBILEGL_IPC_SPIN_US), the same 43,232-record frame goes 221.5 s -> 182.8 s (-17%), with
rtt_mean 5,040 -> 4,145 us. Pinning the device and disabling its deep idle states is worth
166.5 s (-25%) but needs root; the spin needs neither root nor a rebuild.
- The server log proves the knob landed (`mgl-srv-apply started ... spin 2000 us`) rather than
leaving the reader to trust an env var, and the note carries the exact am start line.
- So the load stall now has three ordered levers with numbers behind them: batch the waits
(client-only, not built), keep the server hot (this commit, usable today, trades device CPU for
wall clock on a frame that is 94% waiting), and pin the device for measurement discipline.
- Three one-variable experiments on the same device, same workload (the 43k-record frame): a 40x
larger client spin moves the per-wait mean by ~10% (so it is not the client park/wake); the
same frames over adb forward - a link whose raw RTT is several times lower - by ~25-30% (so it
is not the link, which is what the handoff saw in 3.6 and could not account for); and pinning
the device (governor performance on all eight cores, cpuoff_l / clusteroff_l / mcusysoff /
s2idle disabled) by 26% per wait and 25% of the whole frame: 221.5 s -> 166.5 s for 43,218
records and 100.5 MB staged, against 43,232 and 101.4 MB in the baseline.
- The mechanism: the server's apply thread parks between records, the SoC drops into a deep idle
state, and every record pays the exit latency - milliseconds, once per record, which is the
entire shape of the frame. It also matches the server profile the handoff took (65% socket
syscalls, ~15% doorbell spin: waiting and waking, not working).
- Stated where it matters: the handoff's 249 s was taken on an UNPINNED device, so its absolute
numbers carry this idle tax - the project has a pinning discipline for exactly this reason.
- The lever list is now ordered: batch the waits (K <= 8 by the reply-slot pool, client-only),
keep the server hot between records (its own spin budget, which the APK does not wire yet), and
measure split numbers on a pinned device. The device was restored to stock afterwards.
- A wait count alone cannot say whether a frame is slow because it sends too much or because it
waits too often. The device run made that the next question: one atlas frame paid 43,232 waits
while the upload row paid none.
- LinkMetricsNoteRecord is called at the encoder's one commit point (PipeWireCodec.cpp, beside the
existing CallClass::WireRecords count, for the comment's reason: after chunking no emitter knows
how many records its call produced). LinkMetricsRecordsFor reads it back, and every frame that
emitted something now logs `P65LinkMetrics kind=frame-sent frame=N records=M by_op=...`.
- The host measurement now prints and asserts both halves for the same workload. An atlas-shaped
load (64 sub-uploads into one level) sends 128 ResourceSubData records and waits for ZERO of
them, while ResourceCreate 76/76, ResourceRespecify 130/130 and SetTextureParams 130/130 are
one wait per record. That is item B stated exactly: the records still go out; the round trips do
not. It also says what the next thing to fix is - the record COUNT of those three rows, not their
waits, which the contract rules they must keep.
- The device run that motivated this made the point: 43,232 of a session's 43,629 reply waits were
in ONE atlas frame, and the exit summary never appeared at all - a SIGTERM does not take the
session's clean stop path, so LinkMetricsEnd is never reached. The split added one commit ago
was therefore invisible on exactly the run that needed it.
- Emit now writes a second line for any frame that bought a wait:
`P65LinkMetrics kind=frame-op frame=N wait_replies=M by_op=<id>=<count>,...`. The formatting is
factored into FormatOpSplit and shared with the exit line. Frames that bought nothing stay
quiet, so a steady-state session does not double its log.
- Measured on the device (MC 26.2 into a world over TCP, MOBILEGL_PIPE_STATS=1): the steady state
is one wait per frame and it is op 9 (Present); the upload-heavy startup frames are
`2=50,3=68,47=62` - ResourceCreate / ResourceRespecify / SetTextureParams, with ResourceSubData
(48) absent, which is item B working on the wire it was written for.
- The tally added beside this is only useful if somebody can read it where the problem is. The
handoff says why that was hard: under spawn the client does not advance the frame window, so the
per-frame P65LinkMetrics lines never print, and a SIGKILL takes the exit-time dump with it -
"only the dump at exit, and SIGKILL does not get it".
- LinkMetricsEnd writes the split on its own line from the one place a client that ends the
ordinary way always reaches: P65LinkMetrics kind=per-op wait_replies=N by_op=<id>=<count>,... .
Ids stay raw (this file includes only <cstdint> and friends, not the op table) and the reader
names them: 2 ResourceCreate, 3 ResourceRespecify, 47 SetTextureParams, 48 ResourceSubData. The
mapping is written down in mgl-device/README.md, which is the page the device run reads.
- What was open: the handoff's 4.3 called ResourceCreate and SetTextureParams "a second problem of
the same kind, needing its own analysis" and left it there; 5.4 said the per-op split of the
55,428 waits was never quantified. The measurement committed beside this answers both.
- The ruling (CONTRACT-P5E.md 2.5): ResourceCreate, ResourceRespecify and SetTextureParams stay
kWaitReply, because each answer carries a fact about the SERVER's record table that the client's
own tracker provably cannot know. RespecifyOnce says it outright - "the applier's REFUSAL is the
only signal that says 'I hold nothing for this handle'", which is the scope
MGPipeApplierReleaseObjectRecords leaves for a served context whose frontend objects live on;
EmitTextureParams uses its own refusal as the same self-heal trigger; PublishCreate may not take
the publication latch on a refused create (D-I1/c0b) or the death path emits a resource_destroy
for a record that does not exist. Unlike the upload half these are not "the emission gate already
answered it" rows, so removing their waits removes the signal.
- The numbers, both shapes a stitcher produces: an atlas stitch goes 64 -> 0 reply waits; a texture
per sprite goes 384 -> 320 with ResourceSubData now zero and the remainder all three of those
rows. So item B hits exactly the shape it was aimed at, and what is left is per-resource /
per-call rather than per-chunk.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md gains 2.4 with the table and the ruling, and its "second
problem of the same kind" entry is closed rather than carried; MEASUREMENTS.md records the two
shapes beside the 1 -> 0 single-upload number.
- The handoff measured the total (55,428 reply waits in one Minecraft frame) and left "which rows"
open: a total is the same number whether the waits are uploads, creates or parameter sets, and
those three have different fixes - so knowing the split is what says whether P12 item B is the
whole story or a fraction of one.
- LinkMetricsBeginReply now takes the op and Window carries a per-op tally, so
LinkMetricsReplyWaitsFor(op) reads back what the P65LinkMetrics line prints as `wait_replies`
split by row. Same activation as everything else in that file (MOBILEGL_PIPE_STATS=1), one array
in the window, and a value past the table lands on the last slot so the per-op numbers still SUM
to the total.
- RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad drives the two shapes a stitcher
produces and prints the table: an atlas stitch (64 glTexSubImage2D into one level) goes from 64
reply waits to ZERO, and a texture per sprite (64 textures, 256 glTexParameteri) from 384 to 320
with the remainder all ResourceCreate 76 / ResourceRespecify 130 / SetTextureParams 130.
- It prints before it asserts: the table is the point of the case, and a gate that fired first
would hide the numbers on exactly the run that needs them - the red one. The assertions are the
structural ones that can be wrong about the mechanism (upload rows zero, the other rows not, the
per-op tally summing to the total); the numbers themselves are recorded, not gated.
- The A/B is a real run: with the wire-arm arm test removed the case goes red with the full table,
which is where the 64 and 384 above come from.
- CONTRACT-P5E.md 2.5 said "the TEXTURE half keeps its wait because DrainTextureSubData clears the
level's dirty flag on the accepted reply (D-D5; trailing)". The trailing is paid, so the clause
now states the three facts that make that answer DERIVABLE on the wire arm (the emission gate IS
the consumer question the applier's own acceptance belt asks; what remains is a loud bug class;
a client-cancelled record still answers "not accepted"), where the wait moves to (SEG_STAGE),
that MGPipeSubDataWantsItsReply stays the one place that decides, and the gate with its red-once.
ResourceCreate and SetTextureParams are named as analyzed separately rather than swept in.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md: 2.3 now carries the G1 A/B this machine ran - .text
13872002 bytes on both sides, 31746 symbols, 0 added / 0 removed, no per-symbol difference -
instead of deferring it to CI, and 3 trades its "G1 not run" item for the device tooling
(mgl-device/: launch-mc.py, gl-aliases.sh, start-phone-server.sh) the next round needs.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md: the two items, their root causes with file:line anchors,
the five cases that pin them, the red each one produced when the fix was taken back out, and the
four things this round did NOT do (device end-to-end, local G1, the ResourceCreate /
SetTextureParams pair, and the sandbox's missing /dev/dri - which reds the two EventForfeitPeer
lanes for reasons that have nothing to do with the change).
- p12/README.md: a line in the summary and the new file in this directory's table.
- MEASUREMENTS.md: the P12 conclusion and row now carry the number the gate pins - one reply wait
per pure texture upload becomes zero - with the device wall clock named as the measurement still
owed rather than left implied.
- MGPipeSubDataWantsItsReply is the ONE place that decides whether a resource_subdata record waits
for its answer (WireTables.cpp:370 and PipeRoute.h:328 both read it, and the comment beside it
says that a second spelling is how the client comes to wait for an answer the emitter told the
server not to bother with). The buffer half has answered "no" since P5e; the texture half now
does too, ON THE WIRE ARM ONLY.
- THE ANSWER IS DERIVABLE THERE, which is the argument - not that nobody reads it:
* The emission is already gated on the answer's content. A texture record is produced only where
FamilyIsLive(kMGPipeSubsystemTextureResources, ...) holds, and under split that gate IS
CapsMirrorInstance().ServerConsumes(bit 10) (PipeFill.cpp:1719-1740) - the same published fact
the applier's own acceptance belt reads (PipeApply.cpp:1408-1419, NoP4aConsumer). So the one
refusal this acceptance exists to carry, "no consumer" (ID-39 / D-D5 / ID-18 M3, which is why
the texture half ever asked), cannot be the answer to a record this client emitted.
* What remains is a bug class, and it is loud already: a record that names no texture, declares
no texels or carries no bytes is an MGP_TRIP_WIRE_REPORT (PipeApply.cpp:1201-1218, :2245-2252),
and a handle the applier does not hold is counted and named (ResolveResourceIn, :721-728) with
the client's own tracker preventing it from arising (PipeFill.cpp:1045-1051).
* Every answer the client can still get for free, it still gets: the emitter reads this route's
Bool exactly as before, so a record the CLIENT cancelled (the device-lost latch, an exhausted
stage) still answers "not accepted" and the level stays dirty and is retried - D-D5's safe
direction, unchanged.
- THE WAIT MOVES TO THE STAGE. A run that does not fit SEG_STAGE reclaims retired bytes and parks
on the stage doorbell (PipeWireCodec.cpp StageAllocate / ReclaimStagedBytes) - the one wait a
run-ahead client owes, and it is proportional to the WINDOW rather than to the call count. On
Minecraft 26.2's atlas frame that is the handoff's 55,428 reply waits at ~4.4 ms each: 96 MiB of
uploads through a 32 MiB window.
- THE GATE IS THE NUMBER ITSELF. LinkMetricsReplyWaits is added so a case can read back the same
`wait_replies` the per-frame P65LinkMetrics line prints - LinkMetrics.cpp:76-80 counts exactly
the records that own a reply slot. TextureUploadsStopOwningAReplyOnTheWireArm asserts that a pure
glTexSubImage2D crosses the wire and moves it by ZERO, while a resource_create row - kWaitReply,
and it always was - still moves it, so the zero cannot be "nothing was counted". Red-once run:
with the arm test removed the child exits 142.
- Monolith and every non-wire arm are untouched, and that is pinned twice: the existing
RemoteRunAhead.OnlyTheTextureHalfOfResourceSubDataWantsItsReply still reads true with no arm
installed, and the new test sits inside MOBILEGL_BUILD_DISAGGREGATED with the original expression
as its fall-through, so a pull build preprocesses to the line it had (G1).
- Verification debt, stated rather than implied: the host gate is the COUNT, not a wall clock.
In-process control sessions run lockstep (the caps peer publishes no kCapRunAheadApply), so the
verb barrier still waits there; the end-to-end wall-clock win is the device measurement the
handoff describes and it has not been taken yet.
- ReadFenceReply (EmitTables.cpp) and ReadQueryReply (QueryEmit.inc) tested `status != kStatusOk`,
which is two values in one branch. ReplySink's own header says "DECLINED IS A REAL ANSWER, not
a failure" (PipeWireCodec.h:457-460), and the escape rows are held to the other half of the same
rule - "ERROR IS NOT A DECLINE ... an escape may not fold 2 into `false`" (WireTables.cpp:446-450).
- BOTH VALUES OF 1 ARE REACHABLE WITHOUT A BYTE OF CORRUPTION: the server declines these five rows
by design whenever it cannot answer them (PipeWireCodec.cpp:1820-1830 for the two fence rows,
:1841-1861 for the three query rows - a zero-length answer to a kWaitReply row, R-5), and the
client produces the same status itself once the session stops carrying records
(ClientSession.cpp:1837 the device-lost latch, :2025 a doorbell that died during teardown, where
the comment says outright that "the verb did not happen" is reported as DECLINED and not ERROR).
- WHAT THE FOLD COST: after a clean device loss Minecraft 26.2's next glClientWaitSync - the
chunk-upload fences - died of Fatal{ProtocolCorruption, "Fence.reply"} (rc=134) instead of taking
the no-op the design promises ("GL calls become no-ops", design/08-runtime-and-platform.md:33).
The query rows had the same shape waiting for them.
- The no-op is the one this frontend already gives for a sync it cannot wait on: GL_Sync.cpp:103-106,
PipeApplier.cpp:175-177 and DirectGLES.cpp:16489 all answer GL_ALREADY_SIGNALED. A query row
answers its own "no answer yet" value - availability false, Produced 0 (which the caller already
returns as false with the out-parameter untouched), timestamp 0.
- ERROR and a short OK keep the named Fatal they always had: a transport fault is not the server
saying no, and an OK answer that arrived empty is a reply nobody can read. ReadPixels keeps its
deliberate Fatal{ReadbackDeclined}: unlike these rows a blocking readback cannot succeed short of
complete pixels, and that ruling is untouched.
- PipeFill.cpp's UncarriedInitialBytes self-check now spares a session that has latched device-lost.
The resource_subdata it counts is DECLINED by design there, so "it emitted nothing" is the latch
working rather than bytes this client forgot to carry - and there is no server left for them to
exist on. P12's exit gate (a) is that killing the server gives a CLEAN device-lost latch, not a
corpse at the next glBufferData that carries data.
- Four cases in RemoteClientControls.inc drive the production consumers over a live session whose
peer declines them. Red-once run for all three Fatals (Fence.reply, Query.reply,
UncarriedInitialBytes), and ErrorFenceReplyStillRefusesByName is the control that keeps the
tolerance from becoming blanket. RemoteClientTest 166/166; the host unit lane 2610/2610.
- Split only: both consumer files compile under MOBILEGL_BUILD_DISAGGREGATED and the PipeFill hunk
sits inside its `#if`, so a pull build preprocesses to exactly what it had (G1).
- PublishCapsSnapshot filled that field with info.RendererGLInfo.TargetGLVersion.toString(). The
client reads it back through BackendObject_Remote::GetBackendAPIVersionString (CapsMirror's
ApiVersion) and prints it in the parentheses of GL_RENDERER, which is the only place a real
device is named; a monolith build fills the same parentheses from the backend's own
GetBackendAPIVersionString. A split client therefore answered "Espryt (MobileGL Core) (4.6.0)"
where monolith answered "Espryt (MobileGL Core) (Adreno (TM) 750, OpenGL ES 3.2)".
- The target version still crosses the wire inside the renderer-info blob (CapsCodec), which is
where GL_VERSION reads it, so no reader loses it.
- Content only: the field already existed and the blob layout is untouched, so the wire
fingerprint needs no bump, and the file compiles only under MOBILEGL_BUILD_DISAGGREGATED.
- Verified with a PC client against a phone server: Minecraft and Sodium both log
"Espryt (MobileGL Core) (Adreno (TM) 750, OpenGL ES 3.2)".
- README, ARCHITECTURE, ROADMAP, MEASUREMENTS and CURRENT_STAGE_PROGRESS are rewritten as indexes
(84 KB -> 15 KB): what the project is, where it stands, and where each detail lives. ROADMAP is one
plain sentence per phase; MEASUREMENTS is the five conclusions that matter plus one line per phase;
CURRENT_STAGE_PROGRESS is the P12 goal, a checklist and the next steps.
- ARCHITECTURE's body moves unchanged into ten design/ files, keeping §1-§17 and appendices A/B, so
`ARCHITECTURE.md §N` references in code resolve through the table in ARCHITECTURE.md.
- guide/ holds build-and-run, code map, glossary and the engineering discipline (from README and
ROADMAP), and the device pinning record that used to sit in devices/.
- The open-debt table moves to notes/DEBTS.md; the detailed current-stage tables (with decision IDs,
the open line items and evidence locations) move into notes/p12/README.md. notes/README.md maps
every moved path and section.
- The documentation citation lint in test.yml also covers design/ and guide/, which describe the
current tree as the top-level files did.
- ROADMAP, MEASUREMENTS and ARCHITECTURE are rewritten as summaries (333 KB of index text -> 84 KB).
ROADMAP's phase table is one line per phase plus a link; MEASUREMENTS keeps headline numbers under
its old §1-§13 numbering (plus §14-§16 for P6.5, P7 and P12); ARCHITECTURE keeps every decided rule
under the same §1-§17 / appendix numbering, so the `ARCHITECTURE.md §N` references in code still
land, and §17 becomes an index of the phase landing shapes.
- CURRENT_STAGE_PROGRESS now describes the current stage (the P12 on-screen-server subset) and absorbs
CURRENT_STAGE_STATUS, which is removed. The P7 status tables, the P6 closing snapshot and the P6.5
first-wave snapshot move to notes/p7, notes/p6 and notes/p65.
- Each phase gets notes/<phase>/README.md: a short summary, then the verbatim material that left
the index documents (its ROADMAP row, its MEASUREMENTS section with subsection numbers kept, its
ARCHITECTURE §17 part, milestones) and a file index. P8-P11 and P13 get their planned scope;
the full open-question list moves to notes/OPEN-QUESTIONS.md.
- The phase reports that sat at the top level move into their phase folders with unchanged names:
P5D-INPROC-PERFORMANCE, P5E-RUNAHEAD, P5F-WIRE-COMPLETENESS, P6-SPAWN-PLAN, P6-CONTRACT-DRAFT,
P6-ENDSTATE-REVIEW. Their relative links, CONTRACT-P6.md's links to them and one tool docstring
are updated; notes/README.md maps every old path and section number to its new place.
- 71 files under notes/recovered/ that were byte-identical copies of phase-folder files are removed.
- Corrections made while condensing: the server process reads Transport as Spawn (CONTRACT-P6 §3.1
amended the old "child forces monolith" rule); both backends now publish kCapRunAheadApply;
MOBILEGL_IPC_RESPAWN is a named refusal and MOBILEGL_IPC_IDLE_EXIT_S is not parsed; the Redmi
pin row is in tools/device_bench/pin_device.sh.
check_doc_citations over docs/Disaggregated/*.md: 0 problems. No relative link is newly broken.
- unit 2605/2605, integration-spawn/tcp and the three Magma two-process lanes green, hygiene 12/12, census 79 / ratchet 171 unchanged, revision 3 pinned.
- Major 1 red without its fix ("Detach answered TimedOut in 3000 ms after 200 of 200 streamed records"), major 2 red without its fix (all four expectations), both green restored.
- SplitLogPaths / spawn_lane_parity / integration-gpu reds are registration artifacts of this container's CMake 3.28 (lane environment missing) and 4.4 (lane environment flattened into one variable; gate-review-d04ac7d1-cmake4.log); the four PersistentCoherentMapScenario and three F1WireScenario cases behave identically at cb0404f2 and at this head when the binary is run directly with the same environment. No CMake file changed since cb0404f2.
gate/gate-review-3c342c97.log: the P12 gate at head 3c342c97 (dirty=0) in a 4-core container with clang-19, lavapipe (mesa 25.2) and ctest 4.4.3 - unit 2605/2605 (the four new review cases included), integration-spawn/tcp and the three Magma two-process lanes green, hygiene 12/12, fatal census 79 / link ratchet 171 unchanged, protocol revision 3 holds. Still being attributed: SplitLogPaths and spawn_lane_parity (the container's ctest tooling; no CMake file changed since cb0404f2), four PersistentCoherentMapScenario and three F1WireScenario cases in integration-gpu (being run at base cb0404f2 with the same flags), and IterationRPProgram203Scenario (out of scope, ID-P12-12). gate-container.sh is gate.sh with the host paths removed and G1 run separately.
- notes/p12/INTEGRATOR-DECISIONS-P12.md (new): ID-P12-1..4 the subset's shape (the user rulings, one surface mode per session, the lost-window order, the in-process thread hand-off), ID-P12-5..11 the review round's ten fixes (two majors: the lost window answered before every ring record, the ended session's Espryt unit shadows emptied with its twins), ID-P12-12 IterationRP out of scope, ID-P12-13 the device stage not re-run in this environment.
- ARCHITECTURE.md §15.2: the deleted Surface -> AIDL window path is struck through, and the server-owned window path, the lost-window contract and the in-process session boundary are described.
- ROADMAP.md: the P12 row's status names the review round and that the device checks were not repeated.
The Vulkan renderer (swapchain and VkSurfaceKHR on the window) is a process global that only ReleaseEGLResources or a released surface drops, and ~BackendObject_DirectVulkan was defaulted. A client that went away without eglTerminate (a crash, a kill, a dropped connection) sends neither, so after its session the renderer stayed on the display Activity's window past the end of the session's lease. Under a transport the server backend's destruction - the session's end, on the apply thread, before the lease ends - now drops the renderer (with the generation bump and the program reflection caches, as ReleaseEGLResources does). Split build only; the pull build's destructor is still defaulted (G1).
Major. DropEveryTwinForEndedServerSession drops every twin with InProcessTeardown() answering true, which is exactly the case in which a texture / sampler twin's destructor does NOT scrub itself out of TextureImpl::g_boundTexturesCache / SamplerImpl::g_boundSamplersCache. The shadows kept raw pointers to freed twins; the next in-process session's twin allocated at a recycled address read "already bound" and skipped its glBindTexture / glBindSampler on a context where nothing was bound, so its uploads went to texture 0 and it rendered black. The drop now empties both shadows and puts the active-unit shadow back to 0 (the new context's GL_TEXTURE0). SanityTest EsprytServerSession.TheServerBackendsDestructionEmptiesTheTextureAndSamplerUnitShadows (split build only; the pull build is unchanged, G1).
SetLogForwarder forwards every server-role thread's line, which is right for a session child (the process is the session) and wrong for the display Activity's process, where MOBILEGL_IPC_ROLE=server makes every thread a server thread: the UI thread's surface callbacks and the listener's lines - naming other peers' addresses, Busy refusals included - went into the live client's control socket. And the forward was a blocking SendFrame under the global log mutex, so a client that stopped reading held that mutex and hung the UI thread at its next log line (surfaceDestroyed's Detach logs).
In-process, RunSession now installs SetSessionLogForwarder: only the session thread and the threads that opt in (the session's apply thread, SetThreadForwardsLogToPeer) forward, and the send happens after the log mutex is released, under a forward mutex of its own (WithLogBarrier takes both, so the flush ack still follows every send that began; a line logged by the send itself is not re-sent). The forked session child keeps SetLogForwarder unchanged; the pull build's Log() is unchanged (G1).
DirectGLES keeps the requested eglSwapInterval in a process global on purpose (a surface re-created mid-session re-applies it), and every server process used to serve one session. The in-process display server serves the next session in the same process, which then silently inherited the previous client's interval. The server backend's destruction under a transport now calls DirectGLES::ForgetRequestedSwapInterval() beside DropEveryTwinForEndedServerSession (split build only; the pull build is unchanged, G1).
- Client resize: eglResize on a headless client's server-owned surface only rewrote the backend's record of the window - the phone's SurfaceView kept its size - and the client's EGL state answered the size it asked for. The server now recognises the surface as its own (ServerLoop::m_serverOwnedSurfaces, per session) and resizes it as a geometry request (the lease re-taken by its holder, the display waited for), resizes the backend surface to the window's real extent and replies with it; BackendObject_Remote adopts that extent (ServerResizeServerOwnedWindowSurface).
- Size tracking: the client held ONE server-owned surface, so a second one took the first's geometry updates and releasing it left the first with none. ClientSession now tracks every server-owned surface and applies an extent to all of them (they share the server's one window).
- Size reports without the knob: Espryt's new extent publish fired for every window surface of a spawn/tcp session, including windows a client named itself without MOBILEGL_IPC_SURFACE=server, switching their consumer to the reallocating arm. ServerLoop now names its own leased window to the backend (MGPipeServerSetOwnedWindow, split-only) and Espryt publishes the extent only for that window.
AcquireFor(0, 0) granted the lease at once, at whatever the window reported - and right after a session that fixed the size, that is still the fixed size: setSizeFromLayout answers asynchronously, like setFixedSize. ServerDisplay now remembers the geometry last requested and the layout's last reported extent; a 0/0 request that follows a fixed one waits (within the geometry grace) for the layout's extent, or for any report after the request that is not the old fixed size. ServerDisplayTest.TheWindowsOwnSizeAfterAFixedSizeIsTheLayoutsNotTheStaleFixedOne.
- Major: the window-lost release was picked up only between drain batches (PumpControlRequest), and a client streaming frames keeps the command ring from emptying for seconds - surfaceDestroyed waited out its 3 s bound on the UI thread, answered TimedOut, and the session kept presenting into the destroyed window. DrainRing now checks the request after its latch check, before every pop. ServerLoopLatchTest.ALostServerWindowIsReleasedMidBatchWhileTheClientStreams streams a 4 s batch under Detach.
- Stop while streaming: mobilegl_server_stop_inprocess ended the session through loop.Stop(), whose batch and final drain of a ring the client keeps refilling could outlast the bounded join (Fatal{ApplyThreadJoinTimeout} aborts the display Activity's process). RunSession now calls ServerLoop::AbandonQueuedRecords() first when the server is stopping: the drain stops after the record in hand and the exit path declines the rest, as a ReverseChannelForfeit does. ServerLoopTest.StoppingTheServerUnderAStreamingClientLeavesTheQueueAndJoinsPromptly.