Files
MobileGL/docs/Disaggregated/notes/p5e/reports/fb-v1.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.

1. P6's plan and contract draft (neither started; not one line of code)

     docs/Disaggregated/P6-SPAWN-PLAN.md      the package plan
     docs/Disaggregated/P6-CONTRACT-DRAFT.md  the contract draft (moves to
                                              MG_Remote/CONTRACT-P6.md when c6 lands)

   The three findings that mattered, all written into the documents:

   a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
      the tree and already tested: SocketDoorbell including hangup detection and the two
      lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
      passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
      protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
      SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
      assembly plus a control plane plus a process, not "write a socket transport".

   b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
      audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
      the tree again. So the first package, a6, is a read-only audit that writes no code, and
      every contract row that depends on it is marked PENDING a6 rather than decided in
      advance.

   c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
      and protocol.fbs:186-196's own comment says Android transfers the window out of band -
      that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
      the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
      gate measures. A real window reaching the server is a named refusal. This gets its own
      section because "P6 is done" would otherwise read as "the game runs now".

   The contract adds rule G: a role may not name a process-local handle. It extends P5c's
   rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
   EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
   a token this contract defines or they do not cross, and a descriptor crosses only through
   SCM_RIGHTS, where the kernel renumbers it.

   What P5e left for P6 gets its own section. The latch semantics are already designed
   (ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
   client no longer waits while the peer process can DIE: the client holds records that were
   published and will never be applied, and Android's freezer suspends the child without
   closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
   That is P6's only new design; everything else is carriage. It shares a predicate with E1's
   outstanding re-specification (ID-122), and the two should be designed together.

   Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
   match integration-split's name for name, because a differing set means the lane is quietly
   running less; and the arm-proof gate requires every entry to record the child pid and
   transport=spawn in its OWN private log - ID-124 established that this kind of false green
   really happens, and a spawn lane that actually ran monolith would pass every other gate.
   S2's shape must be settled before it is written, and the document says why: E1 is the
   cautionary case.

2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)

   ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
   the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
   is under no version control. A record cited hundreds of times that lives only locally is not
   a record, so it moves in whole, with its directory structure intact.

   Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
   stay where they are: they are evidence DATA rather than documents - large, regenerated per
   run, and mostly carrying absolute paths and device serials that mean something only on the
   machine that produced them. The mapping and this split are written in
   docs/Disaggregated/notes/README.md.

   Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
   document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
   material salvaged from failed workflow runs, which may duplicate the like-named phase
   directories and is kept because a few of those were the only complete version at the time.

   CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
   deliberately outside it: those notes' file:line citations were written against their own
   code heads, and resolving them against today's HEAD would fail wholesale - which is not an
   error, it is their correct state as a historical record. To check one, use the revision it
   declares (--rev).

3. Inconsistencies fixed in passing

   - README.md's status line still said P5e was in progress while the stage table already said
     closed.
   - CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
     (a) and (b) were run in the previous commit; rewritten to closed plus the one item that
     actually remains (E1).
   - ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
     package order, the scope boundary and the two new gates.

Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
2026-09-18 23:31:39 -04:00

216 lines
15 KiB
Markdown

# P5e package `fb` — framebuffers, attachments, images, blit (report v1)
Branch `p5e/fb`, head **`ca1beb69`**, base `f6cfcbd3`. Tree `~/w7/p5e-fb`. Six commits; the fourth
(`f4675d1a`) is a SCAFFOLD that the sixth (`ca1beb69`) reverts — see §5.
## 1 What changed, per file (ranges at the NEW head)
**`DirectGLES.cpp`** — `:242-264` the doors (`RefuseFramebufferBindingSlotRead`,
`FramebufferRecordArmIsMandatory` = `Transport != Monolith && FramebufferSubsystemEnabled()`,
ruling 1 / §5.8, which selects every arm below). `:2171-2272` the list keys: a new
`FboAttachmentSyncEntry {Res, backend}`, six statics, `SyncRecordAttachmentTextures` — **I did NOT
touch `UnitTextureSyncEntry`**; tx2 owns its re-shape, the attachment lists borrow nothing, and a
type of their own avoids the collision. `:2344-2412` `SyncFramebufferAttachmentTexturesByRecord`
(the core), selected at `:2573`. `:2726-2968` images: `ResolveShaderImageRecord` and
`g_imageRecordSeamIsAuthoritative` DELETED; `IssueImageTextureBind`, `GLAccessForImageView`,
`IsWritableImageBufferView`, `SyncImageTextureBinding(const MGPImageView&)`,
`NoteImageUnitBoundWithoutReadingTheFrontend`. `:2976` the GPU-write marks. `:3055-3180` the sweep
walk and its gate. `:3290` `FramebufferRecordMatchesBinding` monolith-only. `:3428-3543`
`SyncCurrentFBOByRecord`'s loop; `:3571` / `:4781` the two declines; `:4920`
`SyncAndBindFramebufferByHandle`; `:4956` `ForceBindCurrentFBO`; `:6570` `Clear`'s debug
diagnostic. `:8609-8806` the record blit overload + `PushedReadBufferPoint` + `BlitEndpointRecord`,
`:8850` named arm, `:8955` bound arm. `:9409-9484` the detach walk. `:10118` the RBO refusal.
`:10561` `BindImageTexture`.
**`Managers.cpp`** — `:4374` `PipeTextureTargetForHandle`; `:9662` `PushedSurfaceForAttachment`
exported; `:9675-9820` `SyncAttachmentSurface` loses its attachment parameter and both
cross-checks; `:9951-10035` `ApplyReadBufferFromRecord` + `SyncReadBufferToBackendByHandle`;
`:10331` the record-side empty-point test; `:10525-10760` the reverse index +
`BackendFramebufferObject::SyncToBackendByHandle`; `:11228` `BoundImageUnitFormat`;
`:14025-14105` `BackendRenderbufferObject::SyncToBackendByHandle`; the two fb stubs deleted.
**`Managers.h`** — six declarations (§6.4, §6.8). **Tests** — `FramebufferEmitTest.cpp` +2,
`ImageEmitTest.cpp` +1, all three in the X-macro skip lists (G2/G14).
## 2 Kimi-audit rows 68-90
**RETIRED** (no frontend read under a transport): 68 (monolith-only), 69, 70 (`NoteStateForHandle`
deleted), 71, 74, 76, 77, 78, 79, 80, 81, 82, 85, 86, 87, 88, 89, 90.
**LEFT as monolith glue**, unreachable under a transport because `SyncCurrentFBO` / `BindCurrentFBO`
abort before them: 72, 73, 75, 83.
**NOT MINE:** 84 (`SyncCurrentProgram`'s fragColor-broadcast half). Its record arm exists and is
what a transport takes; its decline arm still reads the binding slot, and that decline is in **pg's**
line range. A framebuffer row inside a program function — named so the integrator can rule whether
pg refuses it or fb widens. Nothing in the split lane reaches it.
**NOT FOUND:** none. Every row 68-90 exists at this head.
## 3 The rulings I was asked to answer
- **ID-94 / ruling 16** — done. `Access` decodes through `MGPipeValueTypes.h`; the stale "the
encoding does not exist at the contract commit" comment is deleted and corrected in place.
- **ID-86 / ruling 7** — done: `(ShaderImagesSerial, TextureShutterSerial, ContextSerial,
g_backendContextGeneration)`, in their own three fields beside the frontend pair. The old
comment's argument is KEPT because it is what decides which values are admissible: an applier
serial moves when the CLIENT said something, strictly before the sweep the saying provoked, so
the "re-minted inside this sweep" property survives the re-key. Never a backend re-mint counter.
The window/high-water union is kept and re-argued in place.
- **`SyncReadFramebufferTextureAttachments` ungated on the framebuffer bit** — the delta is
SMALLER than S3 feared. The function is still not gated on the bit; what selects the record arm
is the TRANSPORT plus the bit. So the object A/B moves only in its SPLIT arm, and the
push-monolith arm is token-for-token what it was. **It still needs re-baselining for the split
arm** — integrator, with the phase's other re-baselines.
- **The renderbuffer cross-check (base `Managers.cpp:9711`)** — fixed by DELETION, not gating
(§4.4's instruction). **A latent RULE violation, not a latent wrong-picture bug**: the probe ran
on the apply thread under split while its texture sibling was transport-gated, but inside
`MGPipeFrontendKeyedRegistryScope` with the client parked, so its answer was correct. It becomes
a correctness bug the instant the record is unbarriered — which is why it is deleted, not gated.
## 4 Red-once evidence (executed, quoted verbatim)
**R1 — `SyncAttachmentSurface` takes the frontend attachment again** (reverted
`SyncCurrentFBOByRecord` to `SyncToBackend(currentFBO, target)`, the only way the attachment sync
can be handed one). Full strict lane marker tables: with the arm, no `GetFramebufferBindingSlot`
marker at all; with it reverted, `2 Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}`.
The private log's line:
```
MGPipe: Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"} [BARRIER-PULLED, MOBILEGL_IPC_STRICT_ERRORS=1, retires in P5e (Espryt unbarriered), P7 (Magma)]
```
**R2 — the draw-FBO list key back to the pointer compare**, id's hook armed
(`kMGPipeP5eClientWaitRuleLanded = true`), on
`ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels`:
```
MGPipe: Fatal{RoleViolation, "MGPipeSlots"} - the apply thread called MGPipeSlots().HandleOf. ...
A named exemption scope admits it only while the record being applied is BARRIERED (barriered=0) ...
#3 MGPipeRefuseAllocatorFromApplyThread #4 BackendSlotTable<ITextureObject>::Find
#5 TextureImpl::SyncTextureObjectToBackend #6 TextureImpl::SyncNeccessaryTextures
#7 DirectGLES::Clear #8 ServerVerbSink::OnClear
```
Control (key restored, hook still armed): the entry passes `OnClear` untouched and aborts later at
`#5 VertexArrayImpl::ResolveVaoTwin #6 PrepareForDraw #7 DrawArrays #8 OnDrawVbo` — **vi's** site.
The isolation is by FRAME, because with the hook armed every unlanded family aborts somewhere; it
becomes a plain before/after once vi and tx2 land.
**R3 — the image `Access` mapped 1↔2.** (a) the SHARED table (`WriteOnly = 2, ReadWrite = 1`):
```
../MobileGL/MG_Test/Pipe/ImageEmitTest.cpp:511: Failure
Expected equality of these values: Which is: '\x2' (2) Which is: 1
../MobileGL/MG_Test/Pipe/ImageEmitTest.cpp:512: Failure
Expected equality of these values: Which is: '\x1' (1) Which is: 2
```
(b) the SERVER decode alone (`GLAccessForImageView`): `Split.UnboundImageDescriptorScenario` 13/13
and the whole split lane 111/111 stay GREEN — **lavapipe does not enforce `glBindImageTexture`'s
access**, so the unit case is the only witness that exists here, which is why the brief asked for
it. `FormatlessImageBakeScenario` / `ImageLoadStoreSsoScenario` are MONOLITH-lane entries with no
split twin: they run the frontend arm and cannot reach the decode at all.
**R4 — the named blit back on the frontend objects: DID NOT GO RED**, and I stopped after two
attempts rather than manufacture one. `Split.NamedBlit.F1WireScenario` 4/4 green with the revert.
Three measured reasons: both endpoints of those cases are BOUND (hence synced) before the blit, so
the twin is already configured and the state note is not load-bearing for them; the
layered-destination substitute is gated on `MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer`,
which lavapipe does not trip, so the aspect plan's body never executes in this lane on EITHER arm;
and with id's hook armed the entry aborts earlier at tx2's unlanded unit walk
(`#5 SyncTextureObjectToBackend #6 SyncNeccessaryTextures #7 Clear`). What IS shown:
`Split.NamedBlit.F1WireScenario` (4) and `Split.CopyImageLayeredScenario` (6) are green with the
record-driven blit — the substitution is behaviour-preserving. **Ruling wanted**: whether a
device-lane witness is required before this arm is accepted.
**R5 — the per-point version memo as a second gate: the proof is that it is ABSENT and the lane is
green.** `SyncToBackendByHandle` has no `m_syncedFrontendAttachmentVersions` at all (the record
hash plus `g_attachmentBackendIdGeneration` are the only gates) and `integration-split` is 111/111,
including `Split.F1.F1WireScenario` (13) and `Split.CopyImageLayeredScenario` (6). The
re-storaged-attached-RBO shape S3 named for `P4aSeamAuditScenario` F-3 is covered by the existing
`FramebufferEmit.ARestoragedAttachedRenderbufferPublishesItsNewExtent`, still green.
## 5 Gate
`build` rc=0 · `unit` **2209/2209** (2206 before the three new cases) · `isplit` **111/111** ·
`strict` 3/111 · `gens` all seven rc=0 · `one` **77/77** on `Split.{F1,NamedBlit,Ct}`,
`Split.CopyImageLayeredScenario`, `Split.UnboundImageDescriptorScenario`, `FramebufferEmit.`,
`ImageEmit.` · seam greps SILENT (the image seam log is deleted; no framebuffer "does not describe
the binding it names" line in any of the 88 private logs). The one `gens` doc-citation complaint
(`CONTRACT-P5.md:524`) is PRE-EXISTING — verified with `--rev f6cfcbd3`.
**Strict markers, base (22 private logs) vs now (88; the slice is four times larger because fb's
field no longer aborts first on those entries):**
| marker | base | now | owner |
|---|---|---|---|
| `GetFramebufferBindingSlot@Clear` | 6 | **0** | fb — retired |
| `GetImageTextureBinding@BindImageTexture` | 0 visible | **0** | fb — surfaced at 13 mid-package, fixed (§6.8) |
| `GetTextureUnitObject@Clear` | 13 | 18 | tx2 |
| `GetTextureUnitObject@GenerateMipmap` | 2 | 0 | tx2 |
| `GetBoundVertexArray@DrawArrays` | — | 14 | vi |
| `GetProgramForDispatch@DispatchCompute` | — | 7 | pg |
| `ValidateProgramName@ShaderStorageBlockBinding` | — | 2 | pg (barriered, §7 allowlist) |
| `GetTextureObject@CopyImageSubData` | — | 6 | P7/P9 (barriered) |
| `GetTextureObject@Clear` | — | 39 | **the scaffold's own cost**, gone with it |
**THE HEAD DOES NOT PASS `isplit`, BY CONSTRUCTION.** fb calls tx2's
`SyncTextureToBackendByHandle` / `SyncMipmapsToBackendByHandle` from four places. With c0e's
aborting stubs back, every entry whose framebuffer has a texture attachment dies on
`Fatal{UnmigratedVerb, "TextureImpl::SyncTextureToBackendByHandle"}`. Every number above was taken
at `fcd7c5c8`, where `f4675d1a` bodied the two seams by resolving the frontend texture from the
record's own `Desc.GlNameForDiag` through the `GetTextureObject` sticky forward — correct under
lockstep, deliberately NOT tx2's design, reverted at `ca1beb69`. **fb and tx2 land together, or fb
after tx2.**
## 6 Seams assumed, and deltas to name
1. **`SyncTextureToBackendByHandle(h, imageBindable)` is the by-handle twin of
`SyncTextureObjectToBackend(obj, …)`** — it does (or memo-skips) params, builtin sampler and
mipmaps, and returns the twin. The two attachment lists make ONE such call per entry instead of
the pre-handle arm's three, because by-handle forms of `SyncTextureParamsToBackend` /
`SyncBuiltinSamplerToBackend` / `IsDrawSyncClean` are tx2's new names and are not declared
seams. If tx2's `IsDrawSyncCleanByRecord` ends up OUTSIDE that call, fb's lists need a re-look.
2. **`SyncMipmapsToBackendByHandle(h)` drives storage from the descriptor** with no frontend
object — that is what let `SyncAttachmentSurface` drop its parameter.
3. **`MarkWritableImageBufferTexturesGpuWritten` deleted under a transport** — paired with **sb's**
deletion of the backend storage-block marks. I gated at the TOP of the function, not at the two
call sites, so the unit bookkeeping is out of an apply thread's reach too; sb should say whether
it did the same.
4. **The reverse index is maintained at `SyncToBackendByHandle`**, not at `set_framebuffer_state`
apply as §5.4's wording suggests. Same set: a framebuffer whose record was never synced has no
driver attachment to detach, and every path that can have one syncs first. It lives in
`FramebufferImpl` (`NoteFramebufferTextureAttachments` / `FramebuffersAttachingTexture`) and
**tx2 calls the second one** if it wants it.
5. **`UnitTextureSyncEntry` untouched** (see §1).
6. **Shared-code delta (G1):** the image bind's format rules are factored into one `static`
`IssueImageTextureBind` that the PULL build compiles too, because the alternative was the same
hundred lines of widening/split reasoning twice and a divergence there is an out-of-bounds image
read, not a wrong pixel. One caller in the pull build, internal linkage, no new symbol. Named
here rather than left to a G1 diff. Everything else is under `MOBILEGL_PIPE_PUSH` /
`MOBILEGL_BUILD_DISAGGREGATED`.
7. **Push-monolith delta:** `SyncAttachmentSurface`'s two loud cross-checks and the N-6 refusal go
on that arm too (no second answer left to check against), and `SyncImageTextureBinding(Uint)` no
longer takes its four field values off the record there. Both behaviour-identical by
construction — the client copies the unit's own binding, P4a's own argument — but they ARE token
moves in the push-monolith build. The pull build is unaffected.
8. **Out of my stated range, taken deliberately:** `DirectGLES.cpp`'s `BindImageTexture` (the
`bind_shader_image` sink target) was reading `GetImageTextureBinding` on an UNBARRIERED row and
strict named it on 13 entries; no other package's range names it. It now records the high-water
mark and defers to the sweep (the sink bumps the texture shutter serial right after, so the
sweep cannot be suppressed). `Managers.h:717` gained `PipeTextureTargetForHandle` beside
`PipeTextureRecordForHandle`. **Ruling wanted**: `MGPImageBind::Res` stops at the sink, so that
funnel cannot resolve a twin at all — should the backend `BindImageTexture` entry eventually
take the handle, or is deferring the permanent answer?
## 7 Is a blit still barriered?
**No, and it need not be.** `Blit` is `kWaitNone` in §2.2's static column and fb removes the last
reason S3 could see to escalate: both arms resolve every endpoint, aspect and texture from records
and applier state, and the two `ForceBindCurrentFBO` calls that restore the bindings afterwards are
handle-only. The predicate decides and **ra owns the flip** — fb changed no wait class, and asks
for none.
## 8 What I did not do
No device measurement, no adb, no pull-build (G1) compile — the gate configures the split flavour
only. The object A/B is not re-baselined (§3). The resize case has no negative control beyond the
suite's own `AnUnchangedBindingPairEmitsNothing`, which is what stops it passing because the
emitter fires unconditionally. Row 84 left to pg (§2).