Commit Graph
3816 Commits
Author SHA1 Message Date
BZLZHH 82de3f6826 [Docs] (P12): the ring-1024 experiment - every deferred answer comes back (readfail 8,872 -> 0), so eviction was the whole cause and the residency budget is now the brake
One-line experiment before writing the retention table: StreamLink's kReplyRing 8 -> 1024,
nothing else changed, same load frame on the same device.

                       ring = 8 (today)   ring = 1024
    readfail                    8,872              0
    taken                          63          4,499 / 4,500
    blocked (fell back)         4,435              0
    by_op 2                     9,071          3,443
    wall                        27.9 s         23.04 s
    frame-sent        create 4,536, records 103,097   identical per op

TWO CONCLUSIONS. First, eviction was the ONLY cause: with a buffer deep enough, every
deferred answer was read back and not one create fell through to the blocking path.
The wedge (want=310) and the useless budget (budget=4,451) were two faces of "eight
slots is not enough". Second, what brakes now is the BUDGET, not the buffer: by_op 2 =
3,443 matches the number of times the budget forced an early read (3,395), i.e. every
one of those is a real blocking wait. Its constant is 7, sized for the 8-slot pool, so
against a 1,024-deep buffer it became the only throttle and the wall only moved 27.9 ->
23.0 s.

WHAT IT MEANS FOR THE DESIGN. Once the answers are RETAINED until read, correctness no
longer depends on how long one survives in a bounded buffer, so the budget must be
sized to the RETENTION CAPACITY rather than to the pool - or be replaced by "retained
means safe". by_op 2 = records/W then puts W=2 at ~2,268 waits and the wall at ~15 s,
which is the goal's own success criterion.

Experiment reverted (ring back to 8, probes deleted); 172 + 13 + 22 + 103 green.
2026-09-26 02:44:30 +08:00
BZLZHH b1ef484116 [Perf] (MG_Remote): the residency budget does not make the window usable either - the answer is gone before the client next looks, so a deferral needs RETENTION, not a budget
Steps 1 and 2 of the plan the design review set out, landed and measured on the
device. Step 1 answers the question step 2 depended on; step 2 answers the one the
whole line depended on, and the answer is that the window's shape is wrong.

STEP 1 - CAN AN ANSWER BE LOST IN DELIVERY? NO. ReplyPool now counts what the link
took (PostedReplies) and what it refused (FailedReplies), and a non-OK result is
NAMED instead of swallowed: it checked only BUFFER_TOO_SMALL and let
TRANSPORT_CLOSED vanish with no line and no counter, so "the server never sent it"
and "it was sent and the buffer overwrote it" were the same symptom with opposite
fixes. Measured: posted=4501 failed=0. The twenty-answer gap between the server's
tally and the client's was a sampling difference, not a drop - and the server's
m_answered counts ATTEMPTS (incremented before PostReply), which is why it could
never have settled it.

STEP 2 - DOES A RESIDENCY BUDGET FIX THE WINDOW? NO.
Wire_ResourceCreate now measures how many reply-owning records the client has
emitted since the oldest window entry was pushed (ClientSession::ReplyPostings, a
new clock) and takes the oldest answer back as soon as that approaches the reply
buffer's depth, instead of deferring past the point where the answer can still
exist. On the device:

    P12win call=4501 cfg=2 eff=2 pending=2 taken=63 pushed=65 blocked=4435
           budget=4451 readfail=8872 posts=54377
    kind=frame-op frame=14 wait_replies=9078 by_op=2=9071   wall 27.87 s

budget=4451: it fires on essentially EVERY create, the window stops deferring
(pushed=65), and the drain still reads nothing back. THE REASON IS STRUCTURAL, not
a constant:

  * the window only gets control WHEN A CREATE ARRIVES. Between two creates this
    client emits on the order of a thousand records - at the FIRST failed read the
    reply ring already spanned [1175..1504] while the head was seq 310 - and nothing
    looks at the window in that gap;
  * what displaces a deferred answer is not what this client emits, it is WHAT THE
    SERVER ANSWERS. The server is the one behind (one answer per ~5 ms wakeup while
    the client runs ahead), so the replies arriving during the gap are its LATE
    ANSWERS TO THE EARLIER CREATES - they fill the 8 slots first;
  * so the check fires too late by construction. Tightening it does not fix that; it
    only removes the deferral entirely, which is what pushed=65 is.

WHAT THAT LEAVES. A deferral is readable only if the client KEEPS the answers it
deferred. The buffer is bounded by COUNT and the client cannot bound how many
answers the server will post between two of its creates, so hoping the buffer still
holds one is a race the client loses - which is also why the earlier two diagnoses
(reply-pool churn, then FIFO-with-no-way-out) were both describing symptoms of the
same thing. The shape that can work is retention: the reader keeps the answers for
the seqs still outstanding (a handful), drops them when read, and reports a LOSS
when a read asks for one it no longer has. That is a transport change - StreamLink's
ring becomes a retention map with a cap and a lost counter - and it is the next step.

BOTH STEPS STAY: step 1 is a net gain on its own, and step 2 is the safe direction
(it can only degrade the window to no-deferral, i.e. the pre-window shape) and its
clock is the one the retention design needs. MOBILEGL_IPC_CREATE_WINDOW stays at 1.

172/172 and 13/13 after the probes were removed. The measurements and the revised
conclusion are in notes/p12/CREATE-WINDOW-MEASURED.md section 9.
2026-09-26 02:35:05 +08:00
BZLZHH 4e48702f26 [Feat] (MG_Remote): a record can tell the server its answer will never be read - the bit works and cuts a frame's replies by 92%, and it proves the window's real blocker is the FIFO drain, not the pool depth
The create window could not collect its deferred answers. The previous commit
diagnosed that as the reply pool being 8 slots deep against 55,903 replies in one
frame, and left a no-reply bit as the way out. This lands that bit, measures it on
the device, and reports that the diagnosis was half wrong.

WHAT LANDED. ClientSession::EmitAndWaitTails takes a second question beside
wantReply: willReadReply. They are not the same question and the window is the case
that separates them - it emits with wantReply=false and READS the answer later, so
its records must still be answered. kRecNoReply (RingRecordFlags bit 5, which
aliases kOptional in the call-flag space; the flag-space asserts were extended and
the alias named) is stamped when willReadReply is false, and the decoder skips
PostReply while keeping every acceptance counter - "nobody reads the answer" is not
"nothing happened". The two illegal combinations are Fatal by name.

Three call sites say it: SetTextureParams for a confirmed object, the respecify
row's fire-and-forget half, and sub-data where MGPipeSubDataWantsItsReply decides.

MEASURED ON THE DEVICE (aikjs4s8cys8ba7l, rebuilt APK + client, both halves new):
    server:  skipped=50,001   answered=4,521        (a frame's replies: 55,903 -> 4,543)
    client:  Reply frames received seen=4,501, latest seq=97,181
    frame-sent: identical per op, records untouched
    create row: by_op 2 = 9,070 (was 4,536)  - because the drain's blocking take is
               now counted, so the row's number stays honest instead of reading as
               "create stopped waiting" while the waiting merely moved
AND THE WINDOW STILL READ BACK NOTHING: readfail 8,951 -> 8,872, i.e. -0.9%.

THE REAL CAUSE, from the miss probe:
    P12miss n=1     want=310 replySeq=1504   ring=[1175..1504]
    P12miss n=9001  want=310 replySeq=102915 ring=[102866..102915]
want=310 every time. The drain is STRICTLY FIFO and a failed read has no way out -
it retries the same head for ever. Any reply buffer is bounded by design (8 pool
slots, or the 8-frame ring the stream link now keeps), so with 4,536 deferred
answers at least one is overwritten before it is read, and that one wedges the
window permanently. Cutting the churn by two orders of magnitude does not help: it
turns a certainty into a certainty. That is the constraint, not the depth.

THE FIX IS NAMED AND NOT TAKEN (the goal this work ran under stops here on purpose).
On a failed read of the head, abandon it: MGPipeNoteHandleUnpublished +
NoteCreateSuspect + pop - the DECLINE path reused for "unknown". The window then
self-heals through the blocking path and the object merely gets slower. It needs its
own gate: a failed read must let the window ADVANCE.

Gates, all green (172 + 13 + 22 + 103):
  PipeWireCodecTest.TheNoReplyBitSkipsTheAnswerAndKeepsTheAcceptanceRecord - the skip,
    the kept tally, the answered/skipped split, and the negative control that a record
    WITHOUT the bit is still answered (a server that stopped answering everything
    would otherwise pass)
  RemoteClientControls.ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - now also
    asserts the answers CAME BACK (answers_taken=11, outstanding=1, fallbacks=0 against
    12 records). RED-ONCE PROVED: giving the window's creates the literal
    "fire-and-forget => skip" predicate makes this case fail.
  RingTest - the sixth and last flag alias, and the hand-written exhaustiveness list
    extended (with a note that it is only as exhaustive as that list).
  Server side: AnsweredRecords()/SkippedAnswers() - the number that made this
    debuggable, since a wire change whose effect is invisible is undebuggable.

Client-first is safe: an old server ignores bit 5 and replies as before, so the two
cross-version directions degrade to today's behaviour - this is "both ends must
upgrade to take EFFECT", not "to avoid breaking". Hit that in practice first.
2026-09-26 02:00:44 +08:00
BZLZHH 7c822cdc85 [Docs] (P12): the no-reply flag reviewed - the arithmetic holds and the flag is cheap, but the predicate has to be "this answer will never be read", not "fire-and-forget"
An independent review of the one design door the measurement left open (stop sending
the ~51,000 answers nobody reads, so the 8-slot pool stops churning and the create
window becomes collectable). Verdict: feasible, worth doing, and the proposal AS
WRITTEN would disable the thing it exists to enable.

THE CATCH. The windowed create is wantReply=false AND its answer is read -
WireTables.cpp's drain reads exactly those seqs. A bit keyed on "fire-and-forget"
therefore stops the server answering the creates the window is waiting for: the
window goes from "cannot be collected" to "there is nothing to collect", and the
readfail counter that is the only way to diagnose it disappears with it. It fails
silently, not loudly. The predicate has to be per-record "this answer will never be
read" - true of params / respecify-escape / sub-data, false of a windowed create -
carried as a separate input from wantReply (willReadReply), with
willReadReply && !rowCarriesReplySlot rejected rather than ignored.

VERIFIED HERE, not taken on trust (the reviewer's file:line claims, checked):
  - the flag space is RingRecordFlags (Ring.h:281-285), bits 0-4 taken, BIT 5 FREE,
    and the header's own table (PipeWire.inc:35-65) says "bit 5 kOptional vs (nothing)
    no ring counterpart" - so a new ring bit aliases kOptional, which is exactly what
    the assert at PipeWireCodec.cpp:142-145 refuses;
  - the three static_asserts that will trip are real and deliberate
    (PipeWireCodec.cpp:106, :142-145, RingTest.cpp:740);
  - WireFingerprint mixes payload-layout, catalogue, render-state digests + OpCount,
    none of which a ring bit moves - so no protocol revision bump is required, and
    both cross-version directions degrade to today's behaviour. That makes this
    "must upgrade both ends to take EFFECT", not "must upgrade both ends not to
    BREAK" - one notch cheaper than handoff-2 section 3.2's protocol surface;
  - StreamLink.cpp:564-571 is the independent progress flush (>= 64 records or
    >= 1 ms) and :577 is PostReply piggybacking SendProgress on the reply - so
    skipping replies coarsens progress granularity. Bounded, but unmeasured.

AND IT CORRECTED TWO OF MY OWN NUMBERS. The frame's reply count is 55,903, not
59,673: op 4 ResourceDestroy carries kNone in the catalogue (PipeWire.inc:179) and
PostReply Fatals on a row with no kReplySlot, so it never answers - I had added its
3,770 records to the total. The derived figures move with it (12.3 replies per create,
0.65 of a create of survival, 6,988 pool churns), and every copy is fixed here:
Config.h, ConfigLoader.cpp, WireTables.cpp's section comment, CONTRACT-P5E.md,
CREATE-WINDOW-MEASURED.md.

TWO MORE THINGS THE FOLLOW-UP MUST CARRY, both recorded in the note's section 7:
  - LinkMetricsBeginReply counts ownsReplySlot, so once the flag lands by_op 2 falls
    to ~0 - and section 1 of the note tells the reader to use by_op 2 to check whether
    the window engaged. That check becomes a FALSE GREEN; the flag needs a
    server-side "how many times did this op answer" counter beside it.
  - the ~112,000 sendmsg/frame the flag removes are on the apply thread, so the
    flag's own value is a measurable claim that nobody has measured.

172/172 and 13/13 after the comment corrections.
2026-09-26 01:13:31 +08:00
BZLZHH 7205509a9a [Docs] (P12): the create window is dead by arithmetic, and the two debts it exposed (the applier can no longer say it dropped a record; the picture still has no real control) 2026-09-26 00:51:56 +08:00
BZLZHH d011395939 [Docs] (MG_Test, P12): the dropped-records repair is gone, not merely narrowed - the first-use work removed the last row that read an applier refusal, and the case that pins the debt
handoff-2 section 3.3 asks for a gate on the one scope that empties the applier's
object records while the frontend objects live on (MGPipeApplierReleaseObjectRecords,
"latent" because its only caller in the tree is MG_Test). The gate was expected green
for a CHANGED descriptor: the respecify is refused, RespecifyOnce turns the refusal
into a create plus one retry. It is not green.

MEASURED:
    dropped-records probe: creates=0 applier_records=0 published=1

Nothing is emitted at all. The refusal can only reach the emitter through an answer
the row READS, and P12 made both rows answer themselves for a "confirmed" object:
the respecify is fire-and-forget (MGPipeResourceRespecifyWantsItsReply) and the
params row writes a PROVISIONAL kStatusOk (Wire_SetTextureParams). "Confirmed" is
MGPipeHandleIsPublished, which answers "did a create GO OUT" - and it stays true
after the records are dropped, which is the scope's whole definition. The only row
left that reads the real answer is create, and create is only sent when the latch is
clear - the one thing that stops being true.

So section 3.1's "clearing the latch is not enough" is the smaller half: nothing
clears the latch either, and nothing can until the applier can say "I hold nothing for
this handle" - section 3.2's third option, the reverse channel, i.e. the protocol
surface the handoff hoped to avoid paying for. That hope was priced on the trigger
being unreachable; it is still unreachable (no production caller), so the call stands,
but the cost of a production caller is now known to be the whole thing.

THE CASE IS WRITTEN AS A DEBT RATHER THAN A SPEC, and says so in its name and its
comment: it pins today's three numbers, so whoever lands the signal sees THIS case go
red and knows the repair started working. Removing the heal from RespecifyOnce would
not make it red - nothing reaches the heal - which is exactly why the assertion is on
the applier's table and the latch rather than on a count (handoff-2 section 3.3: "a
count cannot show this class of error").

Also rewritten: notes/p12/UNPUBLISH-GUARD-SIGNALS.md sections 4 and 6 now carry this
as 4.2 (sections 2 and 3 stand: neither caps generation nor FreshlyPrimed is a signal).
172/172.
2026-09-26 00:50:00 +08:00
BZLZHH d2efd1d79d [Perf] (MG_Remote/Client): the create window lands OFF - the deferral cannot be collected, 8 reply slots against 59,673 replies in one frame - measured on the device at 4,536 blocking creates either way
The window was the handoff's remaining item: let up to N resource_create answers be
outstanding, read a provisional accept, collect the answers in order, and un-latch an
object whose late answer is DECLINED. It is a sound mechanism and the gates below pin
it - and on the device it does nothing at all, which is why the default is 1.

MEASURED, same device, same frame, same 101,421,396 B staged, same binary:

    MOBILEGL_IPC_CREATE_WINDOW=4   by_op 2 = 4,536   wall 28.64 s
    MOBILEGL_IPC_CREATE_WINDOW=1   by_op 2 = 4,536   wall 28.64 s
    (the handoff's pre-window baseline: 31.3 s)

by_op 2 counts the creates that took the BLOCKING path (LinkMetricsBeginReply is
called with ownsReplySlot = rowCarriesReplySlot && wantReply), so it not moving means
no create deferred. A temporary counter in the row said why: pushed=24, blocked=4,476,
took=20, readfail=8,951, pending pinned at 4 - the drain READ the answers back and
failed every time.

WHY, IN THE PROTOCOL'S OWN ARITHMETIC. An answer is readable only until its pool slot
is reused, and the pool is kDefaultReplySlotCount = 8. The server answers EVERY
reply-owning record, not only the rows that wait, because it cannot know which those
are (ClientSession.cpp:1897-1900: "The server posts either way"). This frame posts
59,673 of them: create 4,536 + respecify 20,633 + params 18,056 + sub-data 12,671 +
the rest. 8 slots against 59,673 replies churns 7,459 times per frame, i.e. 13.2
replies between two creates, so an answer is overwritten after 8 replies - 0.61 of a
create. The deferral is not slow, it is UNREADABLE, and no window depth fixes that.

AND IT IS NOT NEUTRAL: with the window on, the provisional accept latches an object
the applier may have refused, and the un-latch that corrects it lives in that same
unreadable drain. A refusal would become an object the client believes is published -
the silent divergence the suspect list exists to prevent. So the machinery is kept
(one experiment away from usable: a deeper pool, or a "do not answer me" bit, both
wire changes) and the default is off.

Worktree note: the window implementation itself was uncommitted in the tree at
82ed1df9 (MOBILEGL-CS-HANDOFF-2.md section 2, "can compile, gates mostly green, never
run on a device"). This commit lands it in the state the measurement supports. The
row also gains the heal the suspect list was missing - a BLOCKING create that lands
takes its object back out of it, or kCreateSuspectMax recoveries would retire the
window for the rest of the process - and the clamp that ties the environment's value
to the array, with a static_assert tying the array to half the reply pool so "someone
raises kCreateWindowMax to 8 and steps on the pool" is a compile error.

Gates (RemoteClientControls.inc), all green, 171/171 RemoteClientTest:
  ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - window on = 12 records 0 waits;
    the SAME CHILD turns the knob to 1 and it is 12 records 12 waits (the negative
    control); pending <= effective after every create; 99 clamps to the array; the
    array is <= half the pool
  ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlocks - a real refusal (the
    server's consumed mask narrowed) clears the latch, +1 suspect, the object's next
    create BLOCKS (2 records 2 waits), and restoring the consumer HEALS it (latch
    back, suspect -1)

Docs: CONTRACT-P5E.md section 2.5 rewritten (it still said all three rows keep
kWaitReply), plus
docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md (the numbers, the arithmetic,
the picture evidence: both arms SSIM 0.977) and
docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md (handoff section 3: neither
caps generation nor FreshlyPrimed is a signal for "the applier dropped its records",
and the residual gap is one line - the dedup at TextureEmit.h:838 does not read the
latch, so an UNCHANGED descriptor after a drop produces no record and therefore no
refusal to self-heal from).
2026-09-26 00:47:19 +08:00
BZLZHH a2c1f5f292 [Fix] (MG_Remote/Transport): StreamLink::ReadReply answers by seq instead of "is it the newest" - the contract ShmLink already kept, and the reason a deferred answer could never be read back
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.
2026-09-26 00:47:06 +08:00
BZLZHH 82ed1df90e [Perf] (MG_Pipe, MG_Remote/Client): a texture respecify for a CONFIRMED object stops waiting - the row pays its round trip once, at first use - and the frame drops from 130.9 s to 31.3 s
- 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".
2026-09-25 23:29:08 +08:00
BZLZHH f2b55b9d2d [Perf] (MG_Remote/Client): the buffer half of resource_respecify stops waiting - its answer is discarded at both call sites - and the measurement says the texture half is where the other 20,611 waits live
- 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.
2026-09-25 22:53:38 +08:00
BZLZHH 5ad8370b37 [Docs] (P12): the first-use confirmation measured on the device - 43,232 waits to 25,176, 221.5 s to 134.4 s, op 47 gone and every other row identical 2026-09-25 22:36:51 +08:00
BZLZHH b62709604d [Perf] (MG_Impl/Pipe, MG_Remote/Client): a parameter change for an object whose existence is confirmed takes no reply - the round trip moves to the object's FIRST USE
- 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.
2026-09-25 22:31:16 +08:00
BZLZHH c05e702d8c [Docs] (P12): the server's spin budget is the portable half of the idle-exit fix - 221.5 s -> 182.8 s on the same frame, no root and no code change
- 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.
2026-09-25 21:36:39 +08:00
BZLZHH 7c72dfde2b [Docs] (P12): the ~5 ms per round trip is the DEVICE's deep-idle exit, not the link and not the client - which is what the handoff measured and could not explain
- 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.
2026-09-25 21:28:13 +08:00
BZLZHH 6e7c0ce710 [Docs] (P12): the per-wait 5 ms is not the link - the same frames over adb forward move it by only a quarter, which is what the handoff measured and could not explain 2026-09-25 21:14:15 +08:00
BZLZHH f99415a493 [Docs] (P12): the atlas window's exact composition, and the negative result that kills the record-dedup idea before anyone builds it 2026-09-25 21:03:42 +08:00
BZLZHH 1d363eb109 [Perf] (MG_Remote): the client counts RECORDS SENT per op beside the waits each row PAID, and the pair says item B stopped the waiting without stopping the sending
- 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.
2026-09-25 20:52:01 +08:00
BZLZHH d057e1cb8a [Docs] (P12): the first device run of A and B - the crash path walked twice without a crash, and the atlas frame says item B is necessary but not sufficient 2026-09-25 20:46:23 +08:00
BZLZHH 170b789e7f [Perf] (MG_Remote/Transport): the per-op reply-wait split prints with every frame, because a whole-session total cannot name the rows behind one frame
- 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.
2026-09-25 20:45:18 +08:00
BZLZHH e734880cf2 [Docs] (P12): the device page names the by_op ids, and the note says where the per-op line comes from 2026-09-25 20:20:24 +08:00
BZLZHH 618395529f [Perf] (MG_Remote/Transport): the per-op reply-wait split also lands in the exit summary, because a spawn client never prints a frame line
- 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.
2026-09-25 20:20:04 +08:00
BZLZHH ce0235b190 [Docs] (P12): the three rows that keep their wait get a ruling with evidence behind it, and the handoff's open op-split question gets its numbers
- 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.
2026-09-25 20:17:22 +08:00
BZLZHH fa8b500ba8 [Perf] (MG_Remote/Transport): the reply-wait metric gains a per-op tally, and the measurement says which rows actually buy the waits
- 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.
2026-09-25 20:17:13 +08:00
BZLZHH bfd6392668 [Docs] (P12): the P5E contract records the texture half's paid-off wait, and the round's note carries the measured G1
- 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.
2026-09-25 20:00:44 +08:00
BZLZHH 3b9f536dd2 [Docs] (P12): the device-lost reply tolerance and the texture-upload wait granularity get their own record - what landed, the gates that can go red, and what is still owed
- 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.
2026-09-25 19:44:51 +08:00
BZLZHH 33e02f4bcf [Perf] (MG_Pipe, MG_Remote/Client): on the wire arm a texture upload stops owning a reply - the wait moves from every record to the SEG_STAGE window
- 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.
2026-09-25 19:44:38 +08:00
BZLZHH 096fa1a8c1 [Fix] (MG_Remote/Client): a DECLINED reply is an answer, not a corruption - the fence and query consumers stop folding status 1 into the abort, and the uncarried-bytes self-check spares a session that has latched device-lost
- 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).
2026-09-25 19:43:40 +08:00
BZLZHH c984d95205 [Fix] (MG_Remote): CapsSnapshot.apiVersion carries the backend's own API version string, so a split client's GL_RENDERER names the real device instead of the target GL version (split build only; the pull build is unchanged, 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)".
2026-09-25 17:25:21 +08:00
swung0x48 41b35536e3 [Docs] (P12): record post-review verification 2026-09-25 01:34:29 -04:00
swung0x48 0fe015886e [Docs] (Disaggregated): the top-level files become short indexes in plain language; the design text moves to design/, the how-to and reference material to guide/
- 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.
2026-09-24 22:39:46 +00:00
swung0x48 43b5cfa94e [Docs] (Disaggregated): the four index documents keep only summaries, and every phase's detail lives in its own notes/<phase>/README.md
- 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.
2026-09-24 22:21:09 +00:00
swung0x48 1fb18d9e73 [Docs] (P12 review): the status page says the P12 subset and its review round are integrated into feat/disaggregated, and that the device re-check is the next thing owed 2026-09-24 14:03:49 +00:00
swung0x48 341b43fa4f [Docs] (P12 review): ID-P12-15 - G1 holds for the review round: the pull build at cb0404f2 and at this head, same machine and compiler, has .text 0x9b1bf2 on both sides and 30614 symbols with 0 added / 0 removed 2026-09-24 14:03:48 +00:00
swung0x48 2cf3298023 [Docs] (P12 review): ID-P12-14 records the review round's host gate - the new cases' red-without-fix demonstrations, and every remaining red traced to the container's CMake/CTest, not to the fixes
- 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.
2026-09-24 14:03:48 +00:00
swung0x48 b240bd942f [Docs] (P12 review): the review round's host gate log and the container variant of the gate script
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.
2026-09-24 14:03:48 +00:00
swung0x48 b7ad6113ee [Docs] (P12 review): the P12 decision log, ARCHITECTURE §15.2's on-screen path, and the ROADMAP P12 row record the review round
- 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.
2026-09-24 14:03:48 +00:00
swung0x48 251d333231 [MG_Backend] (P12 review): Magma's renderer is dropped when a server session ends, so a session that ended without a clean teardown leaves nothing attached to the server's window
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).
2026-09-24 14:03:48 +00:00
swung0x48 74ba390db5 [MG_Backend, MG_Test] (P12 review): the ended server session's texture and sampler unit shadows are emptied with its twins, so the next session's binds are not skipped against freed twins
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).
2026-09-24 14:03:47 +00:00
swung0x48 ee456ccc40 [MG_Util, MG_Remote] (P12 review): the in-process display server forwards only its live session's own log lines to that session's client, and sends them outside the log mutex
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).
2026-09-24 14:03:47 +00:00
swung0x48 7680b78621 [MG_Backend] (P12 review): an ended server session's swap interval is forgotten, so the next session in the in-process display server starts at the driver's default
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).
2026-09-24 14:03:47 +00:00
swung0x48 29a82617e0 [MG_Remote, MG_Backend] (P12 review): a server-owned surface resizes the server's window and reports its real size, every server-owned surface keeps its geometry, and Espryt reports a window's extent only for the server's own window
- 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.
2026-09-24 14:03:47 +00:00
swung0x48 277940f39f [MG_Remote, MG_Test] (P12 review): a session asking for the window's own size after a fixed-size session waits for the layout's size instead of leasing the stale fixed one
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.
2026-09-24 14:03:46 +00:00
swung0x48 8f2a084354 [MG_Remote, MG_Test] (P12 review): a lost server window is answered before every ring record, and a stopping in-process server leaves a streaming client's queue unapplied
- 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.
2026-09-24 14:03:46 +00:00
swung0x48 cb0404f2b9 [Docs] (P12): the on-screen server window's plan, audit, harness and device evidence move into the tree, and the status page says what this line is and what it still owes - CURRENT_STAGE_STATUS records that the P12 subset is implemented but NOT handed off (both exit gates unrun: no cross-machine TCP, no FCL + kill-server device-lost), the ROADMAP P12 row stops reading "待排", and notes/p12 carries the brief, the four read-only audits, the gate and device harness, and the curated device evidence (check transcripts, replay verdicts, on-screen captures; logcat left out per notes/README.md's evidence-data rule) 2026-09-24 07:44:40 -04:00
swung0x48 29b7b284b9 [tools] (P12): trace_replay README - the two device servers, tcp_device_server.py --surface window, and the headless client's MOBILEGL_IPC_SURFACE=server --window-surface replay 2026-09-24 00:51:13 -04:00
swung0x48 e7486f8a4c [android-plugin] (P12): MobileGLServerService logs its supervisor's closed output as the service stopping, not as a failure with a stack trace, when the display Activity stops it 2026-09-24 00:51:13 -04:00
swung0x48 06dad59dbd [MG_Remote] (P12): the in-process pin line names the pin it records - MOBILEGL_BACKEND_TYPE when the environment set it, the first session's otherwise 2026-09-24 00:51:13 -04:00
swung0x48 a4a940db2b [MG_Remote] (P12): the display geometry up-call attaches the apply thread to the JVM under its own name, so the rest of the session is not logged as the up-call's thread 2026-09-24 00:40:35 -04:00
swung0x48 c2758abdf9 [MG_Backend, MG_Test] (P12): the server backend's destruction drops every Espryt twin under a transport, so the in-process display server's next session no longer adopts the ended session's programs, VAOs and buffers (split build only; the pull build is unchanged, G1) 2026-09-24 00:40:35 -04:00
swung0x48 984db45576 [tools, scripts] (P12): tcp_device_server.py --surface window|pbuffer (default pbuffer, unchanged) starts the on-screen display Activity on a lit screen, and --backend pins either server 2026-09-24 00:20:11 -04:00