mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-10 11:33:24 +09:00
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.
174 lines
10 KiB
Markdown
174 lines
10 KiB
Markdown
# verb-census.md — which `GlobalBackendFunctionsTable` slots the reduced path actually reaches
|
||
|
||
**Status: STATIC, but not an estimate for two of the three targets.** R-4 asks for a census run
|
||
against an all-`Fatal` emit table; that table does not exist until package c1 lands, so this was
|
||
derived by reading the dispatch chain from every GL entry point down to
|
||
`gBackendFunctionsTable.GL.*`. For **OpenRA the call set is measured, not guessed** — the
|
||
fixture turned out to be hydrated locally and was dumped with the local `apitrace`. **c1 must
|
||
still confirm the whole thing by running the real all-Fatal table**, because a static read
|
||
cannot see a slot reached from a branch it did not know to follow.
|
||
|
||
Base `feat/disaggregated @ a29807cc`, read in `~/w7/p5-c0`. No GitHub fetch, no LFS.
|
||
|
||
---
|
||
|
||
## The answer
|
||
|
||
**Six slots, out of 71.** Five `GLFunctionsTable` entries plus `Present`:
|
||
|
||
| slot | A: ClearThenReadPixels | B: Triangle | D: OpenRA | reached by |
|
||
|---|---|---|---|---|
|
||
| `Clear` | ✔ (all 5 cases) | ✔ | ✔ (128 calls) | `glClear` |
|
||
| `DrawArrays` | ✔ (4 of 5) | ✔ | ✔ (3,648 calls) | `glDrawArrays` |
|
||
| `ReadPixels` | ✔ (all 5) | ✔ | ✔ (harness, not the trace) | `glReadPixels` |
|
||
| `BlitFramebuffer` | ✔ (cases 3, 4) | — | — | `glBlitFramebuffer` |
|
||
| `GetIntegeri_v` | ✔ (4 of 5) | ✔ | ✔ (once) | **the first `glCompileShader` of a context**, not any verb |
|
||
| `Present` | ✔ (all 5) | **only if it calls `EndFrame()`** | ✔ (128) | `eglSwapBuffers`, **through the EGL path, not through GLImpl** |
|
||
|
||
The other **65 slots** take `Fatal{UnmigratedVerb, "<slot>"}`.
|
||
|
||
This is **at the bottom of R-4's predicted 6–15**, and it is smaller than the brief's guessed
|
||
minimum set for reasons worth knowing rather than celebrating — see the four traps below.
|
||
|
||
---
|
||
|
||
## Four things this census found that the guess would have got wrong
|
||
|
||
**1. `Flush` and `Finish` are not slots and reach nothing.** R-4's predicted minimum set names
|
||
`Flush`. There is no `Flush` entry in `GLFunctionsTable` at all, and
|
||
`MG_Impl/GLImpl/Exporting/Definitions.cpp:111-112` makes both `glFinish()` and `glFlush()`
|
||
**literally empty bodies** — one `MGLOG_D` and a return. A `TriangleScenario` that calls
|
||
`glFlush` to order its readback is ordering nothing. If it needs a real sync it must use
|
||
`glFenceSync` + `glClientWaitSync`, which would pull in three more slots (`FenceSync`,
|
||
`ClientWaitSync`, `DeleteSync`); the readback itself is the cheaper ordering point.
|
||
|
||
> **RULING R-15 (integrator, after this census): a getter-shaped slot — one whose answer is a
|
||
> static property of the server's device — is ANSWERED LOCALLY FROM THE CAPS MIRROR, never
|
||
> emitted and never `Fatal`.** That puts `GetIntegeri_v` and `IsTimerQuerySupported` in class A
|
||
> below, leaves five slots emitted and 64 `Fatal`, and the gate already exists
|
||
> (`AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer`). The full three-class
|
||
> split, and the 41 null-checks that are really capability probes, are in `CONTRACT-P5.md §7` —
|
||
> **c1 reads it there rather than re-deriving it.**
|
||
|
||
**2. `GetIntegeri_v` is reached by compiling a shader, not by drawing.** It has no GL-verb
|
||
caller on this path at all: `MG_Util/ShaderTranspiler/CompileEnv.cpp:134-138`
|
||
(`CaptureCompileEnv`) reads it, reached lazily from `MG_State/GLState/Core.cpp:39`
|
||
(`GLContext::GetCompileEnv()`) on the **first shader compile of a context**. So *every* target
|
||
that compiles a shader touches it, whatever its entry-point set looks like — and an emit table
|
||
that raises `Fatal{UnmigratedVerb}` on it aborts on the first `glCompileShader` of every
|
||
scenario. It must have a real emitter (or be answered from the caps mirror, which is the better
|
||
answer: the six compute limits it carries already ride in `MGPCaps::Dynamic`, see CONTRACT §1).
|
||
|
||
**3. `Present` and `SetSwapInterval` have ZERO call sites in `MG_Impl`.** Both are called only
|
||
from `MG_Backend/BackendObject.cpp:396` and `:403`, reached through
|
||
`MG_Impl/EGLImpl/EGLImpl.cpp:178` / `:448`. An emit table built by mirroring the 89 GLImpl call
|
||
sites covers neither, and `Present` is on the reduced path in two of the three targets. The seam
|
||
for those two is the EGL path — which is the same seam v1 is already rerouting to the apply
|
||
thread, so they belong together.
|
||
|
||
**4. `ReadPixels` has a second, invisible caller.** Besides `glReadPixels`
|
||
(`GL_Framebuffer.cpp:3095`), the frontend fallback behind `glCopyTexSubImage*` calls it —
|
||
`CopyReadFramebufferIntoMipmapRegion`, `GL_Texture.cpp:1048`, calling at `:1084`. Not on the
|
||
reduced path, but it is the class of thing that makes "this scenario has no readback" false.
|
||
Two siblings: `GenerateMipmap` can fire from a `glTexImage*` when `GL_GENERATE_MIPMAP` is set
|
||
(`GL_Texture.cpp:1679` → `:1688`), and `DrawArrays` is reached by `glDrawTransformFeedback*`
|
||
(`GL_Drawing.cpp:1710`, `:1712`) with no `glDrawArrays` in sight.
|
||
|
||
---
|
||
|
||
## Per target
|
||
|
||
### A — `DirectGLES.Split.ClearThenReadPixelsScenario` (exists, 5 cases)
|
||
|
||
`MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp`, 335 lines.
|
||
|
||
| case (line) | slots |
|
||
|---|---|
|
||
| `ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels` (80) | Clear, DrawArrays, ReadPixels, GetIntegeri_v, Present |
|
||
| `ClearWithNoDrawIsVisibleToASubRectReadback` (143) | same |
|
||
| `ABlitIntoTheDefaultFramebufferSurvivesAnEarlierClear` (191) | + **BlitFramebuffer** |
|
||
| `AMultisampleResolveBlitIntoTheDefaultFramebufferKeepsItsOrientation` (241) | Clear, **BlitFramebuffer**, ReadPixels, Present — **no shader and no draw**, so it is the one case that does *not* need `DrawArrays` or `GetIntegeri_v` |
|
||
| `ADrawIntoTheDefaultFramebufferSurvivesAnEarlierClear` (308) | Clear, DrawArrays, ReadPixels, GetIntegeri_v, Present |
|
||
|
||
`glBlitFramebuffer` reaches **`BlitFramebuffer`**, *not* `BlitNamedFramebuffer` — the DSA slot is
|
||
reachable only from `glBlitNamedFramebuffer` (`GL_Framebuffer.cpp:3413` → `:2708`). Easy to
|
||
conflate; they are separate slots and only one is on this path.
|
||
|
||
### B — `DirectGLES.Split.TriangleScenario` (does not exist; t1 writes it)
|
||
|
||
Spec: VBO create + upload, compile + link, VAO, `glClear`, `glDrawArrays` (VBO-backed, **no
|
||
client-array indices**), `glFlush`, `glReadPixels`.
|
||
|
||
Slots: **`Clear`, `DrawArrays`, `ReadPixels`, `GetIntegeri_v`** — and `Present` **only if the
|
||
scenario ends with `HeadlessGL::EndFrame()`**. The spec as written does not, which would make B
|
||
a strict subset of A minus `Present`. **Decide that deliberately**: a reduced path whose
|
||
"minimal" case exercises fewer slots than its predecessor is fine, but it should be a choice.
|
||
|
||
Everything else the scenario calls reaches no verb slot: the buffer, VAO, program and texture
|
||
traffic is the `MG_Impl/Pipe` → `MGPipeApply*` family (37 entry points), which `EmitTables.h`
|
||
says this table deliberately does not cover, and the rest is answered in the frontend.
|
||
`glGetProgramiv` is deliberately not a slot (`BackendObject.h:206-212`).
|
||
|
||
### D — OpenRA trace, SSIM ≥ 0.99 — **measured**
|
||
|
||
`tools/trace_replay/trace_cases.json:14-28`: `openra.trace`, target call 31,249, 640×480, crop
|
||
(1,1,638,478), threshold 0.99, 180 s.
|
||
|
||
**The fixture is hydrated in this tree and is the only one that is** — `openra.tgz` is 7,232,355
|
||
bytes with gzip magic, `openra.0000031249.png` is 403,752 bytes. Every other `.tgz` there is a
|
||
130–134-byte LFS pointer. Nothing was fetched.
|
||
|
||
`apitrace dump` over the whole trace: **70,465 calls, 51 distinct entry points** (46 GL + 5
|
||
GLX). Up to call 31,249 the *set* is identical and the counts scale about 4:1.
|
||
|
||
The only entry points that reach a verb slot:
|
||
|
||
| entry point | calls (whole trace) | slot |
|
||
|---|---|---|
|
||
| `glDrawArrays` | 3,648 | `DrawArrays` |
|
||
| `glClear` | 128 | `Clear` |
|
||
| `glXSwapBuffers` → the harness's `eglSwapBuffers` (`tools/trace_replay/apitrace_glws_egl.cpp:317`) | 128 | `Present` |
|
||
| first `glCompileShader` | 14 total, 1 capture | `GetIntegeri_v` |
|
||
|
||
Plus `ReadPixels`, contributed by the **snapshot harness** rather than by OpenRA:
|
||
`tools/trace_replay/apitrace_fbo_dump.cpp:383`. (That file also has a `glGetTexImage` arm at
|
||
`:337`/`:349` for texture-attachment snapshots, which would reach `GetTexImage`; the
|
||
default-framebuffer path takes the `glReadPixels` arm.)
|
||
|
||
**OpenRA is the thinnest of the three targets, not the widest.** Its bulk — 20,055
|
||
`glVertexAttribPointer`, 7,296 `glBlendEquation`, 6,685 `glBindBuffer`, 4,866 `glUseProgram`,
|
||
4,828 `glBindTexture`, 3,030 `glBufferSubData`, all program and texture traffic — is entirely
|
||
the `MGPipeApply*` family. It widens the **site** set and the **record** set enormously and the
|
||
**verb** set not at all, which is exactly what `scout-unmigrated-census:§3` predicted for the
|
||
field set and is here confirmed for the slot set. The framebuffer work (calls 1348–1382) never
|
||
blits or clears an attachment through the table.
|
||
|
||
---
|
||
|
||
## Blast radius of the 65 Fatal slots
|
||
|
||
Of the 69 GL slots, **28 are called with no null check at all** and 41 are guarded.
|
||
|
||
- For the **28 unguarded** — the 18 draw entries, the five `ClearBuffer*`/`Clear`,
|
||
`BlitFramebuffer`, `CopyTexImage2D`, `CopyTexSubImage2D`, `GenerateMipmap`, `ReadPixels` — a
|
||
`Fatal` slot is **strictly better than today**: a null there is already an immediate crash
|
||
with no diagnostic.
|
||
- For the **41 guarded**, a `Fatal` slot is a real behaviour change, from "degrade quietly" to
|
||
"abort". The families that degrade today are sync (`FenceSync` → always-signaled),
|
||
query (`IsQueryResultAvailable` → reported available), `GetTexImage`/`GetTextureImage` (CPU
|
||
fallback), and transform feedback (silently skipped). **That change is R-4's intent** — a
|
||
silent degrade is how a split lane produces the right picture for the wrong reason — but it is
|
||
worth naming, because those are also the families most likely to be hit by an integration test
|
||
that is not on the reduced path.
|
||
|
||
## Counting notes
|
||
|
||
- **89** direct `gBackendFunctionsTable.GL.*` call sites in `MG_Impl/`, not the 91 the scout
|
||
reported; **0** of them are in `MG_Impl/Pipe/` (the two hits there are comments about the
|
||
`MGP_FILL` convention). Distribution: `GL_Drawing.cpp` 36, `GL_Query.cpp` 22,
|
||
`GL_Framebuffer.cpp` 11, `GL_Texture.cpp` 10, `GL_Sync.cpp` 6, `GL_Getter.cpp` 3,
|
||
`GL_Program.cpp` 1.
|
||
- **No** GL slot has zero `MG_Impl` call sites. The zero-call-site result belongs to `Present`
|
||
and `SetSwapInterval` (trap 3) and to `GetIntegeri_v`'s out-of-tree caller (trap 2).
|
||
- `PrefersCpuXfbPrimitiveAccounting` is not a verb: one non-test reader, `GL_Query.cpp:221`.
|