Commit Graph
126 Commits
Author SHA1 Message Date
swung0x48 485dff1421 Pipe: per-framebuffer state memo, object-held handle hints, per-buffer handle reuse and a cached per-verb gate (ValidateForVerb 0.328 -> 0.282 ms/frame; FCL GL CPU Magma 2.68 -> 2.62, Espryt 3.03 -> 3.00; red-once per piece in its unit case). 2026-10-09 07:42:03 -04:00
swung0x48 0b96a3efbf MOBILEGL_FRAMES_IN_FLIGHT drives both backends and derives the split present credit (default 3 keeps credit 1; the Magma name stays as an alias). 2026-10-08 19:49:05 -04:00
swung0x48 2fbcc22410 Pipe: fire-and-forget is async - mipmap, copies, XFB span and storage-block rows to kWaitNone, inproc creates and PBO readbacks stop waiting, and the default dual block lets a barriered verb cost one wait (FCL BSL: ~40 sync points/frame). 2026-10-08 19:49:04 -04:00
swung0x48 7acf3ea8e1 Transport: MOBILEGL_IPC_WAIT_STATS logs the run-ahead client's waits per frame by record op, quiesce reason and barriered verb; the per-frame P65ClientPace line drops to debug. 2026-10-08 18:02:03 -04:00
swung0x48 c8a5abe581 [Perf] (pipe): a vertex-elements record identical to the one its VAO already published is not re-created (FCL Espryt, shadow off: host calls 3724 -> 1228/frame). 2026-10-08 12:31:57 -04:00
swung0x48 b4e209e12b [Perf] (Espryt): a shadow of each driver VAO's attribute state makes the vertex walk emit only differences (FCL host GLES calls 3717 -> 850/frame, CPU 2.78 -> 2.67 ms at cpuhunt). 2026-10-08 10:49:29 -04:00
swung0x48 748ffc31c5 [Perf] (Magma): small busy wire buffers are renamed from a host shadow instead of a staged copy that ends the pass (FCL WriteWireBuffer 0.100 -> 0.077 ms/frame). 2026-10-08 06:59:40 -04:00
swung0x48 855b3b5009 [Perf] (Magma): converted wire vertex streams are cached across frames and default-framebuffer clears stay standalone (BSL Magma 19.5 -> 77.2 fps, MobileGlues 78.2). 2026-10-08 06:59:39 -04:00
swung0x48 4984da923f [Perf] (Magma): wire clears record inside the draw pass, draw passes are cached across frames and blit sets come from per-frame pools (FCL Magma CPU 3.29 -> 2.37 ms/frame). 2026-10-08 06:59:38 -04:00
swung0x48 dc1e95b4c3 [Refactor] (P13 W6c): Magma's frontend arm goes - the frontend program source, sampled-set walk, placeholder textures, VAO draw memos, identity tables and fast path - leaving the record arm. 2026-10-06 17:55:44 -04:00
swung0x48 d61a4240ee [Refactor] (P13 W6a): the record arm is the only data arm - MOBILEGL_PIPE_DATA_ARM and its frontend-arm pins go, and the cube-face readback rules move to an integration scenario. 2026-10-06 16:16:29 -04:00
swung0x48 7fb18840dc [Refactor] (P13 W5 S8): MOBILEGL_BUILD_RECORD_ARM is folded away - the record arm is unconditional - and retired_switches keeps it from coming back. 2026-10-06 15:03:11 -04:00
swung0x48 1961029043 [Feature] (P13 W5 S7): MOBILEGL_PIPE_PERSISTENT_MPROTECT=0 keeps the tracker's SIGSEGV handler out, and the mprotect arm builds only on Linux and Android. 2026-10-06 13:17:25 -04:00
swung0x48 b1978890b4 [Refactor] (P13 W5 S5): the data-arm predicates, their latch and MOBILEGL_PIPE_DATA_ARM follow the record arm instead of the transport. 2026-10-06 13:00:28 -04:00
swung0x48 b13af729e4 [Refactor] (P13 W5 S5): the record arm's three knobs move from IpcTable to RecordArmTable, which every record-arm build parses. 2026-10-06 12:01:49 -04:00
swung0x48 af84eac8ee [Feat] (P13 W4a): Espryt monolith takes the record arm for buffers, indirect, client arrays, binding points and fp64 through an in-process verb port 2026-10-06 08:14:37 -04:00
swung0x48 081854653a [Feat] (P13 W4): MG_Config::DataArmIsRecord and the W4-W6 MOBILEGL_PIPE_DATA_ARM knob 2026-10-06 08:14:36 -04:00
swung0x48 30ebae4346 [Feat] (P13 W3b): subsystem bits 0-13 are fixed on and refused by name at startup, the legacy-memo and texel-retain knobs are gone 2026-10-06 04:04:48 -04:00
swung0x48 7184a40a47 [Refactor] (P13 W3b): strip the push/legacy-memo guards from MG_State, MG_Util and the config 2026-10-06 04:04:43 -04:00
swung0x48 22266c47cf Merge client-recover: open a fresh server session after a device loss. 2026-10-02 23:38:56 -04:00
swung0x48 7823bcd987 [Fix] (Client): recover a fresh server session after a device loss so applications can recreate their contexts 2026-10-02 23:34:21 -04:00
swung0x48 bfa7987ca6 [Fix] (Magma): decline programs whose final SPIR-V fails validation instead of handing the module to the driver 2026-10-02 23:09:58 -04:00
swung0x48 6c742eba8a [Build] (Config): bump the version to 26.10 2026-10-02 19:43:47 -04:00
swung0x48 378020adf3 [Fix] (MG_Remote, P11 T0): the T0 POST also checks what the CPU writes through the held lock after the import and its first GPU use, in both directions - Mali-G1-Ultra passed the old single round (the import cleans the CPU cache) while its GPU saw 0/1024 post-import words, so T0 was granted and create-indirect-in-world-align1024 x Magma lost every Flywheel object on inproc/spawn (0.867/0.848); it now fails there and runs T2 (0.999984), and the Redmi passes both rounds and keeps T0 2026-09-30 10:21:46 -04:00
swung0x48 b32140e614 [Feat] (MG_Remote, P11 B2): T0 is the default - an unset MOBILEGL_IPC_ADOPT_TIER asks T0 (ask 0x80) and falls back to T2 quietly (MGLOG_D, still counted: client T0Fallbacks, server T0Refusals), while an explicit 0 keeps every named line; revision 5's schema comment and pin fixed up 2026-09-29 20:15:29 -04:00
swung0x48 590d74525d [Feat] (MG_Remote, P11 B2): T0 behind MOBILEGL_IPC_ADOPT_TIER - control revision 5 (LinkTerms.adoptTier ask), kCapAdoptT0 granted at the first native bind after the backend's POST, the AHardwareBuffer Offer on the aux socket matched to its map_persistent record by seq, named T2 fallbacks instead of A1's handshake Fatal, the server allow switch, done-no-bytes CPU reads 2026-09-29 17:40:56 -04:00
swung0x48 c371599811 [Merge] (P11): dev@c14283ee - scattered sub-image writes upload exactly their texels, Magma submits a frame's recording every 16384 draws with backpressure, and Magma grows descriptor pools geometrically and trims a quiet slot whole; the handle arm notes driver-side writes by handle and the wire arm splits recordings and keeps the trim's epoch peak across descriptor rewinds 2026-09-29 12:11:37 -04:00
swung0x48 53fa16c2c2 [Fix] (Magma): a frame slot whose descriptor pools stay under a quarter full for 120 frames is handed back whole - its set cache cleared, grown pools destroyed, base pool reset and every memo holding a set handle dropped - so rd12's loading burst no longer lives for the whole process (live sets 15,042 -> 39, pools 65 -> 6); MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES (0 = never) and a test-visible pool census 2026-09-29 10:34:16 -04:00
swung0x48 d20c2d01ae [Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded) 2026-09-29 09:44:58 -04:00
swung0x48 82467823ef [Docs] (P11 C): the contract's A1/A2 sections, Config.h's GPU-ordered landing, Init.cpp's DirectVulkan resource table, and the stream refusal CONTRACT-P6 / design 06 / 07 claimed now cites its code 2026-09-29 07:23:41 -04:00
swung0x48 bcff9b00cc [Feat] (MG_Remote): fence polls are answered from the server's reports - the apply thread posts kEventFenceSignaled as the GPU finishes each client fence (and submits and re-checks while idle), glClientWaitSync(0) / glGetSynciv answer locally, and only every MOBILEGL_IPC_POLL_ESCALATE-th unanswered poll crosses 2026-09-29 03:59:44 -04:00
swung0x48 6485ae1dc0 [Feat] (MG_Remote): a read into a pixel pack buffer crosses as read_pixels_to_buffer / get_texture_image_to_buffer and waits for nothing - the server lands the rows through the buffer-write gate on both backends, the client marks the buffer GPU-written, and a device lost with the write still owed ends in Fatal{ReadbackDeclined, "buffer-writeback"} 2026-09-29 02:02:29 -04:00
BZLZHH 3a6c5c7f04 [Perf] (MG_Remote): The create window ships at 10, and the server now reports where its own frame time goes. 2026-09-26 15:30:48 +08:00
BZLZHH 102f2ca7d5 [Perf] (MG_Remote/Client): The create window's shipped depth is 6, and its ceiling 8. 2026-09-26 11:26:54 +08:00
BZLZHH 3a2555805a [Perf] (P12): the create window lands as the default - declared-answer retention replaces the ring, and the load frame goes 28.66 s -> 16.52 s (11.02 s at window 4)
The three things that made the deferral inert are all fixed, in the order they were
found: the reply ring could not hold an answer across the ~1,000 records between two
creates; a residency budget could not help because what displaces an answer is what
the SERVER sends, not what the client emits; and so the mechanism had to be retention.

WHAT LANDED
  * ILink::DeclareReplyRead + LinkCapabilities::RetainsReplies. The wire declares a
    seq will be read BEFORE it publishes, so the declaration cannot race its own
    answer; a link with the capability false ignores it, and the window does not
    defer on one (CreateWindowEffective() is gated on it).
  * StreamLink's ring of the last 8 answers becomes a declared-answer store: a
    monotone whitelist plus a cursor, answers kept until their reader TAKES them
    (take-and-delete under one lock, bytes swapped into the scratch), and a capacity
    of 24 that is FATAL to exceed rather than evicting. An evicted wanted answer is
    unrecoverable - re-latching by sending another create wipes the server's record
    (PipeApply.cpp:2033) and its PendingUploads (PipeApply.h:332) while the client
    cleared its dirty flags at emission (PipeApply.h:298-304) - so the capacity is
    sized so it cannot be reached and reaching it says its own name.
  * An answer nobody declared is DROPPED, which is what keeps the store bounded by the
    client's deferral depth instead of by the server's answer rate. Retain-on-arrival
    grows ~11.3 entries per create against a server that ignores the no-reply bit.
  * A read that fails after WaitForApplied has returned Reached is now Fatal{ReplyMissing}
    naming the seq: on the stream arm the reply frame strictly precedes the progress
    frame that publishes appliedSeq, so that combination is a broken invariant, and
    retrying it (what the row used to do) wedges the window for the rest of the
    process while leaving every deferred object latched on a provisional accept.
  * The residency budget is deleted (WireTables.cpp keeps its history), the window
    retirement branch drains once before falling back - it could otherwise strand the
    answers it was holding - and Detach clears the store.

MEASURED ON THE DEVICE, load frame, both ends rebuilt, frame-sent identical per op
(create 4,536, records 103,097) throughout:

    window 1 (R-5's shape)              4,536 takes   28.66 s
    window 2 explicit                   2,500         17.27 s
    window 2 AS THE SHIPPED DEFAULT     2,563         16.52 s
    window 4                            1,644         11.02 s
    window 2 against an OLD SERVER      2,663         16.38 s   (zero Fatals)

The last row is the version-skew case the design review named as a must: a server
built before the no-reply bit answers ~55,903 records a frame, the whitelist drops
every answer nobody declared, and the deferral still reads its own back.

GATES (172 + 15 green), and the important one is proved to go red:
  * ADeclaredAnswerSurvivesFarMoreInterveningAnswersThanAnyFixedBuffer - reverting the
    store to "keep the last 8 arrivals" fails it immediately with "the declared answer
    did not survive 64 intervening ones". That is the line between the ring-1024
    stand-in and a bounded mechanism.
  * AnAnswerNobodyDeclaredIsNotKeptAtAll - red if everything is retained.
  * TheWindowRefusesToDeferOnALinkThatCannotRetainAnswers - on the in-process (ShmLink)
    arm the knob at 4 must still block every create, red if the capability gate goes.
  * The refused-create case now reads the repair that exists on THAT arm (the latch),
    and asserts the window's own books are empty there.

NOT DONE, and named: the window has no unit gate on the STREAM arm yet - only the
device runs. Three rounds of this feature were green in the lanes while inert on the
device because the lanes and the device run different transports; the transport-level
retention gates close that for the store, and the window-level one should be built on
StreamClientPublicationTest's real stream session next.
2026-09-26 03:31:14 +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 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
swung0x48 d481393f8f [MG_Remote, MG_Impl, MG_State, Config] (P12): MOBILEGL_IPC_SURFACE=server - headless window surfaces (NULL accepted), one ServerOwned CreateWindowSurface and no SetWindowHandle, the server's geometry into the EGL state before the create returns, and every refusal named on the client 2026-09-23 23:55:23 -04:00
swung0x48 38ac5dbd2d [MG_Remote, docs] (P7): record PH-6's debts in CONTRACT-P7 and fix the stale event-ring knob text 2026-09-23 13:44:37 -04:00
swung0x48 3d0f531170 [MG_Remote, MG_Test, docs] (P7): forfeit the reverse channel on a non-draining peer (PH-6) 2026-09-23 13:44:13 -04:00
swung0x48 2ab757792c [MG_Remote, MG_IntegrationTest] (P7): surface ops that may bring the spawned server's backend up wait a cold-start reply budget 2026-09-23 13:23:09 -04:00
swung0x48 f0356dbe61 [MG_Backend, MG_IntegrationTest] (P7): the wire watermark's sync point also fires above 1024 parked stores, so small orphans that never reach the byte budget are bounded by count
Measured on bsl-esc-menu-854 (spawn, lavapipe) with the byte watermark alone: the dead VkBuffers
at the end fell from 25,935 to 32, but one stretch with no submission still parked 12,498 stores
in 39.5 MB - under 64 MiB, so nothing forced a sync. A fixed ceiling
(VkBufferManager::kWireDeferredCountCeiling = 1024, not a knob) now rides the same
WaitForSubmitIndex call; MOBILEGL_IPC_WIRE_DEFERRED_MB=0 still turns both triggers off.
MagmaWireReclaimScenario gains the case: 3000 respecify-and-draw rounds of a 256-byte store in one
frame (750 KB, far below the lane's 8 MiB). Red before this commit on inproc and spawn (live peak
3001 vs a bound of 1027), green after (1025, two forced syncs).
2026-09-22 21:46:08 -04:00
swung0x48 c569c916c0 [MG_Backend] (P7): when the orphaned wire buffer stores still parked exceed MOBILEGL_IPC_WIRE_DEFERRED_MB (64 by default), the server takes the host-access sync point mid-frame and reclaims them all
The defer-path sweep frees an orphan once its last submission has retired, but inside one long
frame a store that was drawn from is named by recorded, unsubmitted work, and nothing submits it
until the frame ends. The knob is read once through MG_Config::Ipc like its siblings; above it
the park calls WaitForSubmitIndex(GetSyncPointSubmitIndex(), flush) - the call
WaitForWireBufferHostAccess makes - and sweeps again, which empties the list because no entry can
be tagged past the sync point just waited out. A sync point that cannot complete dies through
the existing Fatal{ResourceUnavailable, "buffer-write-sync"} site, now one helper shared with the
host write (fatal_census: 79 abort sites, unchanged). 0 is the negative control.
2026-09-22 21:44:06 -04:00
swung0x48 df17c416c0 [MobileGL, MG_Backend, MG_Remote, scripts] (P7): the split wire comes up before the first frontend object, and the caps mirror stops deciding a family's fate from a placeholder
F1. `MG_State::Init()` runs before `MG_Backend::Init()`, and the second is what
starts the session - so `GLContext`'s `TextureState` constructor materialised its
eleven default texture objects, each of them emitted `resource_create` through
`FamilyIsLive`, and `CapsMirror::ServerConsumes(0x400)` answered every one of them
from a generation-0 placeholder with `callMask=0`. All eleven records were dropped
and the whole event was one WARN line:

  MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
  family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
  caps generation 0

It is NOT a race. The line reproduces byte-for-byte on lavapipe and on the Adreno
830, because the two Init calls are two statements on one thread. And "the legacy
pull path runs for it" is false on the client under split: there is no pull path
on this side of the wire, so withholding is not a fallback, it is a drop that only
a later respecify's self-healing create ever undoes.

Two of the three options the brief offered do not apply, and both facts are worth
recording. (b) "extend the P6.5 fix to inproc" is already the tree: `FinishStartup`
step 7 is the one funnel every transport reaches and it will not return without a
real snapshot. (a) "a bounded wait in Consumes()" cannot work alone and would
deadlock: at `MG_State::Init()` no handshake is in flight, and the one that will
land is started later BY THE SAME THREAD.

So the fix is the ORDER, with the rule behind it as the gate:

  * `MobileGL::Initialize` calls `MG_Backend::InitSplitRolesBeforeState()` before
    `MG_State::Init()`. `Init()`'s split arm becomes one body, `InitSplitBackend()`,
    shared with the direct callers that still reach `Init()`; the monolith arm keeps
    its place after `MG_State::Init()`, because moving it would move pull `.text`.
  * `CapsMirror::ServerConsumes()` refuses to answer from a placeholder. It waits,
    bounded, when a bring-up is genuinely in flight on ANOTHER thread, and otherwise
    dies as `Fatal{CapsBeforeFirstSnapshot, "<family>"}` through `SessionFail` -
    a new family word, one row in `FatalFamilies.def`, no new abort site.
  * Two arms are exempt and say why in place: the server arm (`MGPipeServerArm()` -
    the apply thread and the spawn server, where the client's mirror is the wrong
    object rather than a late one), and a process whose own configuration never
    asked for a split transport (`MG_Config::SplitTransportRequestedByConfig`, set
    by `MG_ConfigLoader::InitTransport`, which is what tells a real run apart from a
    fixture that assigns `MG_Config::Transport` by hand).

The read accessors keep P5's ruling untouched: `LogBackendInfo` reads
`GetRendererInfo()` during `MG_Backend::Init()` and an abort there is a process that
cannot start. Only the DECISION changed. The warning was not made quieter and the
callMask was not defaulted to all-ones.

`MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` (server side, disaggregated builds, inert unless
set) holds back the FIRST CapsSnapshot. On spawn/tcp that is a genuinely late
snapshot in another process and it exercises the client's step-7 wait; on inproc it
only makes `Start` slower, which is itself the proof that the only window a client
can read a placeholder in is the one before `Start` is called.

G1: `.text` 0xa52203 and the 30570-symbol list identical to p7-before/pull-syms.txt.
Census: 79 abort sites unchanged, 44 family words (was 43). Link ratchet: 186,
unchanged.
2026-09-22 15:55:08 -04:00
swung0x48 018fab0f5b [WIP] (MG_Remote): implement P6.5 TCP and stream transport 2026-09-22 07:19:18 -04:00
swung0x48 a63577354d [MGPipe] the Fatal texts no longer claim P5 does not chunk
R-10's "P5 does not chunk (R-10)" wording predates the stage chunk budget: buffer
content ranges are cut at MGPipeStageChunkBytes() (1e7c372e) and a texture level
at whole-width slabs (9469d48e), so the claim is false for the two rows that can
outgrow SEG_STAGE - and these messages are what a bring-up reads.

- PipeWireEncoder::StageAllocate's two Fatal{RingOverrun, "SEG_STAGE"} sites now
  say what reaching them means: a blob is staged whole, a row that cuts its
  content at the stage chunk budget never arrives, so this is a record type with
  no cut or a piece still larger than the arena.
- EncodeRecord's MaxRecordBytes arm says the bound is about the record's own
  bytes - the stage chunk budget cuts blobs, not a header, payload or inline
  tail - and keeps the integrator decision it names.
- Comments in PipeWireCodec.h, EmitTables.cpp's maxrecop line, Config.h's
  STAGE_MB knob, PipeStats.h's MaxRecordBytes gauge, SplitRuntimePeek.h and
  WireLedgerChecks.h said P5 does no chunking; they now say what is cut and what
  is not.
- PipeWireCodecTest.ARecordLargerThanHalfTheRingIsFatalRingOverrun asserts the
  new diagnostic text instead of "does not chunk (R-10)".
2026-09-20 19:39:51 -04:00
swung0x48 cfcc12f73a [MG_Backend, MG_Impl, MG_Remote, MG_Test, MG_IntegrationTest, CI] (Disaggregated): P5f f1 - the dual-block rehearsal: per-role PipeInputs, the MOBILEGL_IPC_ROLE_SPLIT_STATE knob, and the named-Fatal arm that makes the red a census 2026-09-19 09:59:33 -04:00
swung0x48 5ef4701294 [Config, MG_Backend, MG_Test] (Disaggregated): P5e c0e - the two knobs and the caps arm
- IpcTable::RunAhead (MOBILEGL_IPC_RUN_AHEAD, default 1, forced 0 under PipeVerify beside
  BatchWaits) and IpcTable::PresentCredit (MOBILEGL_IPC_PRESENT_CREDIT, default 1, range
  1..8). Both parsed unconditionally like every other IPC knob and both in the IPC log
  line, because a knob that silently means nothing on one arm of an A/B is how an A/B
  stops being one.
- RUN_AHEAD=0 is an A/B CONTROL and not a negative one: identical server code, identical
  records, identical fill decision, and only the client's wait differs. VERB_BARRIER=0
  keeps its own meaning as the lockstep arm's negative control.
- MG_Backend/Init.cpp: kMGPipeP5eRunAheadReady = false, and the caps arm goes through
  MGPipeRunAheadCapBitsFor so the "DirectGLES only, and only once the integration commit
  flips the constant" decision has one spelling. A constant and not a knob: an operator
  must not be able to turn a half-migrated server into a run-ahead one, because the
  failure mode is a torn read that renders wrong rather than aborting.
- MagmaPipeIdentityTest pins both halves - no bit 10 for DirectVulkan whatever the
  integration constant says, bit 10 for DirectGLES only when it is true, and that one bit
  and nothing else.
- The push default becomes kMGPipeSubsystemsMigratedAtP5e (0x3fff). Inert: bit 13 is not
  in kMGPipeWiredSubsystems, so no emission gate opens on it; 0x1fff survives as P5e's
  "everything P4a had and nothing of mine" control.
2026-09-18 06:26:32 -04:00
swung0x48 cb06538c06 [MG_Remote, MG_Pipe, MG_Impl, MG_Test, MG_IntegrationTest, Docs] (Disaggregated): inproc performance package - dirty-driven persistent-map push, batched barrier waits, spin-clock batching
VD12 Minecraft on the Redmi (FCL, DirectGLES): the split's in-world frame rate
13 -> 60-66 fps against monolith's 110-114, and persistent-map wire bytes
25 MB/frame -> ~0.23 MB/frame. The residual split-specific costs are named at
the end, not hidden: the R-1 serialization (WaitForApplied ~12%, P11's
versioned-inputs design) and a split-only mutex complex still under
investigation.

Persistent-map push, three arms picked per buffer at map time:
- mprotect dirty tracker: the mapped range's page-aligned interior is
  protected PROT_READ; the app's next write faults, the SIGSEGV handler marks
  the page in the slot's bitmap and unprotects, and the push ships one 64 KB
  block per block containing a faulted page. Ownership is three tests -
  SEGV_ACCERR only (a userspace raise and an unmapped page chain verbatim),
  inside a live tracked range, and not a refault on a still-set bit (a write
  we unprotected succeeds, so that is a non-write access and it crashes
  honestly). The alignment is INWARD: an outward-aligned edge page holds
  foreign bytes, and a signal-blocking worker thread faulting on one is a
  process kill - that was the ctest-only SmallRing segfault. Edge spans
  (at most two per map) are hashed per push and ship their containing block
  only on change (unconditional edges measured 112 MB/frame). Re-arm is one
  mprotect per contiguous run. The first push ships everything once.
- hash-suppress arm (MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS, default 1):
  per-block XXH3 against the last push's hashes; untracked buffers only.
- PushDrawConsumers: only the buffers the upcoming draw/dispatch can read
  (VAO attribs/element, indexed binding points, indirect/parameter slots,
  TexBuffer backing - the last also retires the per-frame-cadence cloud
  twitch), plus the fault-epoch skip: an unmoved epoch proves no interior
  byte changed, tracked edges are hashed in place, and any hash-arm member
  vetoes. The membership map caches the tracker slot pointer (a 64-slot
  rescan per buffer per draw was measured and removed).

MOBILEGL_IPC_BATCH_WAITS (default 1; MOBILEGL_PIPE_VERIFY forces 0): a
value-class record (kCtxState/kCtxCso/kCtxObject with no reply slot) is
published without waiting for its own apply; the barrier is taken at the next
pull-reading verb, the only place BARRIER-PULLED fields are read, so the R-1
fence posts are untouched and only the round trips between them change. Two
consequences are pinned rather than argued: kCtxObject verbs the tests read
synchronously get explicit fences (GenerateMipmapFieldsCross,
FramebufferDeathCrosses), and a frontend object whose last SharedPtr drops on
the apply thread - impossible before batching - now enqueues its
(kind, lifetimeId) on the deferred destroy queue and the GL thread replays
the DestroyAndFree helper at the verb hooks (CopyImageLayeredScenario aborted
Fatal{RoleViolation, MGPipeSlots} without it).

Doorbell::Wait's spin reads the clock once per 64 yields: on the workload
device a steady_clock read is a real syscall (no vDSO) and the per-iteration
reads were ~20% of the whole process at ~1000 waits/frame.

Gates: gen_pipe --check/--self-test green; unit 2188/2188;
integration-split 110/111, the one red being GenerateMipmapDepthPixels whose
DirectGLES and DirectVulkan MONOLITH siblings flake identically with every
code path of this package provably inert (PushIsArmed false, no ClientSession,
deferral's monolith early-out).
2026-09-18 01:58:59 -04:00
swung0x48 7417bfcfbf [Feat] (Config, ConfigLoader): parse MOBILEGL_TRANSPORT and the MOBILEGL_IPC_* family entirely behind the disaggregated option, so the pull build's symbol set cannot move 2026-09-11 13:05:05 -04:00