mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 21:43: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.
1716 lines
155 KiB
Markdown
1716 lines
155 KiB
Markdown
# P3a implementation brief — handle wave 1 (Espryt): buffers and VAOs behind the `resource_*` / vertex calls
|
||
|
||
Tree: `feat/disaggregated @ 55d2af9b` (worktree `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`,
|
||
read-only for the scouts and for this brief). **`55d2af9b` is the P3a base ref**; every `file:line` below was re-opened
|
||
at it. `$BASE` = `55d2af9b` throughout.
|
||
|
||
WSL: `~/w7/pipe` is a worktree of the same repo on `feat/disaggregated`; `~/w7/base` is `dev@81b17c0b` (the performance
|
||
anchor, untouched). Tree creation is `~/w7/notes/tools/wsl_p2_tree.sh` copied to `wsl_p3a_tree.sh` with `p2`→`p3a`
|
||
throughout (it creates `~/w7/p3a-<slug>` on branch `p3a/<slug>`, initialises submodules, copies
|
||
`3rdparty/glslang/External` and the trace fixtures, then configures and builds `build-linux` (pull), `build-push`, and
|
||
with a third argument `verify` also `build-verify`).
|
||
|
||
Corrections to the scouts are **[correction]**; deliberate departures from the design documents are **[deviation]** and
|
||
carry their reason and the doc line the integrator must correct.
|
||
|
||
---
|
||
|
||
## Scout verification summary
|
||
|
||
All four scouts were read in full and their load-bearing claims re-opened at `55d2af9b`. What follows is wrong or
|
||
incomplete in them and is fixed here. Everything not listed was confirmed verbatim.
|
||
|
||
1. **[correction] `scout-docs-spec.md` mis-cites eight `ARCHITECTURE.md` lines.** The correct lines are:
|
||
the call-flag legend incl. `kOptional`/`kReplySlot`/`kNeedsAck` and Magma's deliberate `BufferSubDataResident`
|
||
omission is **`:94`** (not `:102`; `:102` is the "explicitly not migrated" list);
|
||
the `MGPResourceDesc` prose gloss is **`:128`** (not `:127`, a table separator);
|
||
the `resource_create`/`resource_respecify`/`resource_destroy` lifetime rule is **`:208`** (not `:204`);
|
||
the fp64-vertex-narrowing emulation row is **`:225`** (not `:236`);
|
||
the per-site `SyncPersistentMappedRange`/`SyncGpuWrites` reconcile table is **`:235-241`** with its gate sentence at
|
||
**`:243`** (not `:239-243`);
|
||
`OnGpuWritten` is **`:274`** and `OnBufferWriteback` **`:275`** (not `:271-273`);
|
||
§9.1 "原样不动的东西" is the heading at **`:314`** and the Espryt list at **`:316`** (not `:315`);
|
||
the two surviving byte-level equalities are **`:507`** (not `:508`);
|
||
`SetIndexBuffer` independent of the VAO config version (D5) is **`:99`** (not `:101`);
|
||
the `NEW_INDEX_BUFFER` dirty row is **`:165`** (not `:164`);
|
||
the CSO content-addressing capacities (render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096) are
|
||
**`:63`** (not `:57`); `VertexInputStateFactory::m_cache` as the server-side counterpart is **`:55`**.
|
||
2. **[correction] `ROADMAP.md` open question 10 (`ResidentSubData` asymmetry) is `:83`, not `:87`.** Questions 15/16/17
|
||
are at `:88`/`:89`/`:90` as the scout says. The re-baseline table is `:60-68`: P3a's trigger row is `:64`, P7's note
|
||
about P3a's checkpoint being blind to Magma is `:66`, and "任一触发,先跑 `inproc` 的证伪数字" is `:68` (not `:67`).
|
||
3. **[correction] Six `Managers.cpp` line numbers in `scout-espryt-buffers.md` are off by 2–8.** Verified:
|
||
`Ops_OnDestroy` starts at **`:1417`** (not 1421); `g_deferredBufferReleases` is declared at **`:672`**;
|
||
`struct PooledBuffer` at **`:687`**; `g_bufferPool` at **`:693`**; the three pool budget constants
|
||
(`kMaxPoolableBufferBytes` 8 MiB / `kMaxPoolBytes` 64 MiB / `kMaxEntriesPerBucket` 32) at **`:696-698`**;
|
||
`IsPoolable` at **`:700`**; `EnrollIntoPool` at **`:711`**; `AcquireFromPool` at **`:740`**;
|
||
`g_bufferBackendIdGeneration`'s definition at **`:1503`**. `Ops_Respecify` `:1213`, `Ops_SubData` `:1266`,
|
||
`Ops_ResidentSubData` `:1304`, `Ops_FlushMappedRange` `:1314`, `Ops_ReadbackFromGpu` `:1382`,
|
||
`Ops_AcquirePersistentMap` `:1134`, the seven tracked wrappers `:1449-1480`, `g_glesBufferBackendOps` `:1482-1490`,
|
||
`Register/UnregisterBufferBackendOps` `:1505`/`:1512`, `OnBackendContextDestroyed` `:1527`,
|
||
`ProcessDeferredBufferReleases` `:1542`, `IsBufferDrawClean` `:1579`, `EnsureBufferResource` `:1602`,
|
||
`TrimBufferPool` `:1882`, `ClearBufferPool` `:1913`, `FlushPendingRangesNow` `:1004`, `DrainResidentWritesNow`
|
||
`:1084`, `RespecifyStorageNow` `:895`, `UploadRangeNow` `:946`, `UploadRingUsableNow` `:964` are all exact.
|
||
4. **[correction] `MEASUREMENTS.md`'s "6.5–9.3 accessor calls per draw" is `:64`, not `:66`.** `:87` (the persistent-map
|
||
adoption baseline) and `:96` (the `create-indirect` pre-existing failure) are exact.
|
||
5. **[correction] The `map-persistent-roundtrips` counter does not exist in any form.** `PipeStats.h`'s `ByteClass`
|
||
(`:46-77`) and `CallClass` (`:81-118`) carry no such member and `PipeStats.cpp:173-179`'s name table has no such
|
||
string. P3a **mints it** (D-B2 below), and it must be `#if MOBILEGL_PIPE_PUSH`-guarded for the reason `PipeStats.h`
|
||
`:101-106` already states for `RenderStateCsoMints`/`Binds`: growing the enum in a **pull** build resizes the counter
|
||
arrays, the name table and `FormatWindowLine`, which is a G1 break for a counter that could never leave zero.
|
||
6. **[correction] `scout-espryt-vao.md`'s flag about the second view is right, and it is stronger than the scout says.**
|
||
`MGPipeValueTypes.h:529-531` states it as a design fact in the tree — *"Attributes configured through the
|
||
binding-point API are resolved eagerly into the flat VertexAttribute view above, so backends keep consuming resolved
|
||
attributes and never see binding points."* — and `grep -rn 'VertexBufferBindingPoint\|GetAttributeBindingIndex\|
|
||
GetAttributeRelativeOffset' MobileGL/MG_Backend/` returns **zero hits**. `ARCHITECTURE.md:130`'s justification for
|
||
carrying both views ("pointer stride 0 = element size, binding stride 0 = same element") is therefore **stale**: the
|
||
frontend already resolved it, and `MGPipeValueTypes.h:496-503` says a surviving zero can only have come from the
|
||
binding model. D-G below carries both views anyway, for a different and still-valid reason, and the integrator
|
||
corrects `ARCHITECTURE.md:130`'s stated reason.
|
||
7. **[correction] `scout-espryt-vao.md` §4.2's `baseInstance` question resolves in favour of a payload change, and the
|
||
free `Pad0` it points at is the wrong one.** `MGPVertexBuffer` (`MGPipeTypes.h:356-364`) does have a `Uint32 Pad0`
|
||
at `:362`, but the fetch shift is **one value per emitted set**, not per binding: putting it per entry lets a
|
||
malformed record disagree with itself and gives the applier a case to police. `ROADMAP.md:19` names
|
||
`set_vertex_buffers`, whose payload is `MGPVertexBuffers` (`:367-371`, 16 B, no padding). D-H takes that struct from
|
||
16 to 24 bytes.
|
||
8. **[correction] `scout-pipe-and-tests.md` §5.1's "edit `PipeCalls.def` in place for `kNeedsAck`" understates the
|
||
problem.** Flags are a **per-call** static property, and `ResourceRespecify` is the one call used for both
|
||
`glBufferData` and `glBufferStorage`. A bare `kNeedsAck` on it would ack every `glBufferData` in Minecraft's world
|
||
upload. D-A5 resolves it with a per-record predicate and a flag-legend edit.
|
||
9. **[correction] The four ID-3 admitted resizes do not apply to P3a's G1.** They are resizes measured against the
|
||
**pre-P2** baseline and are already present in `$BASE`. Against `$BASE` the admitted set is **empty**: 0 added,
|
||
0 removed, 0 renamed, **0 resized**. They reappear only when the report is run against `087685d1`, which is what CI's
|
||
`monolith-symbol-report` job does by default (`.github/workflows/test.yml:14-21`). G1 below names both readings so
|
||
neither can be confused for the other.
|
||
10. **Confirmed at `55d2af9b`, unchanged from the scouts**: all sixteen `PipeCalls.def` rows P3a lands
|
||
(`:75-79`, `:95-97`, `:108-110`, `:128-132`) with exactly the payloads, classes and flags quoted;
|
||
`MGP_CALL_LIST_DOCUMENTED_COUNT 71` (`:69`); `MGPResourceDesc` 88 B (`:150-176`) and `kMGPipeWholeBuffer` (`:177`);
|
||
`MGPSubData` 72 B with the buffer-half convention and its three accessors (`:575-617`); `MGPFlushRange` 32 B
|
||
(`:628-635`); `MGPReadback` 24 B (`:637-641`); `MGPVertexElements` 40 B (`:250-259`); `MGPIndexBuffer` 24 B
|
||
(`:373-380`); `BufferBackendOps`' seven members and every doc comment quoted (`BufferObject.h:76-122`);
|
||
`g_glesBufferBackendOps` setting all seven (`Managers.cpp:1482-1490`); `MGPipeCallbacks`' ten callbacks including
|
||
`OnGpuWritten` and `OnBufferWriteback` (`MGPipeCallbacks.h:27-51`); the seven `MGPipeApply*` entry points and
|
||
`MGPipeApplierState` (`PipeApply.h:96-149`, `:48-87`); `MGPipeDirty`'s 18 bits and `MGPipeSubsystemForDirty`'s
|
||
`default: return 0` (`Tracker.h:57-135`); `MGPipeSuppressorSlot::SetVertexBuffers = 0` and the reserved hash 0
|
||
(`SetHashSuppressor.h:36-59`); the six `NotifyStateObjectDestroyed` raisers and `Managers.cpp:212-215`'s
|
||
`default:` arm naming `BufferBackendOps::OnDestroy`; the subsystem bits 0..6 and
|
||
`kMGPipeSubsystemsMigratedAtP2 = 0x7f` (`MGPipe.h:72-85`); `SubsystemForEmitter`, its five `static_assert`s and
|
||
`kMGPipeWiredSubsystems` (`PipeFill.cpp:624-672`); `Coverage.def:36-45` and `:128-130`; `FillPoints.def:60-64`
|
||
recording the class-field-mask retirement as **P3a work**; the VAO twin's whole private state
|
||
(`Managers.h:886-951`) and `SyncToBackend`'s gate (`Managers.cpp:2437-2472`); the three `ScopedFetchBaseInstance`
|
||
sites (`DirectGLES.cpp:5135, 5172, 5224`) and `EmulatedFetchBaseInstance`/`UseNativeBaseInstance`
|
||
(`:5122-5130`); `retrace_gate.py`'s five flags (`--tree --lib --out -j --only`, no `--ssim`, no `--backend`);
|
||
the 40 trace cases, the two `coherent_as_flush: true` Create fixtures (`trace_cases.json:275, 283`) and the global
|
||
`"ssim_threshold": 0.99` at `:6` with per-case overrides at `:292` (0.995) and `:303` (0.98); and all fifteen named
|
||
scenario files under `MobileGL/MG_IntegrationTest/Scenarios/`.
|
||
|
||
---
|
||
|
||
## A. Goal and acceptance gate
|
||
|
||
`docs/Disaggregated/ROADMAP.md:19`, verbatim (all five cells):
|
||
|
||
> | **P3a** handle wave 1(Espryt):buffer、VAO | 18–23 | 7 个 `BufferBackendOps` → `resource_*`、`buffer_subdata_resident`(可 null)、`resource_flush_range`、`resource_readback`、`map_persistent`(不碰实现);pool 与延迟释放原样搬;vertex elements 三件(两个视图都带);`set_vertex_buffers`(`baseInstance` 显式字段);`set_index_buffer`;Adreno SIGSEGV workaround 保留 | 全套门;buffer/VAO 族场景(`LargeArenaAdoption`、`StorageBufferRegrow` 发布 `map-persistent-roundtrips`、`VertexAttribBinding`、`MultiDraw`、`PrimitiveRestart`…);Create/rd12/26.3/sodium trace;MC 26.3 在 Adreno 上 p99 不变。**再基线检查点 1:超过 27 天必须重定基线** | P2 |
|
||
|
||
Binding context: `ARCHITECTURE.md:94` (the call-flag legend, including `kOptional`'s named exception), `:99` (D5:
|
||
`SetIndexBuffer` is independent of the VAO config version), `:116-122` (payload record conventions and the `MGPSubData`
|
||
buffer half), `:128-130` (the `MGPResourceDesc` and `MGPVertexElements` rows), `:149-155` (§5.1 — push happens at the
|
||
validate moment before a verb, **and the one exception: only the seven `BufferBackendOps` hooks push at GL-call
|
||
time**), `:157-174` (§5.2 — the dirty table, five aggregate generations, wrap widening), `:194` and `:200` (§5.4 — the
|
||
D-B3 ordering invariant and live resolution of indexed ranges), `:208` (§5.6 — the create/respecify/destroy lifetime
|
||
hooks and the three payload-expressed ordering constraints), `:221-225` and `:235-243` (§5.7 — emulation ownership and
|
||
the per-site reconcile table), `:267-288` (§8.1/8.2 — the reverse channel and the epoch-ordering rule), `:293` (§8.3 —
|
||
**the only entry allowed a synchronous ack is `glBufferStorage`; the `kNeedsAck` marker lands with P3a's buffer
|
||
path**), `:314-316` (§9.1 — the Espryt do-not-touch list: three rings, buffer pool, the Adreno disabled-attribute
|
||
SIGSEGV workaround), `:363` (§9.5 — the 21-memo census P3a pays into), `:367` (§9.6 — `MOBILEGL_PIPE_LEGACY_MEMOS`
|
||
stays a compile-time arm **through P3a/P4a**), `:383-389` (§10.3 — the index host mirror P8 builds out of P3a's three
|
||
calls), `:466` (D-B4 — `AcquirePersistentMap` untouched for the whole monolith conversion), `:479-485` (P5's persistent-map
|
||
push and the `coherent_as_flush` rule), `:497-507` (§13.2 — the five-part validation gate and the two surviving byte
|
||
equalities), `:515` (present/`eglSwapBuffers` strictly 1:1, which is what protects `TrimBufferPool`). `ROADMAP.md:7`:
|
||
默认 ALL target 必须完整构建;禁止提交热路径插桩;**每个门必须能因它存在的理由变红**;Windows 不是正确性门;设备对比走
|
||
reboot-clean + 同热窗口配对 A/B;每阶段出口跑一次五部分门;每阶段性能判据是逐线程 CPU 时间. `ROADMAP.md:34`: **P3a is one of
|
||
the five architecture boundaries that owe a full `gl44to46` caselist run (~56,271 cases) on both devices, off the
|
||
critical path.** `ROADMAP.md:64`: **P3a over 27 days means the narrow-handle premise was wrong and P4a re-baselines.**
|
||
|
||
### A.1 The user's standing rules (2026-09-08), applied
|
||
|
||
> (a) Advance the roadmap as fast as possible without a large performance regression. **Performance is RECORDED
|
||
> against the pull baseline (P2's A/B and DriverBench numbers in `MEASUREMENTS.md`), never a blocking gate in P3a.
|
||
> Correctness gates stay.**
|
||
> (b) Agents run on opus, so packages stay mechanical and well-specified.
|
||
|
||
Concretely: **part 4 of the five-part gate (`ARCHITECTURE.md:504`) becomes a measure-and-publish obligation, not a
|
||
pass/fail gate**, and so does `ROADMAP.md:19`'s "MC 26.3 在 Adreno 上 p99 不变". Parts 1, 2, 3 and 5 stay hard gates.
|
||
This rule is transcribed into `MEASUREMENTS.md`'s P3a section by the integrator (D.5) because it is not written in
|
||
`docs/Disaggregated/*`.
|
||
|
||
### A.2 The gate, decoded
|
||
|
||
Unless a row says otherwise, commands run in a P3a WSL worktree. ID-4's errata are applied throughout: ctest names are
|
||
extracted with `grep -E "^[[:space:]]*Test[[:space:]]+#[0-9]+:" | sed -E "s/^ *Test +#[0-9]+: //" | LC_ALL=C sort`
|
||
(the naive `^\s+Test #` drops every id under 1000); `retrace_gate.py` has **no** `--ssim` and **no** `--backend` (the
|
||
threshold is the per-case one in `trace_cases.json`); the verify arming line is
|
||
`MGPipe: verify armed - 63 fields, 69 verbs, fatal=1` and retrace case logs carry
|
||
`MGPipe verify: <case> <backend> armed, zero divergences, zero unmigrated reads`.
|
||
|
||
| # | statement | the exact command that checks it |
|
||
|---|---|---|
|
||
| **G1** | The **pull build** (`MOBILEGL_PIPE_PUSH=OFF`, `MOBILEGL_PIPE_VERIFY=OFF`, Release/INFO) is symbol-identical to `$BASE`: 0 added, 0 removed, 0 renamed, **0 resized**. P3a's admitted-resize set is **empty** — every P3a edit to `MG_State` / `MG_Pipe` / `MG_Backend` is inside `#if MOBILEGL_PIPE_PUSH` (D-J). The four ID-3 resizes (`RenderState::{RenderState,SetCapability,IsCapabilityEnabled}`, `_GLOBAL__sub_I_DirectGLES.cpp`) are already in `$BASE` and reappear only in the `087685d1` reading. | `python3 scripts/symbol_report.py --before ~/w7/p3a-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0 --json` → `added==removed==renamed==resized==0`. The `087685d1` reading is CI's: `gh workflow run test.yml --ref feat/disaggregated -f baseline_sha=087685d1`, whose Resized table must be **exactly** those four. |
|
||
| **G2** | `ctest -L integration-gpu` is **name-for-name identical between the pull and the push build**, on both backends, and green in both. | `for d in build-linux build-push; do ctest --test-dir $d -N \| grep -E "^[[:space:]]*Test[[:space:]]+#[0-9]+:" \| sed -E "s/^ *Test +#[0-9]+: //" \| LC_ALL=C sort > ~/w7/p3a-names-$d.txt; done; diff ~/w7/p3a-names-build-linux.txt ~/w7/p3a-names-build-push.txt` → empty; then `ctest --test-dir build-linux -L integration-gpu --no-tests=error -j 8` and the same on `build-push`, both green, plus `MOBILEGL_PIPE_PUSH=0 ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8` (the all-pull arm). |
|
||
| **G3** | The 40-trace corpus replays **under push** at each case's own SSIM threshold on both backends (79 desktop cases: 40 × 2 minus `iterationrp × DirectGLES`). | `python3 ~/w7/retrace_gate.py --tree ~/w7/pipe --lib ~/w7/pipe/build-push/libMobileGL.so --out ~/w7/retrace-out/p3a-push -j 4` → exit 0. |
|
||
| **G3b** | The five **named** traces pass under push on both backends: `minecraft-1.21.1-neoforge-create-indirect-in-world`, `minecraft-1.21.1-neoforge-create-instancing-in-world`, `minecraft-1.21.4-rd12-odinlite-in-world`, `improved-transparency-minecraft-26.3`, `minecraft-1.21.4-fabric-sodium-in-world`; and the two `coherent_as_flush: true` Create fixtures take the **same buffer path** under push as under pull (`ARCHITECTURE.md:485`). | `python3 ~/w7/retrace_gate.py --tree ~/w7/pipe --lib .../build-push/libMobileGL.so --out ~/w7/retrace-out/p3a-named -j 4 --only 'create-indirect,create-instancing,rd12-odinlite,improved-transparency-minecraft-26.3,fabric-sodium'`. **`create-indirect` on device is excluded and blocked on a `dev` fix** — see the caveat below. |
|
||
| **G4** | The **verify** build shows zero divergence: no `Fatal{PipeVerifyDiffer`, no `Fatal{UnmigratedPipeInput`, no `Fatal{PipeResidualDiverged`, and **every case armed**. | `ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4`; `MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py --tree ~/w7/pipe --lib ~/w7/pipe/build-verify/libMobileGL.so --out ~/w7/retrace-out/p3a-verify -j 4`; then `grep -l 'Fatal{' ~/w7/retrace-out/p3a-verify/*.log` empty and `grep -L 'MGPipe verify:' ~/w7/retrace-out/p3a-verify/*.log` empty. |
|
||
| **G5** | **"pool 与延迟释放原样搬" is literal.** Nine named functions in `Managers.cpp` are **byte-identical** to `$BASE`: `IsPoolable`, `EnrollIntoPool`, `AcquireFromPool`, `TrimBufferPool`, `ClearBufferPool`, `ProcessDeferredBufferReleases`, `CreateRingStorage`, `RingAvailable`, `RingAllocate`. | `bash scripts/p3a_untouched_regions.sh $BASE HEAD` (package D, C.4) — extracts each named function by brace matching, `sha256sum`s the list on both refs, diffs. Non-zero rc names the first function that moved. |
|
||
| **G6** | **Vertex-input emission consistency**: for every VAO configuration the client's emitted `MGPVertexElements` blob + `MGPVertexBuffers` set + `MGPIndexBuffer` reproduce **exactly** the values `BackendVertexArrayObject::SyncToBackend` reads from the frontend today, field by field, for all 32 attribute slots. | `ctest --test-dir build-push -R 'VertexInputEmit\.' --no-tests=error --output-on-failure`. |
|
||
| **G7** | **G6's negative control**: dropping one field from the wire attribute conversion (a break the compiler cannot see, because the struct still has the member and still asserts its size) turns G6 red **naming that field**. | `bash scripts/p3a_vertex_input_negative_control.sh build-push` — patches `VertexInputEmit.h` to stop copying `IsBgra`, rebuilds, expects `ctest -R 'VertexInputEmit\.'` to **fail** naming `IsBgra`, then reverts and rebuilds. Exit 0 = tripped and named; 1 = did not answer; 2 = could not run. |
|
||
| **G8** | **`HandleRecycleScenario` is green after the re-key and red before it for the two P3a kinds**, as always-on ctest entries: `.Handles` green, `.Legacy` green, `.AbaControl` green-by-asserting-the-corruption. P3a adds a buffer case and extends the VAO case to the vertex-input path. | `ctest --test-dir build-verify -R 'HandleRecycle' --no-tests=error --output-on-failure`. The "red before" evidence is recorded on the contract tree at D.2. |
|
||
| **G9** | `gen_pipe_dirty_surface.py` stays a gate **and its scan root is widened** to `MG_State/GLState` so the four `MGP_NOTE_MUTATION` sites in `TextureState.h` stop being outside it (`BRIEF-P2.md:969` records the widening as a P3a item). Both directions still fail: an unmapped mutator, and a row naming a mutator the scan no longer finds. | `python3 scripts/gen_pipe_dirty_surface.py --check` (rc 0) and `--self-test` (rc 0); `git diff --exit-code -- MobileGL/MG_Pipe/DirtySurface.def`. |
|
||
| **G10** | **`map-persistent-roundtrips` exists, is published, and can move.** `StorageBufferRegrowScenario` asserts N storage definitions of an adopted store publish exactly N roundtrips and **not** one per draw; a device window prints the counter non-zero. | `ctest --test-dir build-push -R 'StorageBufferRegrow' --no-tests=error --output-on-failure`; on device `grep 'MGPipe stats:' mobilegl.log \| tail -1` shows `mpr=` > 0. |
|
||
| **G11** | **RECORDED, NOT GATED (rule (a)).** Two devices, reboot-clean, same thermal window, paired A/B, per-thread CPU p50/p99 from `frameCpuTimesMs[]`; MC 26.3 p99 on Adreno published against `MEASUREMENTS.md:87`'s adoption baseline (p99 163→21 ms, 40→115 fps, ~400 MB); DriverBench `ns_per_op` T1/T2. A regression is written down and does **not** stop the phase. | D.4. |
|
||
| **G12** | The **subsystem A/B is real**: with bits 7 and 8 cleared the tree runs the legacy `BufferBackendOps` / `MGB_CTX`-reading twin arm and produces identical integration results; a ctest entry proves the switch actually changes behaviour rather than being dead. | `MOBILEGL_PIPE_PUSH=0x7f ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8` (P2's default = P3a's subsystems off) versus the default `0x1ff`; `ctest --test-dir build-push -R 'ResourceSubsystemControl' --no-tests=error`. |
|
||
| **G13** | Purity holds: `pGLContext` still appears zero times under `MG_Backend/`, the include closure holds, no stdio instrumentation, generators regenerate clean, **and no `MG_State::GLState` type appears in any `MGPipeResourceOps` signature**. | `grep -rc 'pGLContext' MobileGL/MG_Backend \| grep -v ':0$'` empty; `python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all`; `python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test`; `python3 scripts/symbol_report.py --self-test`; the `pipe-gates` stdio grep; `grep -n 'MG_State' MobileGL/MG_Pipe/PipeApply.h` shows no hit inside the `MGPipeResourceOps` block. |
|
||
| **G14** | Existing test names are never removed (additions only) and the full CI matrix is green. | `ctest --test-dir build-linux -N \| <ID-4 extractor> \| LC_ALL=C comm -23 ~/w7/p3a-before-ctest-names.txt -` empty; `gh workflow run test.yml --ref feat/disaggregated`. |
|
||
| **G15** | **P3a is one of the five architecture boundaries**: the full `gl44to46` caselist (~56,271 cases) runs on **both** devices, off the critical path, and per-backend conformance is within 0.5 pp of the `$BASE` reading. Report shape: rows = GL version/extension, columns = status counts, rate = Pass/(Pass+Fail), NS **not** in the denominator. | Scheduled at the `dev` merge (D.5), not before the phase verdict. |
|
||
|
||
**Baseline captures the integrator takes before any package lands** (in `~/w7/pipe` at `$BASE`):
|
||
|
||
```sh
|
||
cd ~/w7/pipe
|
||
cp build-linux/libMobileGL.so ~/w7/p3a-before-libMobileGL.so
|
||
ctest --test-dir build-linux -N | grep -E "^[[:space:]]*Test[[:space:]]+#[0-9]+:" \
|
||
| sed -E "s/^ *Test +#[0-9]+: //" | LC_ALL=C sort > ~/w7/p3a-before-ctest-names.txt
|
||
bash scripts/p3a_untouched_regions.sh $BASE $BASE > ~/w7/p3a-before-untouched.sha # the nine G5 shas
|
||
python3 scripts/gen_pipe_dirty_surface.py > ~/w7/p3a-before-dirty-surface.txt
|
||
```
|
||
|
||
**Two caveats that bind this gate and are not P3a's work to fix.**
|
||
|
||
- **`minecraft-1.21.1-neoforge-create-indirect-in-world` fails on both devices at `dev@81b17c0b`** — Adreno 830: Espryt
|
||
black frame after ~4.5 min, Magma `VK_ERROR_DEVICE_LOST` at a texture-upload submit; Mali SSIM 0.85 / 0.45
|
||
(`MEASUREMENTS.md:96`, `ROADMAP.md:90`). It is a **pre-existing `dev` bug, not this branch's**, and it must be fixed
|
||
on `dev` before it can serve as a P3a/P8 acceptance case. **Decision: it stays in the desktop 79-case SSIM corpus
|
||
(G3/G3b) and stays out of the device A/B (D.4), exactly as P2 did.** If it is still red on `dev` at the phase exit,
|
||
the integrator records "G3b partial: create-indirect blocked on `dev` open question 17" rather than claiming a pass.
|
||
- **Re-baseline checkpoint 1**: if P3a exceeds **27 working days**, "窄句柄化"'s premise was wrong and P4a re-baselines
|
||
(`ROADMAP.md:64`); `ROADMAP.md:68` says to run the `inproc` falsification numbers first. The integrator records
|
||
elapsed days per package for D.4.6's Track H unit-cost census.
|
||
|
||
---
|
||
|
||
## B. Fixed design decisions
|
||
|
||
Nothing in this section is re-decided inside a package.
|
||
|
||
### D-A — The seven `BufferBackendOps` hooks become handle-shaped `resource_*` calls
|
||
|
||
#### D-A1 — Where they push, and what replaces the ops table
|
||
|
||
`ARCHITECTURE.md:155` is the whole rule and it is the **only** exception to push-at-validate:
|
||
|
||
> **只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。
|
||
|
||
So: **`ResourceCreate`, `ResourceRespecify`, `ResourceSubData`, `BufferSubDataResident`, `ResourceFlushRange`,
|
||
`ResourceReadback`, `ResourceDestroy`, `MapPersistent` and `UnmapPersistent` are emitted at the GL call that causes
|
||
them, from the same `BufferObject` dispatchers that call the ops table today** (`BufferObject.cpp:39-43` `~BufferObject`,
|
||
`:45` `NotifyRespecify`, `:53` `NotifySubData`, `:63` `NotifyFlushMappedRange`, `:73` `NotifyContentWrite`, `:186`
|
||
`TryAdoptLargeStorage`, `:291` `SyncPersistentMappedRange`, `:319` `SyncGpuWrites`, `:371`
|
||
`LandBytesIntoResidentStore`, `:493` `EnsureGpuResidentStorage`). Nothing about buffers moves to validate time in P3a.
|
||
|
||
The conversion is **not** "rename seven functions". Every hook takes `BufferObject&` — a frontend heap reference — and
|
||
four of them read `MappedData()`. So P3a introduces a second, **handle-shaped** backend op table beside the existing
|
||
one:
|
||
|
||
```cpp
|
||
// MobileGL/MG_Pipe/PipeApply.h, inside #if MOBILEGL_PIPE_PUSH.
|
||
// Registered by the active backend at bring-up, cleared at shutdown, exactly as
|
||
// BufferBackendOps is (BufferObject.h:124-127). NO MG_State type appears here - that is
|
||
// gate B, and G13 greps for it.
|
||
struct MGPipeResourceOps {
|
||
void (*Create) (MGPipeHandle res, const MGPResourceDesc& desc);
|
||
void (*Respecify) (MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes);
|
||
void (*SubData) (MGPipeHandle res, const MGPSubData& record, const void* bytes);
|
||
void (*SubDataResident)(MGPipeHandle res, const MGPSubData& record, const void* bytes); // kOptional: may be null
|
||
void (*FlushRange) (MGPipeHandle res, const MGPFlushRange& record, const void* bytes);
|
||
void (*Readback) (MGPipeHandle res, const MGPReadback& record);
|
||
void (*Destroy) (MGPipeHandle res);
|
||
void* (*MapPersistent) (MGPipeHandle res, Uint64 size, const void* seedBytes);
|
||
void (*UnmapPersistent)(MGPipeHandle res);
|
||
};
|
||
void MGPipeSetResourceOps(const MGPipeResourceOps* ops);
|
||
const MGPipeResourceOps* MGPipeGetResourceOps();
|
||
```
|
||
|
||
**The `const void*` companion argument is P2's precedent, not a new idea**: `MGPipeApplyCreateRenderState(const
|
||
MGPRenderStateDesc&, const void* chunkBytes)` (`PipeApply.h:102`) already carries a blob beside its POD because in
|
||
monolith a blob needs no `MGPBlobRef`. In monolith the pointer is the frontend shadow base (`PipeResource::Bytes()`,
|
||
`PipeResource.h:91`) — **zero copy, zero behaviour change**. How those bytes cross in split is P5's problem and P5's
|
||
flag edit; P3a does **not** add `kHasBlob` to `ResourceRespecify` (a `kHasBlob` record must own an `MGPBlobRef` member,
|
||
`ARCHITECTURE.md:119`, and `MGPResourceDesc` has none).
|
||
|
||
**Dispatch selection in `BufferObject.cpp`** (package B), in exactly this shape at every one of the eight dispatch
|
||
sites:
|
||
|
||
```cpp
|
||
#if MOBILEGL_PIPE_PUSH
|
||
if (MGPipeResourceSubsystemEnabled()) { MGPipeEmitResourceRespecify(*this); return; }
|
||
#endif
|
||
if (g_bufferBackendOps && g_bufferBackendOps->Respecify) g_bufferBackendOps->Respecify(*this);
|
||
```
|
||
|
||
`MGPipeResourceSubsystemEnabled()` is `(MG_Config::Features.PipePush & kMGPipeSubsystemResources) != 0 &&
|
||
MGPipeGetResourceOps() != nullptr`. Magma never registers `MGPipeResourceOps` (its buffer path is P7,
|
||
`ROADMAP.md:24`), so a Magma context keeps dispatching `BufferBackendOps` verbatim and P3a cannot touch it. That is
|
||
also what makes the integration order in D.1 always-green: package `client` can land with nothing registered.
|
||
|
||
#### D-A2 — Payload and semantics, hook by hook
|
||
|
||
Every row below states what the client fills, what the applier stores, and what Espryt does. Espryt's **behaviour** is
|
||
unchanged in every row; only where it reads its inputs from changes.
|
||
|
||
| hook (today) | call | payload the client fills | what the applier stores | what Espryt does |
|
||
|---|---|---|---|---|
|
||
| *(none)* | `ResourceCreate` (`PipeCalls.def:75`) | `MGPResourceDesc` with `Resource` = the freshly minted `{slot,gen}` of kind `Buffer`, `Target = Buffer`, `StorageKind = Buffer`, `BindMask = 0`, `Usage`/`StorageFlags`/`Width` = 0, `Immutable = 0`, `HasDefinedContent = 0`, `GlNameForDiag = GetExternalIndex()`, everything else zero. Emitted from the `BufferObject` **constructor** (`BufferObject.cpp:35-37`), per `ARCHITECTURE.md:208`. | marks the slot Live, stores the descriptor. | **nothing** — storage is defined lazily by `ResourceRespecify`, and Espryt's `EnsureBufferResource` already tolerates a null resource (`BufferObject.h:72-75`). |
|
||
| `Respecify` (`BufferObject.h:80`) | `ResourceRespecify` (`:76`) | the same descriptor with `Width` = `GetSize()`, `Usage` = `GetUsage()`, `StorageFlags` = the `glBufferStorage` flags (0 for `glBufferData`), `Immutable` = 1 iff the store came from `glBufferStorage*`, `HasDefinedContent` = `HasDefinedContent()`, `BindMask` = D-A3's live mask. `initialBytes` = `MappedData()` when `HasDefinedContent`, else `nullptr`. | replaces the stored descriptor. | `Ops_Respecify` verbatim (`Managers.cpp:1213-1264`) with `bufferObject.GetSize()/GetUsage()/HasDefinedContent()/MappedData()` replaced by `desc.Width / desc.Usage / desc.HasDefinedContent / initialBytes` and `GetChangeSerial()` by the server-side `ResourceSerial` (D-A4). **`RespecifyStorageNow` (`:895-936`) keeps its `glBufferData(target, size, initialData, usage)` single call** — no orphan-then-upload split. `InvalidateIndexedBufferBindingShadowsForId` on an extent move (`:933`) stays. |
|
||
| `SubData` (`:82`) | `ResourceSubData` (`:128`) | `MGPSubData` with `Res`, `Target = Buffer`, and the range encoded **only** through `MGPipeSetSubDataBufferRange(record, offset, size)` (`MGPipeTypes.h:603-611`). `bytes` = `MappedData() + offset`. **A `false` return means the emitter must split** — one record caps at offset 2^31-1 and size 2^32-1, and at `SEG_STAGE`'s 32 MiB the ring's half-capacity bound is far tighter (`:579-586`). | nothing per record (contents are the backend's). | `Ops_SubData` verbatim (`:1266-1297`): all four arms preserved — `pendingRespecify` early-out, the off-thread/stale queue, the adopted zero-copy no-GL stamp (`:1278`), the `EsprytDisableUploadRing` kill-switch immediate `UploadRangeNow`, and the **default queue-only path that is the Mali WAR-stall fix** (`:1284-1296`, `Managers.h:806-826`). |
|
||
| `ResidentSubData` (`:95`) | `BufferSubDataResident` (`:129`, `kOptional`) | the same `MGPSubData` shape; `bytes` is the app's staging store, **valid for the duration of the call only** (`BufferObject.h:84-85`). | nothing. | `Ops_ResidentSubData` verbatim (`:1304-1312`): copies into `pendingResidentWrites` under `pendingMutex`, issues no GL, stays thread-agnostic. Drained by `DrainResidentWritesNow` (`:1084`) at draw sync (`:1670`) or readback (`:1391`). |
|
||
| `FlushMappedRange` (`:99`) | `ResourceFlushRange` (`:131`) | `MGPFlushRange{Res, Offset, Size, AccessFlags}` — `AccessFlags` is the **application's real** `Flags<BufferMappingAccessBit>`, not a normalised subset (`MGPipeTypes.h:628`). `bytes` = `MappedData() + Offset`. | nothing. | `Ops_FlushMappedRange` verbatim (`:1314-1379`), including the kill-switch-only arm that honours `InvalidateRange`/`InvalidateBuffer`/`Unsynchronized` (`:1353-1375`) and the adopted-store stamp-and-return (`:1334-1337`). |
|
||
| `OnDestroy` (`:103`) | `ResourceDestroy` (`:77`) | `MGPHandleOnly{Handle, Kind = Buffer}`, emitted from `~BufferObject` (`BufferObject.cpp:39-43`) **before** the client frees the slot. | clears Live, drops the record; then the **client** calls `MGPipeSlots().Free(MGPipeKind::Buffer, handle)`. | `Ops_OnDestroy` verbatim (`:1417-1447`) with the moved `SharedPtr<BackendBufferResource>` replaced by the server-side slot table's own entry: the three outcomes (stale generation → zero the id; on-thread → `IsPoolable`/`EnrollIntoPool` else scrub+`glDeleteBuffers`; off-thread → `g_deferredBufferReleases` + the lock-free flag) are unchanged. |
|
||
| `AcquirePersistentMap` (`:114`) | `MapPersistent` (`:78`, `kReplySlot\|kOptional`) / `UnmapPersistent` (`:79`) | `MGPHandleOnly` plus `size` and `seedBytes` (the shadow, still live at this point). | bumps `MapPersistentRoundtrips` (D-B2). | **`Ops_AcquirePersistentMap` (`:1134-1210`) is untouched below its signature** — D-B4, `ARCHITECTURE.md:466`. See D-E. |
|
||
| `ReadbackFromGpu` (`:121`) | `ResourceReadback` (`:132`, `kReplySlot`) | `MGPReadback{Res, Offset = 0, Size = GetSize()}` — the contract is whole-buffer (`BufferObject.h:115-116`). | nothing; the answer travels back (D-D). | `Ops_ReadbackFromGpu` verbatim (`:1382-1419`) except that `bufferObject.WritebackFromBackend(...)` (`:1410`) becomes the reverse callback of D-D. Both branches survive: the adopted-store `DrainResidentWritesNow` + `glFinish()` (`:1387-1394`) and the map/`WritebackFromBackend`/unmap path preceded by `FlushPendingRangesNow` (`:1404`). |
|
||
|
||
#### D-A3 — `BindMask`, and the one bit P8 keys on
|
||
|
||
`MGPResourceDesc::BindMask` (`MGPipeTypes.h:154-157`) is
|
||
`VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY`.
|
||
**The `ELEMENT_ARRAY` bit is the D-B7 switch: with `kCapNeedsHostIndexBytes` set the server mirrors this resource**
|
||
(`ARCHITECTURE.md:385`). Getting it wrong silently disables restart rewriting and multi-draw flattening in split, and it
|
||
is invisible in monolith — no P3a gate can see it.
|
||
|
||
**Decision:** the client maintains a per-`BufferObject` sticky `everBoundAs` mask, ORed at every `glBindBuffer` /
|
||
`glBindBufferBase` / `glBindBufferRange` / VAO element-slot bind / `glVertexAttribPointer` with a bound
|
||
`GL_ARRAY_BUFFER`, and **`BindMask` is emitted on both `ResourceCreate` and every `ResourceRespecify`** (P8 expectation
|
||
1). It is sticky, never cleared, exactly like `ImageBindableHint`'s `everImageBound` (`MGPipeTypes.h:165`). The
|
||
`BufferTarget` → bit mapping is a single `constexpr` table in `MG_Impl/Pipe/ResourceTracker.h` with a
|
||
`static_assert` that it covers every `BufferTarget` enumerator, so a new target cannot be silently unmapped.
|
||
|
||
**Test that can see it in monolith:** `ResourceDescriptorTest.EveryBufferTargetSetsItsBindMaskBit` walks every
|
||
`BufferTarget`, binds a buffer to it, and asserts the applier's stored descriptor carries the bit — on both
|
||
`ResourceCreate` and a following `ResourceRespecify`.
|
||
|
||
#### D-A4 — The server-side resource record, and what identity replaces
|
||
|
||
`GLESBufferResource` (`Managers.h:592-644`) moves out of `PipeResource::m_backend` and into the **seventh** Espryt slot
|
||
table, `BufferImpl::g_backendBufferResources` of kind `MGPipeKind::Buffer`. It is the first table keyed by a
|
||
**client-minted handle carried in the call**, rather than resolved from a `SharedPtr`'s `GetLifetimeId()` — which
|
||
discharges, for these two kinds, the debt `SlotTables.h:70-77` records against itself
|
||
(*"this header is under `MG_Backend/` and it mints handles … the minting and the resolution belong on the client"*).
|
||
`BackendSlotTable` gains a `GetOrCreate(MGPipeHandle)` / `FindByHandle(MGPipeHandle)` overload pair that never calls
|
||
`MGPipeSlots()`.
|
||
|
||
Fields of `GLESBufferResource` that change owner:
|
||
|
||
| field | `Managers.h` | P3a |
|
||
|---|---|---|
|
||
| `id`, `storageSize`, `storageInitialized` | `:598-600` | unchanged, server-side |
|
||
| `contextGeneration` vs `g_bufferContextGeneration` | `:603`, `Managers.cpp:646`, bumped `:1529` | unchanged, server-side |
|
||
| `std::atomic<Uint64> syncedChangeSerial`, mirroring the frontend's `GetChangeSerial()` | `:608` | **re-keyed**: mirrors the applier's `ResourceRecord::Serial`, a server-owned `Uint64` incremented on every `ResourceRespecify` / `ResourceSubData` / `ResourceFlushRange` / `BufferSubDataResident` the applier applies. It is an `MGGen`-class counter (`ARCHITECTURE.md:44-49`) and **never crosses the line**. |
|
||
| `pendingRespecify`, `pendingRanges`, `pendingResidentWrites`, `pendingMutex` | `:614-625` | unchanged, server-side |
|
||
| `drawCleanEpoch` | `:631` | unchanged, server-side |
|
||
| `persistentMapped`, `persistentPtr`, `immutableStorage` | `:637-644` | unchanged, server-side |
|
||
|
||
`IsBufferDrawClean` (`Managers.cpp:1579-1601`, decl `Managers.h:665-675`) today reads five frontend facts per draw per
|
||
buffer — `GetBackendResource()`, `IsMapped()`, `GetSize()`, `GetChangeSerial()`. Under P3a it reads: the slot table
|
||
entry (identity), `record.Desc.Width` (size), `record.Serial` vs `resource->syncedChangeSerial`, and
|
||
`record.HasLiveHostWrites` for the map state. **`HasLiveHostWrites` is stored on the record, is always `false` in P3a,
|
||
and is written by nobody** — it exists so P5's client-side persistent-map push can set it with *zero new record kinds*
|
||
(`ARCHITECTURE.md:481`, P8 expectation 5). A `MOBILEGL_PIPE_VERIFY` assertion pins that it is false, so P5 cannot land
|
||
a silent semantic change under it.
|
||
|
||
**`g_bufferBackendIdGeneration` (`Managers.cpp:1503`, decl `Managers.h:701-707`) survives untouched and stays
|
||
server-local.** It answers "did *I* re-mint the driver id" — a server-owned `MGGen` — and it is the *only* thing that
|
||
catches a re-mint at persistent-map adoption (`:1173`) or immutable-store retire (`:1639`) that moves no client-side
|
||
version. A `set_vertex_buffers` keyed on the client content hash alone reintroduces the bug commit `d7655247` fixed
|
||
(scout-espryt-vao trap 1). D-G4 keeps it in the VAO twin's gate.
|
||
|
||
#### D-A5 — `kNeedsAck`, first use in the tree, and why it is not a bare flag
|
||
|
||
`ARCHITECTURE.md:293`: *"**唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)** … 目录里目前没有条目携带
|
||
`kNeedsAck`(`ResourceRespecify` 是 `kNone`),标记随 P3a 的 buffer 路径落地."* `grep -rn kNeedsAck` confirms no
|
||
`X(...)` row carries it; it is defined at `MGPipe.h:42` and named in `PipeCalls.def:18`'s legend and
|
||
`Doorbell.h:19`'s prose only.
|
||
|
||
Flags are a **per-call** static property, and `ResourceRespecify` serves both `glBufferData` and `glBufferStorage`. A
|
||
bare flag would ack every `glBufferData` in Minecraft's chunk upload.
|
||
|
||
**Decision:** P3a adds `kNeedsAck` to `ResourceRespecify`'s flag word **and** lands the per-record predicate that
|
||
decides:
|
||
|
||
```cpp
|
||
// MGPipeTypes.h, next to MGPipeSetSubDataBufferRange.
|
||
// kNeedsAck on a call means "records of this call MAY require an acknowledgement; this
|
||
// predicate decides per record". glBufferStorage is a real synchronous allocation and is the
|
||
// only entry allowed one (ARCHITECTURE.md:293); glBufferData through the same call is not.
|
||
inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) { return desc.Immutable != 0; }
|
||
```
|
||
|
||
`PipeCalls.def:18`'s legend gains that sentence in the same commit. In monolith the ack is `((void)0)` — the applier is
|
||
one function call away. P5 wires the doorbell to the predicate. `PipeCatalogueTest` gains
|
||
`ResourceRespecifyAcksOnlyImmutableStorage`, which pins the predicate against both idioms and is the negative control
|
||
for a future flag that over-acks. Opcodes do **not** move: a flag-word edit changes the generated tables, so
|
||
`python3 scripts/gen_pipe.py` + `git diff --exit-code -- MobileGL/MG_Pipe/generated` is mandatory in the same commit.
|
||
|
||
### D-B — `buffer_subdata_resident` nullability, and the `map-persistent-roundtrips` counter
|
||
|
||
#### D-B1 — `BufferSubDataResident` stays nullable and stays asymmetric
|
||
|
||
`ARCHITECTURE.md:94`: *"`kOptional`(O,后端表里可为 null:Magma 故意不注册 `BufferSubDataResident` 与
|
||
`SetSwapInterval`)."* Verified in the tree: `g_vulkanBufferBackendOps` (`DirectVulkan/Renderer/VkBufferManager.cpp:105-112`)
|
||
sets `.Respecify`, `.SubData`, `.FlushMappedRange`, `.OnDestroy`, `.AcquirePersistentMap`, `.ReadbackFromGpu` and
|
||
**omits `.ResidentSubData`**. `ROADMAP.md:83` (open question 10) is a standing hands-off: *"null 项保住今天的行为;给
|
||
Magma 补真实现是行为变更,独立 `dev` PR."*
|
||
|
||
**Decision:** `MGPipeResourceOps::SubDataResident` may be null, the frontend checks it exactly as it checks
|
||
`g_bufferBackendOps->ResidentSubData` today (`BufferObject.cpp:371` `LandBytesIntoResidentStore`), and **P3a does not
|
||
give Magma an implementation**. `kCapResidentSubData` (`MGPipeTypes.h:110`, bit 2) is the split-mode spelling of the
|
||
same fact and is not wired in P3a.
|
||
|
||
#### D-B2 — `map-persistent-roundtrips`: definition, home, and why it is non-zero in monolith
|
||
|
||
`ROADMAP.md:19` demands `StorageBufferRegrow` **publish** it; `ROADMAP.md:21` (P5) and `:28` (P11) demand it again;
|
||
`ARCHITECTURE.md:474` sets its meaning: T1 costs *"**每次存储定义一次 round trip**(不是每 store 一次)"*.
|
||
|
||
A counter defined as "round trips actually taken" is 0 by construction in monolith and can never go red — which
|
||
violates `ROADMAP.md:7`'s **每个门必须能因它存在的理由变红**.
|
||
|
||
**Decision — and this is a correction to the reading in `scout-docs-spec.md` §1.2(b):**
|
||
**`map-persistent-roundtrips` counts every `MapPersistent` emission, i.e. every acquisition attempt (mint *or*
|
||
decline), because every one of them requires an answer from the resource owner.** In monolith that answer is free; in
|
||
split it is a real round trip. The number is therefore identical in both modes and equals "one per storage definition"
|
||
exactly as `ARCHITECTURE.md:474` requires, it is **non-zero and assertable today**, and a regression that acquires
|
||
per-draw instead of per-definition shows up immediately.
|
||
|
||
Home: `MG_Util/Metrics/PipeStats.h`, `enum class CallClass`, new member `MapPersistentRoundtrips`, short name **`mpr`**,
|
||
**inside the existing `#if MOBILEGL_PIPE_PUSH` block at `:101-116`** — for the reason that block already states: growing
|
||
the enum in a pull build resizes the counter arrays, the name table and `FormatWindowLine`, which is a G1 break.
|
||
`PipeStats.cpp:173-179`'s `kCallClassNames` gains `"map-persistent-roundtrips"`; `FormatWindowLine` prints
|
||
`mpr=<n>` beside `csom`/`csob` (`PipeStats.cpp:424-427`); `MG_Test/Util/PipeStatsTest.cpp` pins the name in
|
||
`CounterNamesAreStable` (`:260`) and `SummaryLineCarriesEveryClassAndGate` (`:137`).
|
||
|
||
The counting site is the **client** emitter (`MG_Impl/Pipe/ResourceTracker.h`), one `AddCalls` behind the standard
|
||
`if (PipeStats::Enabled())` shape (`PipeStats.h:29`) so an off build pays one predicted branch. **No timer anywhere**
|
||
(`ROADMAP.md:7`, `Tracker.h:35-38`).
|
||
|
||
### D-C — `resource_flush_range`
|
||
|
||
`MGPFlushRange` (`MGPipeTypes.h:628-635`, 32 B) is landed and unchanged:
|
||
|
||
```cpp
|
||
// Carries the application's REAL access flags, not a normalized subset.
|
||
struct MGPFlushRange { MGPipeHandle Res; Uint64 Offset, Size; Uint32 AccessFlags; Uint32 Pad0; };
|
||
MGP_ASSERT_POD(MGPFlushRange, 32);
|
||
```
|
||
|
||
`AccessFlags` is `Flags<BufferMappingAccessBit>` **as the application passed it** — the frontend must not merge
|
||
`INVALIDATE_RANGE` / `INVALIDATE_BUFFER` / `UNSYNCHRONIZED` before emitting, because Espryt's kill-switch arm
|
||
(`Managers.cpp:1353-1375`) reads them per call to choose between `glMapBufferRange(WRITE | INVALIDATE_RANGE? |
|
||
UNSYNCHRONIZED?) + Memcpy + glUnmapBuffer` and `UploadRangeNow`. `NotifyFlushMappedRange` (`BufferObject.cpp:63-70`)
|
||
already has them; the emitter passes them through with a `static_assert` that
|
||
`sizeof(Flags<BufferMappingAccessBit>) <= sizeof(Uint32)`.
|
||
|
||
The default (ring-enabled) arm still just queues the range (`:1344-1348`), the adopted-store arm still stamps and
|
||
returns (`:1334-1337`), and `FlushPendingRangesNow`'s three-tier drain (`:1004-1072`) is a G5-protected function:
|
||
tier 1 `glMapBufferRange(WRITE | INVALIDATE_BUFFER|INVALIDATE_RANGE)` + memcpy for a whole-buffer flush or a range
|
||
`>= kInvalidateRangeMinBytes` (128 KiB, `:975`); tier 2 upload ring + `glCopyBufferSubData`; tier 3 `UploadRangeNow`.
|
||
**Ranges are flushed as queued, never collapsed to their union** (`:1000-1003`) — collapsing re-copies whole chunk
|
||
arenas.
|
||
|
||
### D-D — `resource_readback` and its reverse-channel reply
|
||
|
||
`MGPReadback{MGPipeHandle Res; Uint64 Offset, Size;}` (`MGPipeTypes.h:637-641`, 24 B), flagged `kReplySlot`
|
||
(`PipeCalls.def:132`) = *"答进 `MGPReplySlot`,永不阻塞"* (`ARCHITECTURE.md:94`).
|
||
|
||
Espryt's `Ops_ReadbackFromGpu` today ends in `bufferObject.WritebackFromBackend({mapped, size}, 0)`
|
||
(`Managers.cpp:1410`) — the backend reaching into the frontend's address space. Under handles that is
|
||
`MGPipeCallbacks::OnBufferWriteback(MGPipeHandle res, Uint64 offset, MGPBlobRef bytes)`
|
||
(`MGPipeCallbacks.h:32`), which is landed and unused.
|
||
|
||
**The applier-side reply, in exactly this order** (`ARCHITECTURE.md:288` is a correctness requirement, not a
|
||
preference — *"写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。**"*):
|
||
|
||
1. Espryt maps the store read-only and produces the bytes.
|
||
2. Espryt invokes `OnBufferWriteback(res, 0, {Seg = kMGHostSpanSegNone, Offset = (address), Size})`.
|
||
3. The **client**'s implementation of that callback resolves `res` → `BufferObject*` and calls
|
||
`WritebackFromBackend({bytes, size}, offset)`.
|
||
4. Espryt unmaps, stamps `syncedChangeSerial` (`:1413-1418`) and calls `BumpBufferMutationEpoch()` — **the epoch bump
|
||
stays server-side and happens after the writeback, never before**.
|
||
5. The reply slot is stamped; in monolith the whole sequence is synchronous inside
|
||
`MGPipeApplyResourceReadback`, so `SyncGpuWrites`'s caller sees the reconciled shadow on return, exactly as today.
|
||
|
||
**The handle → object resolution is the client's**, and it is new: `MG_Impl/Pipe/ResourceTracker.h` keeps a
|
||
per-kind `Vector<BufferObject*>` indexed by `MGPipeHandle::Slot`, written at `ResourceCreate` and cleared at
|
||
`ResourceDestroy`. A **raw** pointer is exact here and a `WeakPtr` would be wrong: the entry exists only between
|
||
create and destroy, both of which are emitted from the object's own constructor and destructor, and a readback is only
|
||
ever issued for a live, bound buffer. A `MGPipeHandle::Gen` compare against the allocator (`MGPipeSlots().GenOfSlot`)
|
||
guards a stale handle in debug builds.
|
||
|
||
**The other reverse callback P3a wires: `OnGpuWritten(res, rangeCount, ranges)`** (`MGPipeCallbacks.h:30-31`),
|
||
replacing the three Espryt `MarkGpuWritten` sites (`DirectGLES.cpp:500`, `:544`, `:2012`). `ARCHITECTURE.md:274` calls
|
||
it a **narrowing** channel — *"client 在每个 draw/dispatch 发射点**保守自建** pending 集"* — so the client marks
|
||
conservatively at emission and the callback only ever removes. In P3a the client's conservative set is exactly what
|
||
`MarkShaderStorageBuffersGpuWritten` / `SyncAtomicCounterBuffers` / `MarkWritableImageBufferTexturesGpuWritten` mark
|
||
today, so the observable behaviour is identical and P8/P9's narrowing has its channel (P8 expectation 4).
|
||
|
||
The four other writeback shapes stay on `WritebackFromBackend` for now and are **explicitly out of scope**: the
|
||
`glReadPixels` PBO path (`DirectGLES.cpp:9406-9468`), the `glGetTexImage` PBO path (`:9832-9880`),
|
||
`StoreReadbackRowsToClient` (`:7876-7906`) and `Utils.cpp:2297-2354`, and the XFB capture/scatter pair (`:838-878`,
|
||
`:918-985`). They are texture- and XFB-shaped, they are P4a/P8/P9's, and each already pairs its writeback with a
|
||
`BumpBufferMutationEpoch()` (`:872, 978, 7903, 9462, 9872`; `Utils.cpp:2352`).
|
||
|
||
### D-E — `map_persistent`: the call exists, the implementation is not touched
|
||
|
||
`ARCHITECTURE.md:466` (D-B4), verbatim: *"`AcquirePersistentMap` 是永久的地址空间捐赠(返回 host-visible coherent
|
||
指针,成为该 buffer 的唯一真相源;≥16 MiB 可变 store 由 `TryAdoptLargeStorage` 自动走到,实测 MC 26.3 p99 163→21 ms、
|
||
40→115 fps、省 ~400 MB)。**整个 monolith 改造期一动不动**(D-B4),只有 IPC 那一步会打破它。"*
|
||
|
||
**Decision: `Ops_AcquirePersistentMap` (`Managers.cpp:1134-1210`) changes in exactly two ways and in no other.**
|
||
(1) Its signature becomes `(MGPipeHandle res, Uint64 size, const void* seedBytes)`. (2) The three reads it made through
|
||
`bufferObject` become those parameters: `bufferObject.GetSize()` (`:1139`) → `size`; `bufferObject.MappedData()`
|
||
(`:1184`, the `glBufferStorageEXT` seed) → `seedBytes`; `bufferObject.SetBackendResource(...)` (`:1146`) → the slot
|
||
table's `GetOrCreate(res)`. Every other statement — the four-way capability gate (`:1135-1138`), the stale-generation
|
||
wipe **before** the new stamp (`:1151-1160`), the idempotency hit (`:1163-1165`), the fresh-id sequence with
|
||
`++g_bufferBackendIdGeneration` (`:1169-1179`), the locally defined `0x0040/0x0080/0x0100` bit constants (`:1130-1132`),
|
||
`immutableStorage = true` set as soon as the store exists (`:1188`), the `MGLOG_E_ONCE` decline (`:1189-1197`) and the
|
||
success stamps (`:1198-1209`) — is byte-identical. The tracked wrapper still bumps the epoch **even on decline**
|
||
(`:1470-1476`).
|
||
|
||
Nothing of the tiering work is inherited: T0/T1/T2/T3, the POST probe, `SEG_ADOPT`, `MOBILEGL_IPC_ADOPT_TIER` and the
|
||
client-side push triplet are **P5/P11** (`ARCHITECTURE.md:468-486`, `ROADMAP.md:28`). P3a routes the call, publishes
|
||
`map-persistent-roundtrips`, and stops.
|
||
|
||
The frontend side is likewise untouched: `PipeResource::AdoptPersistentMap` (`PipeResource.h:115`) still drops the CPU
|
||
shadow; `TryAdoptLargeStorage` (`BufferObject.cpp:186-196`) keeps its 16 MiB threshold and its
|
||
`MG_Config::Features.DisableLargeBufferAdoption` kill switch; `MOBILEGL_COHERENT_AS_FLUSH` keeps working
|
||
(`ARCHITECTURE.md:485` — the two `coherent_as_flush: true` Create fixtures must take the same buffer path in both
|
||
modes, which is what makes G3b's name-by-name compare meaningful).
|
||
|
||
### D-F — Pools and deferred release move as-is, and G5 makes that literal
|
||
|
||
`ARCHITECTURE.md:316` puts *"三条 persistent-mapped ring 与 `PersistentRing` 算法、**buffer pool**"* in §9.1's
|
||
do-not-touch list, and `:515` explains what protects them: *"**`present` 与 `eglSwapBuffers` 严格 1:1**:… Espryt
|
||
三个 ring 与 `TrimBufferPool` 的 retire 只在 `Present` 内发生,批量会饿死它们."*
|
||
|
||
**Decision: nine functions in `Managers.cpp` are byte-identical after P3a, and G5 checks it by sha256.**
|
||
|
||
| function | line | why it survives verbatim |
|
||
|---|---|---|
|
||
| `IsPoolable(const GLESBufferResource&)` | `:700` | takes the server-side resource, never the frontend object |
|
||
| `EnrollIntoPool(GLESBufferResource&)` | `:711` | same; the `retireSerial = CurrentFrameSerial() + 1` stamp (`:737-739`) is load-bearing and explained in place |
|
||
| `AcquireFromPool(SizeT)` | `:740` | hands back only entries with `retireSerial <= CompletedFrameSerial()` (`DirectGLES.cpp:10859-10860`) |
|
||
| `TrimBufferPool()` | `:1882` | called once per frame from `Present` (`DirectGLES.cpp:10928`) |
|
||
| `ClearBufferPool()` | `:1913` | context loss |
|
||
| `ProcessDeferredBufferReleases()` | `:1542` | drained per draw from `SyncNeccessaryBuffers`/`SyncComputeBuffers`, fast-outs on the atomic `g_hasDeferredBufferReleases` flag |
|
||
| `CreateRingStorage` | `:1930` | `glBufferStorageEXT` + persistent|coherent map, retires the old store at `CurrentFrameSerial()+1` |
|
||
| `RingAvailable` | `:1989` | self-heals a stale context generation via `ResetRingForNewContext` (`:866-876`) |
|
||
| `RingAllocate` | `:2016` | the fast path is on the hot upload route |
|
||
|
||
The budget constants (`kMaxPoolableBufferBytes` 8 MiB, `kMaxPoolBytes` 64 MiB, `kMaxEntriesPerBucket` 32,
|
||
`Managers.cpp:696-698`), `struct PooledBuffer` (`:687`), `g_bufferPool` (`:693`) and `g_deferredBufferReleases`
|
||
(`:672`) are likewise untouched. The `EnsureBufferResource` pool reseed path (`:1657-1685`) changes only its source of
|
||
bytes (`bufferObject->MappedData()` → the applier's stored `initialBytes` / the shadow pointer the client last sent),
|
||
and is **not** in the G5 set for that reason.
|
||
|
||
**Recorded but not fixed:** `OnBackendContextDestroyed` (`:1537-1538`) resets `g_uboRing` and `g_unpackRing` but not
|
||
`g_uploadRing`; `RingAvailable` (`:1996-1998`) heals it on first use, so it is benign. **P3a preserves the asymmetry
|
||
deliberately** and records it as a `dev`-side follow-up rather than fixing it in flight (`ROADMAP.md:88`'s standing
|
||
rule about not taking unrelated fixes into the split work).
|
||
|
||
### D-G — Vertex elements: three calls, both views, and how the CSO is keyed
|
||
|
||
#### D-G1 — [deviation] The vertex-elements CSO is identity-addressed in P3a, not content-addressed
|
||
|
||
`ARCHITECTURE.md:63` puts vertex-elements CSOs in the client-side content-addressed scheme with capacity 1024, and
|
||
`:55` names `VertexInputStateFactory::m_cache` as the server-side counterpart.
|
||
|
||
**That is right for Magma and wrong for Espryt, and P3a is Espryt-only.** Espryt has no vertex-elements CSO at all: it
|
||
has a **per-VAO twin** (`BackendVertexArrayObject`, `Managers.h:826-955`) that owns one driver VAO name
|
||
(`m_backendVAOId`, `:913`), 32 client-array scratch buffer ids (`:914`), 32 converted-fp64 scratch ids (`:918`) and
|
||
their memos (`:919-920`). Two frontend VAOs with identical *format* cannot share that object, because a driver VAO also
|
||
holds the element-array binding and the per-attribute buffer bindings that `set_vertex_buffers` / `set_index_buffer`
|
||
re-emit into it. A shared CSO would force those to be re-emitted on every `BindVertexElements` — strictly more work
|
||
than today.
|
||
|
||
**Decision:** in P3a a `VertexElementsCso` handle is minted **per frontend `VertexArrayObject`**, off `GetLifetimeId()`
|
||
through `MGPipeSlots().Acquire(MGPipeKind::VertexElementsCso, lifetimeId)` **on the client**, and freed on the VAO's
|
||
existing death notice (`VertexArrayObject.cpp:53`, consumed at `Managers.cpp:209-211`). `CreateVertexElements` is
|
||
**re-issued on the same handle** whenever the configuration moves — legal, because `MGPipeHandle::Gen` increments only
|
||
on slot reuse and never on respecify (`MGPipeHandles.h:44-48`). The client-side content-addressed vertex-elements cache
|
||
and its 1024 capacity are **not built in P3a**; they are recorded as **P7** work, when Magma's
|
||
`VertexInputStateFactory` takes the CSO over and content addressing is what its `VkPipelineVertexInputStateCreateInfo`
|
||
actually wants. The integrator annotates `ARCHITECTURE.md:63` accordingly.
|
||
|
||
Consequences that make the package smaller, and they are the point: no hash, no probe, no `memcmp`, no LRU, no
|
||
`DeleteVertexElements`-on-evict, and no second twin resolve per draw — which also protects
|
||
`BackendSlotTable`'s single-entry `m_memoLifetimeId`/`m_memoHandle` front memo (`SlotTables.h:407-431`), whose comment
|
||
names `ResolveVaoTwin` as one of the two callers it exists for (scout-espryt-vao trap 7).
|
||
`DeleteVertexElements` is emitted from **one** place: the death notice.
|
||
|
||
#### D-G2 — The two views, and what they carry on the wire
|
||
|
||
`MGPVertexElements` (`MGPipeTypes.h:253-259`, 40 B) is unchanged:
|
||
|
||
```cpp
|
||
struct MGPVertexElements { MGPipeHandle Cso; Uint32 AttributeCount; Uint32 BindingPointCount; MGPBlobRef Blob; };
|
||
MGP_ASSERT_POD(MGPVertexElements, 40);
|
||
```
|
||
|
||
`ARCHITECTURE.md:130` demands the blob carry the resolved `VertexAttribute[]` **and** `VertexBufferBindingPoint[]`.
|
||
Neither can travel as the `MG_State::GLState` struct: `VertexAttribute` holds a `SharedPtr<BufferObject> Buffer`
|
||
(`MGPipeValueTypes.h:516`) and `VertexBufferBindingPoint` holds one too (`:533`), and **a payload never contains a
|
||
pointer** (`ARCHITECTURE.md:116`). P3a therefore adds two POD wire forms to `MGPipeValueTypes.h`:
|
||
|
||
```cpp
|
||
// The resolved flat attribute view. Buffer identity does NOT travel here - it travels in
|
||
// set_vertex_buffers, which is what keeps this record stable while buffers change under it.
|
||
// Stride is the RESOLVED distance and a surviving 0 can only have come from the binding model
|
||
// (MGPipeValueTypes.h:496-503); collapsing it back into the element size is what made
|
||
// KHR-GL43.vertex_attrib_binding.basic-input-case7/8 read past the buffer.
|
||
struct MGPVertexAttribWire {
|
||
Uint64 Offset; // 0
|
||
Int32 Stride; // 8
|
||
Uint32 Type; // 12 DataType
|
||
Uint8 Size; // 16 1..4; GL_BGRA keeps 4
|
||
Uint8 Enabled; // 17
|
||
Uint8 Normalized; // 18
|
||
Uint8 IsInteger; // 19
|
||
Uint8 IsLong; // 20 CARRIED SEPARATELY from Type == Float64 (ARCHITECTURE.md:130)
|
||
Uint8 IsBgra; // 21
|
||
Uint8 BindingIndex; // 22 which MGPVertexBuffer entry feeds it (< MAX_VERTEX_ATTRIBS = 32)
|
||
Uint8 Pad0; // 23
|
||
};
|
||
MGP_ASSERT_POD(MGPVertexAttribWire, 24);
|
||
|
||
// The ARB_vertex_attrib_binding view. Buffer identity is again in set_vertex_buffers.
|
||
struct MGPVertexBindingPointWire {
|
||
Uint64 Offset; // 0
|
||
Int32 Stride; // 8 GL 4.6 core table 23.4: the INITIAL value is 16, not 0
|
||
Uint32 Divisor; // 12
|
||
};
|
||
MGP_ASSERT_POD(MGPVertexBindingPointWire, 16);
|
||
```
|
||
|
||
`Divisor` is **not** in the attribute view: it is resolved per binding point and travels in
|
||
`MGPVertexBuffer::Divisor`, which is where Espryt's `glVertexAttribDivisor(attribIndex, …)` (`Managers.cpp:2613-2615`)
|
||
now reads it. `LegacyStride`/`LegacyPointer` (`MGPipeValueTypes.h:518-526`) **stay client-side** — they are the
|
||
`glGetVertexAttrib*` query answers and nothing but the query path reads them (`ARCHITECTURE.md:130`).
|
||
|
||
Blob layout: `MGPVertexAttribWire[AttributeCount]` immediately followed by
|
||
`MGPVertexBindingPointWire[BindingPointCount]`, both in ascending index order, `AttributeCount` and
|
||
`BindingPointCount` each ≤ `VertexArrayObject::MAX_VERTEX_ATTRIBS` (32). The applier refuses a record whose declared
|
||
counts do not match the blob's declared size — `Fatal{ProtocolCorruption}` (`ARCHITECTURE.md:119`).
|
||
|
||
**Why the second view travels even though nothing reads it.** `grep -rn 'VertexBufferBindingPoint|
|
||
GetAttributeBindingIndex|GetAttributeRelativeOffset' MobileGL/MG_Backend/` returns **zero hits** — neither backend has
|
||
ever read a binding point, and `MGPipeValueTypes.h:529-531` says so as a design statement. So
|
||
`ARCHITECTURE.md:130`'s stated reason (pointer stride 0 vs binding stride 0) is stale: the frontend already resolves
|
||
it. The reason that survives: `MGPVertexElements` **declares** `BindingPointCount`, `PipeFields.def:71-72` names it,
|
||
and a record whose declared counts do not describe its own blob is a shape the applier's bounds gate would have to
|
||
police forever. Carrying both keeps the record self-describing, and the cost is paid **once per configuration change,
|
||
not per draw** — the CSO is a create/bind pair, and the blob rides only on `CreateVertexElements`. The integrator
|
||
corrects `ARCHITECTURE.md:130`'s stated reason to this one and records the measured per-frame blob bytes in
|
||
`MEASUREMENTS.md`; trimming the second view is a P13 retune item, not P3a's.
|
||
|
||
#### D-G3 — What the tracker emits, and when
|
||
|
||
Dirty bit 5 `NewVertexElements` already has its exact shutter and it does not change:
|
||
`vaoIdentity = MGPipeMixShutter(vao->GetLifetimeId(), vao->GetConfigVersion())`, 0 when unbound
|
||
(`Tracker.h:210-213`). It fires both when the bound VAO changed and when the bound VAO's configuration changed, so the
|
||
emitter keeps two latches and distinguishes:
|
||
|
||
```
|
||
if (vao == nullptr) -> BindVertexElements(kMGPipeNullHandle)
|
||
else if (lifetimeId != m_lastBoundLifetimeId) -> (re)Create if configVersion moved, then Bind
|
||
else if (configVersion != m_lastEmittedConfigVersion) -> CreateVertexElements on the same handle (no rebind)
|
||
```
|
||
|
||
`m_lastEmittedConfigVersion` is stored **per handle**, not globally, in `MG_Impl/Pipe/VertexInputEmit.h`'s
|
||
slot-indexed table, so ping-ponging between two VAOs does not re-create either. A `Uint32` config version
|
||
(`VertexArrayObject.h:107`) does not wrap in any realistic run and is compared directly; the tracker's
|
||
`MGPipeWidenedCounter` (`Tracker.h:159-177`) is **not** needed for it.
|
||
|
||
Order (`ARCHITECTURE.md:194`, D-B3): all `set_*`/`bind_*` of a verb complete before the verb; between them the only
|
||
ordering requirement is *"资源 create 先于对它的 bind"*. The recommended emission order's vertex segment is
|
||
*"… render state/dynamic → **vertex elements/buffers/index/attrib defaults** → patch/XFB → verb"* and it is code
|
||
organisation, not contract. P3a emits in exactly that order inside `MGPipeValidateForVerb`'s step 3
|
||
(`PipeFill.cpp:1087-1128`), after the four P2 emitters.
|
||
|
||
#### D-G4 — What the applier stores, and what it retires in the twin
|
||
|
||
`MGPipeApplierState` (`PipeApply.h:48-87`) gains, all per context:
|
||
|
||
```cpp
|
||
struct MGPipeResourceRecord { // indexed by MGPipeHandle::Slot, kind Buffer; slot 0 never live
|
||
Uint32 Gen; Bool Live;
|
||
MGPResourceDesc Desc; // the last create/respecify, verbatim
|
||
Uint64 Serial; // server-owned MGGen: ++ on every applied mutation
|
||
Bool HasLiveHostWrites; // always false in P3a; P5 sets it (ARCHITECTURE.md:481)
|
||
};
|
||
struct MGPipeVertexElementsRecord { // indexed by MGPipeHandle::Slot, kind VertexElementsCso
|
||
Uint32 Gen; Bool Live;
|
||
Uint32 AttributeCount, BindingPointCount;
|
||
Array<MGPVertexAttribWire, 32> Attributes;
|
||
Array<MGPVertexBindingPointWire, 32> BindingPoints;
|
||
Uint64 ContentSerial; // server-owned MGGen: ++ on every CreateVertexElements
|
||
};
|
||
Vector<MGPipeResourceRecord> Resources;
|
||
Vector<MGPipeVertexElementsRecord> VertexElementsCsos;
|
||
MGPipeHandle BoundVertexElements;
|
||
Array<MGPVertexBuffer, 32> VertexBuffers; Uint32 VertexBufferStart, VertexBufferCount;
|
||
Uint32 VertexFetchBaseInstance; // the explicit field of D-H
|
||
Uint64 VertexBuffersSerial; // server-owned MGGen: ++ on every SetVertexBuffers
|
||
MGPIndexBuffer IndexBuffer; Uint64 IndexBufferSerial;
|
||
Uint64 MapPersistentRoundtrips;
|
||
```
|
||
|
||
The three `Serial` counters are **`MGGen`-class: server-owned, monotone `Uint64`, and they never cross the line**
|
||
(`ARCHITECTURE.md:44-49`; the norm is *"任何 MGPipe 调用不得要求 client 提供或知晓 `MGGen`"*). They are what retires
|
||
the twin's wrapping-`Uint16`-plus-identity patches. `BackendVertexArrayObject::SyncToBackend`'s gate
|
||
(`Managers.cpp:2437-2472`) becomes:
|
||
|
||
| today | after P3a |
|
||
|---|---|
|
||
| `m_hasSyncedConfigVersion` + `m_syncedConfigVersion` (`Managers.h:937-938`) vs `stateVAOObject->GetConfigVersion()` | `m_syncedElementsHandle` + `m_syncedElementsSerial` vs `BoundVertexElements` + `rec.ContentSerial` |
|
||
| `Array<VertexAttributeVersion,32> m_syncedAttributeVersions` (`:939-940`) | **deleted** — the applier's stored `Attributes[]` *is* what was last pushed, so a per-attribute version compare has nothing left to prove |
|
||
| `m_syncedIndexBufferVersion` (`Uint16`, `:926`) + `m_syncedIndexBufferObject` (raw pointer, `:931`) | `m_syncedIndexSerial` vs `IndexBufferSerial` — one monotone `Uint64`, no wrap, no identity patch. **This is the Track H re-key `ARCHITECTURE.md:363` counts.** |
|
||
| `m_syncedFetchBaseInstance` (`:946`) + `baseInstanceDirty` (`Managers.cpp:2462-2464`) | **deleted from the gate**: a baseInstance change is a `ContentHash` input (D-H), so it moves `VertexBuffersSerial`. The field survives as the record of *what was last emitted*, which is what the next sync must correct — `Managers.h:941-946`'s note holds verbatim. |
|
||
| `m_syncedBufferIdGeneration` (`:952`) vs `BufferImpl::g_bufferBackendIdGeneration` (`:2446-2447`) | **unchanged**. Server-local, and the only thing that catches a driver-id re-mint no client counter moves. |
|
||
| `m_hasConvertedFloat64Attribute` (`:924`, set `:2530`, cleared `:2473`) | **unchanged**. A narrowed fp64 stream is derived from buffer *content*, which no version covers, so the early-out must not be trusted while one is live (scout trap 3). |
|
||
|
||
The per-attribute walk (`Managers.cpp:2477-2617`) reads `rec.Attributes[i]` and `st.VertexBuffers[i]` instead of
|
||
`GetAllAttributeVersions()` / `GetAllAttributes()`. **Its every branch survives**: the `SwitchVersion`
|
||
enable/disable block, the fp64 narrowing with its Adreno disable (D-I), the zero-stride binding-API path
|
||
(`:2545-2560`), `BindAttributeBuffer` (`:2330-2346`) resolving the driver id from the slot table instead of
|
||
`attrib.Buffer`, the `glVertexAttribPointer`/`IPointer` at `fetchOffset` (`:2586-2600`), the BGRA refusal probe
|
||
(`:2568-2611`) and `glVertexAttribDivisor` (`:2613-2615`).
|
||
|
||
`ResolvedDrawBuffers` (`Managers.h:833-869`) keeps its shape and re-keys: `Entry::frontend` (a raw `BufferObject*`)
|
||
becomes `MGPipeHandle`, `iboFrontend` likewise, and `configVersion` becomes the pair
|
||
`{elementsHandle, elementsSerial}`. `vboCleanEpoch`/`iboCleanEpoch` against `CurrentBufferMutationEpoch()` and the
|
||
per-entry `IsBufferDrawClean` re-probe (`DirectGLES.cpp:605-621`) are unchanged.
|
||
|
||
### D-H — `set_vertex_buffers` with an explicit `baseInstance`
|
||
|
||
#### D-H1 — The payload change
|
||
|
||
`MGPVertexBuffers` (`MGPipeTypes.h:367-371`) goes from 16 to **24** bytes:
|
||
|
||
```cpp
|
||
// Var-tail header: MGPVertexBuffer[Count] follows.
|
||
struct MGPVertexBuffers {
|
||
Uint32 Start, Count;
|
||
// The vertex-FETCH base instance these offsets are valid for. Draw state, not VAO state
|
||
// (Managers.h:941-946), and NOT the same thing as MGPDrawInfo::StartInstance, which is the
|
||
// GL draw's baseInstance and feeds gl_BaseInstance. The server decides whether to emulate
|
||
// the fetch shift or let GL_EXT_base_instance do it (ARCHITECTURE.md:112: emulation is
|
||
// server-owned). It is a ContentHash input - see MGPipeTypes.h's suppressor note.
|
||
Uint32 BaseInstance;
|
||
Uint32 Pad0;
|
||
Uint64 ContentHash;
|
||
};
|
||
MGP_ASSERT_POD(MGPVertexBuffers, 24);
|
||
```
|
||
|
||
`MGPVertexBuffer` (the per-entry struct, `:356-364`, 32 B) is **unchanged** — its `Pad0` at `:362` stays padding. Per
|
||
entry the shift is redundant and would let a malformed record disagree with itself.
|
||
|
||
Same-commit consequences, none optional: `MGP_FIELDS_MGPVertexBuffers` (`PipeFields.def:99-100`) gains
|
||
`F(BaseInstance)` (`gen_pipe.py` asserts every field list names exactly the struct's direct members, `Pad<n>` excluded,
|
||
`PipeFields.def:16-19`); `python3 scripts/gen_pipe.py` regenerates `MG_Pipe/generated/*.inc` including G3's wire-record
|
||
`static_assert`s; `git diff --exit-code -- MobileGL/MG_Pipe/generated` is the gate. **No opcode moves** — a payload
|
||
size change is not a catalogue edit (`PipeCalls.def:20-23`).
|
||
|
||
#### D-H2 — What replaces the ambient global, and what stays server-side
|
||
|
||
Today the shift is driven by a **process global**: `g_pendingFetchBaseInstance` (`Managers.cpp:2358`),
|
||
`SetPendingFetchBaseInstance`/`GetPendingFetchBaseInstance` (`:2360-2366`), scoped by
|
||
`VertexArrayImpl::ScopedFetchBaseInstance` around **exactly three** draw entry points —
|
||
`DrawElementsInstancedBaseVertexBaseInstance` (`DirectGLES.cpp:5135`), `DrawElementsInstancedBaseInstance` (`:5172`),
|
||
`DrawArraysInstancedBaseInstance` (`:5224`) — with the value from `EmulatedFetchBaseInstance(baseinstance)` (`:5128-5130`),
|
||
which is `UseNativeBaseInstance() ? 0 : baseinstance` (`:5122-5124`). It is consumed in the VAO sync at
|
||
`Managers.cpp:2463`, `:2524`, `:2589` (`attrib.Offset + BaseInstanceByteShift(attrib, fetchBaseInstance)`) and inside
|
||
`SyncFloat64AttributeAsFloat32` (`:2800-2806`). `BaseInstanceByteShift` (`:2377-2383`) is
|
||
`baseInstance * attrib.Stride`, only for `Divisor != 0`, zero for a stride-0 array.
|
||
|
||
**An ambient process global cannot cross a pushed boundary.** Decision, in three parts:
|
||
|
||
1. **The client emits the draw's raw `baseInstance` in `MGPVertexBuffers::BaseInstance`.** A new one-line client
|
||
setter `MGPipeSetPendingBaseInstance(Uint32)` in `MG_Impl/Pipe/PipeFill.h` is called immediately before
|
||
`MGP_FILL(...)` at the frontend entry points corresponding to those three, and **`MGPipeLeaveVerb` resets it to 0**
|
||
(`PipeFill.cpp:607`). The reset is what keeps
|
||
`DrawParametersScenario.APlainDrawAfterABaseInstancedOneSeesZeroAgain` (`:233`) true.
|
||
2. **The server decides whether to shift.** Espryt's applier computes
|
||
`fetchBaseInstance = UseNativeBaseInstance() ? 0 : hdr.BaseInstance` and stores it in
|
||
`MGPipeApplierState::VertexFetchBaseInstance`. `UseNativeBaseInstance()` is a backend capability and stays in
|
||
`MG_Backend` — emulation is server-owned (`ARCHITECTURE.md:112`, `:225`).
|
||
`g_pendingFetchBaseInstance`, `SetPendingFetchBaseInstance`, `GetPendingFetchBaseInstance` and
|
||
`ScopedFetchBaseInstance` are **deleted**, along with the three scope objects at `:5135`, `:5172`, `:5224`.
|
||
`EmulatedFetchBaseInstance` (`:5128`) and `BaseInstanceByteShift` (`Managers.cpp:2377`) survive and move inside the
|
||
applier arm.
|
||
3. **`BaseInstance` is a `ContentHash` input, and this is a hard requirement, not a nicety.**
|
||
`MGPipeSuppressorSlot::SetVertexBuffers` (`SetHashSuppressor.h:37`) suppresses on an unchanged hash. If
|
||
`BaseInstance` moved but the buffer set did not, and the hash did not include it, the emission would be suppressed
|
||
and the server would keep the previous shift — exactly the bug `baseInstanceDirty` (`Managers.cpp:2462-2464`)
|
||
exists to prevent. The hash is `xxHash(MGPVertexBuffer[Count]) ⊕ mix(Start, Count, BaseInstance)`; hash 0 stays
|
||
reserved for "never emitted" (`SetHashSuppressor.h:23-25, 54`).
|
||
Test: `VertexInputEmitTest.ABaseInstanceChangeAloneStillEmitsTheVertexBufferSet`, and its counterpart
|
||
`…AnUnchangedSetWithAnUnchangedBaseInstanceEmitsNothing`.
|
||
|
||
**Indirect draws are untouched.** None of the three `ScopedFetchBaseInstance` sites is in an indirect loop —
|
||
`DrawElementsIndirect` (`DirectGLES.cpp:5216`) and the multi-draw tiering never used the emulation. Per-command
|
||
baseInstance stays exactly where it is: resolved server-side out of the indirect commands, zero wire traffic
|
||
(`ARCHITECTURE.md:112`: *"multi-draw 分档与 restart 重写永远由 server 拥有"*). The client emits `BaseInstance = 0` for
|
||
every indirect path.
|
||
|
||
**`MGPDrawInfo::StartInstance` (`MGPipeTypes.h:719`, `PipeFields.def:189`) is a different field and stays.** It is the
|
||
GL draw's baseInstance — what `SetCurrentBaseInstance` feeds to `gl_BaseInstance`
|
||
(`BaseInstanceInjectionTest.cpp:77-155`) and what the native `GL_EXT_base_instance` entry point consumes. The two
|
||
being separate and both explicit is the disambiguation `ROADMAP.md:19` asks for. `DrawParametersScenario` (all eight
|
||
cases, `:213-327`) is the gate on the pair.
|
||
|
||
#### D-H3 — What the emitted set contains
|
||
|
||
Espryt consumes **resolved attributes**, so the client emits one `MGPVertexBuffer` per **enabled attribute** with
|
||
`BindingIndex = attribIndex`:
|
||
|
||
```
|
||
Res = the attribute's buffer handle, or kMGPipeNullHandle for a client-memory array
|
||
Offset = 0 (the attribute's own byte offset lives in MGPVertexAttribWire::Offset)
|
||
Stride = the RESOLVED stride (0 is meaningful: binding model, every vertex reads the same element)
|
||
Divisor = the resolved divisor
|
||
BindingIndex = the attribute index
|
||
```
|
||
|
||
`Start` = 0, `Count` = `MAX_VERTEX_ATTRIBS` truncated to the highest enabled attribute + 1, matching the 32-attribute
|
||
prefix walk `ARCHITECTURE.md:164` specifies for dirty bit 9.
|
||
|
||
**Client-memory arrays** (`Res == kMGPipeNullHandle`) are the one shape whose store does not exist at emission time:
|
||
`SyncClientSideAttributesForDrawArrays` (`Managers.cpp:2651-2755`) runs **after** `PrepareForDraw`, at the draw entry
|
||
point, and only for `DrawArrays`/`MultiDrawArrays` (`DirectGLES.cpp:4805-4821`, `:4837-4866`). The push ordering
|
||
contract permits this (`ARCHITECTURE.md:194` requires only "create before bind" *for resources that are named*), and a
|
||
null handle is exactly how the server learns "this attribute is client-sourced, upload it yourself". P3a moves nothing
|
||
here: the uploader, its `uploadSize = (first + count - 1) * stride + elementSize` formula (`:2726`), its
|
||
`StageVertexClient` byte accounting (`:2701-2704`, `:2740-2743`) and its two call sites are unchanged. Moving that
|
||
resolution to the client is **P8** (`HostResolve.cpp`, `ROADMAP.md:25`).
|
||
|
||
`SyncNeccessaryBuffers` (`DirectGLES.cpp:572-707`) also does SSBO binding points and
|
||
`MarkShaderStorageBuffersGpuWritten()` at `:705-706` — **unrelated to vertex input and must not be dragged into
|
||
`set_vertex_buffers`** (scout trap 6). Those rows are dirty bits 15/16/17 and belong to P4b.
|
||
|
||
#### D-H4 — Dirty bit 9, narrowed
|
||
|
||
`Tracker.h:243-244` today: `now[NewVertexBuffers] = MGPipeMixShutter(ctx.GetAnyVaoAttributeGeneration(), vaoIdentity)`.
|
||
That is already exact for P3a: `VertexArrayState::m_anyVaoAttributeGeneration` (`VertexArrayState.h:36-44`) is bumped by
|
||
`MGP_NOTE_AGGREGATE(VaoAttribute)` inside all three `Bump*Version` functions (`VertexArrayObject.cpp:317, 324, 331`),
|
||
which are the only writers of an attribute's format, buffer or enable state. A driver-id re-mint that moves no client
|
||
counter is caught server-side by `g_bufferBackendIdGeneration` (D-A4). Redundant emissions are killed by the
|
||
suppressor. **Keep the shutter; wire the emission; add `BaseInstance` to the hash.**
|
||
|
||
`Tracker.h:69-78`'s comment labels bits 9 and 10 "object class: computed and counted, emitted from P3b/P4b on".
|
||
**`ROADMAP.md:19` puts both in P3a.** Package B corrects the comment in the same commit that wires them.
|
||
|
||
### D-I — `set_index_buffer`, and the two restore traps
|
||
|
||
`MGPIndexBuffer` (`MGPipeTypes.h:373-380`, 24 B) is unchanged and carries its own rule in place:
|
||
*"An independent call, NOT a subset of the VAO configuration version (D5)."* `ARCHITECTURE.md:99` states it in prose and
|
||
the code already agrees — the index slot is explicitly not covered by `m_configVersion` (`Managers.h:932-936`).
|
||
|
||
Emission: `Res` = the handle of `GetIndexBufferBindingSlot().GetBoundObject()`, `Offset` and `IndexSize` from the draw
|
||
(0 and 0 when no draw is pending — the applier stores them and the draw verb overrides). Shutter, **narrowed** (this is
|
||
the narrowing `Tracker.h:245-248` records as owed — today bit 10 is `MixShutter(buffers, vaoIdentity)` and over-fires
|
||
on *any* buffer write anywhere):
|
||
|
||
```
|
||
now[NewIndexBuffer] = MGPipeMixShutter(vaoIdentity,
|
||
MGPipeMixShutter(widen(slot.GetVersion()), boundObject ? boundObject->GetLifetimeId() : 0));
|
||
```
|
||
|
||
`slot.GetVersion()` is a **wrapping `Uint16`** (`MG_Util/Types.h:198`, bumped only on a real change at `:189-195`), so
|
||
it goes through `MGPipeWidenedCounter` (`Tracker.h:159-177`) at the tracker boundary; the bound object's lifetime id
|
||
joins it because identity is what closes the wrap hole (`Managers.h:927-931` explains the same reasoning for the
|
||
twin). This is exactly `ARCHITECTURE.md:165`'s *"索引 slot 版本 + 绑定对象 `{slot,gen}`"*.
|
||
|
||
**Two traps that any re-emit must preserve, and P3a preserves them by not touching them:**
|
||
|
||
1. `ScopedRestartIndexSubstitution::~ScopedRestartIndexSubstitution` (`DirectGLES.cpp:4788-4791`) restores the exact
|
||
previous GL name captured at `:4776` via `BoundElementArrayBufferId()` (`:4629-4634`).
|
||
2. `MultiDrawImpl` does the same at `MultiDraw.cpp:101-109` (whose comment states the memo hazard verbatim) and
|
||
`:929-937`.
|
||
|
||
Both operate on **driver ids**, which stay server-side, and both exist *because the twin memoised that it already
|
||
synced the binding*. The memo is now `m_syncedIndexSerial`, and the restore must leave it consistent: the two dtors
|
||
restore the id **without** touching the serial, which is correct — the serial records what the *applier* last said, and
|
||
the scope restored what the applier said. `ResidentIndexScenario`'s five cases (`:186, 225, 271, 311, 351`) and
|
||
`PrimitiveRestartScenario`'s seven (`:215, 236, 263, 281, 306, 332, 368`) are the gate.
|
||
|
||
Restart rewriting and multi-draw flattening stay **server-owned with zero wire traffic** (`ARCHITECTURE.md:112`,
|
||
`:222`), including `kMaxRestartRewriteBytes` = 64 MiB (`DirectGLES.cpp:4510`), the whole-buffer EBO rewrite
|
||
(`:4691-4721`), the `UBYTE→USHORT→UINT` widening (`:4746-4764`), `kMaxFlattenedIndices` = 1<<24 (`MultiDraw.cpp:74`)
|
||
and the six-tier ladder (`MultiDraw.h:15-31`). **P3a must not move either into the client as a convenience**
|
||
(P8 expectation 8). The two CPU index reads that precede them —
|
||
`indexBuffer->SyncPersistentMappedRange(); indexBuffer->SyncGpuWrites(); MappedData();` at
|
||
`DirectGLES.cpp:4709-4713` and `MultiDraw.cpp:508-514` — stay exactly where they are; moving them is P8's
|
||
`HostResolve.cpp` plus the index host mirror.
|
||
|
||
### D-J — The Adreno SIGSEGV workarounds, and everything else that must NOT change
|
||
|
||
`ROADMAP.md:19` says "Adreno SIGSEGV workaround 保留"; `ARCHITECTURE.md:316` repeats it. `grep SIGSEGV
|
||
MobileGL/MG_Backend/DirectGLES/` returns three hits and they are **two distinct workarounds plus one cross-reference**.
|
||
Both live inside `BackendVertexArrayObject::SyncToBackend`, i.e. inside P3a's own vertex-elements half:
|
||
|
||
1. **A `GL_DOUBLE`/`IsLong` attribute with no buildable stream** (`Managers.cpp:2517-2540`, rationale `:2516-2521`,
|
||
forward reference `Managers.h:887-895`). Becoming long bumps `FormatVersion`, not `SwitchVersion`, so the
|
||
enable/disable block above does not re-run and an already-enabled array would stay enabled with no pointer and no
|
||
`GL_ARRAY_BUFFER` binding — *"the Adreno driver dereference[s] null inside the next draw and take[s] the process
|
||
with it (SIGSEGV in libGLESv2_adreno, KHR-GL43.vertex_attrib_binding.basic-input-case4)"*. Fix: try
|
||
`SyncFloat64AttributeAsFloat32` (`:2757-2874`), and on failure `glDisableVertexAttribArray(attribIndex)` (`:2537`)
|
||
with an `MGLOG_W_ONCE`. The re-enable at `:2529` is explicitly non-redundant.
|
||
2. **A `GL_BGRA` vertex size the driver refuses** (`:2568-2612`, rationale `:2568-2582`): drain `glGetError`
|
||
(`:2585-2588`), issue `glVertexAttribPointer` with `GL_BGRA` as the size (`:2593-2596`), re-check (`:2603`),
|
||
disable on refusal. Deliberately **only** for `attrib.IsBgra` — the per-draw sync must not grow a `glGetError`
|
||
round trip for real formats.
|
||
|
||
Both depend on observing a **driver** error at attribute-declaration time. **Decision: both stay server-side verbatim,
|
||
and the disable is a server-local decision, never a client one.** The wire enabler is already in place —
|
||
`MGPVertexAttribWire` carries `IsLong` and `Type == Float64` **separately** (D-G2, `ARCHITECTURE.md:130`), and fp64
|
||
narrowing remains a server-side emulation (`ARCHITECTURE.md:225`) advertised by `kCapFloat64VertexAttrib`
|
||
(`:106`). `ConvertedFloat64Stream` (`Managers.h:901-909`) re-keys its `sourceLifetimeId` to the buffer handle's
|
||
`{slot,gen}` — one of `ARCHITECTURE.md:363`'s eleven direct deletions (*"`ConvertedVertexStreamKey` 的 `sourcePin`"*) —
|
||
and keeps `sourceChangeSerial` (now the applier's `Serial`), `sourceOffset`, `sourceStride`, `componentCount`,
|
||
`elementCount`, and its rule of **never being trusted for a persistently mapped buffer** (`Managers.cpp:2812-2820`).
|
||
|
||
**The complete must-not-change list for package C**, each with an existing test:
|
||
|
||
| thing | where | pinned by |
|
||
|---|---|---|
|
||
| `AcquirePersistentMap`'s body (D-B4) | `Managers.cpp:1134-1210` | D-E; `BufferTest.cpp`'s adopted-store family (`:1658, 1724, 1952, 1990, 2029, 2058, 2093, 2165-2316, 2451-2943`) |
|
||
| the nine pool / deferred-release / ring functions | D-F's table | **G5** |
|
||
| the two Adreno SIGSEGV workarounds | above | `VertexAttribBindingScenario` (`:241-734`), `DoublePrecisionScenario` (`:863, 916`) |
|
||
| the Mali WAR-stall fix (queue-only `SubData`) | `Managers.cpp:1284-1296`, `Managers.h:806-826` | `CrossFrameBufferScenario` (`:409-518`), `StreamedArenaScenario` (`:675, 731`) |
|
||
| `InvalidateIndexedBufferBindingShadowsForId` on an extent move | `Managers.cpp:933` | `StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding` (`:124`) |
|
||
| the element-array binding restores | `DirectGLES.cpp:4788-4791`, `MultiDraw.cpp:101-109, 929-937` | `ResidentIndexScenario`, `PrimitiveRestartScenario`, `MultiDrawScenario` (10 cases, `:225-526`) |
|
||
| `EnsureProcessTeardownSentinel()` armed on the slot table's first insertion | `Managers.h:52-60`, `Managers.cpp:161-170` | `SanityTest.cpp:2650-2665, 2765-2790` |
|
||
| the twin destructors' `g_backendContextGeneration` compare | `Managers.h:64-70` | `SanityTest.cpp:2650-2665, 2786-2790` |
|
||
| `TempBufferTarget` = `GL_ARRAY_BUFFER` and its redundant-bind cache | `Managers.h:552`, `Managers.cpp:631-632` | `SanityTest.cpp:2535` (`PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero`); **any new path that binds `GL_ARRAY_BUFFER` behind `BindBufferId`'s back silently corrupts it** (scout trap 7) |
|
||
| `g_bufferBackendIdGeneration` server-local | `Managers.cpp:1503`, read `:2446`, `:2649` | scout trap 1; commit `d7655247` is the bug it prevents |
|
||
| no `CollectGarbageIfNeeded` tick on the slot arm | `DirectGLES.cpp:1275`, `SlotTables.h:37-48` | scout trap 8 |
|
||
| the four PBO/XFB writeback shapes and their epoch bumps | `DirectGLES.cpp:9406-9468, 9832-9880, 7876-7906, 838-878, 918-985`; `Utils.cpp:2297-2354` | `PackedWordReadbackScenario` (`:148, 167, 194`), `XfbCaptureBufferReuseScenario` (`:182-283`), `AtomicCounterScenario` (`:172, 201, 242`), `BufferTextureScenario` (`:162, 238, 311`) |
|
||
|
||
### D-K — How the pull build stays inert (G1)
|
||
|
||
Every P3a edit outside `MG_Backend` is inside `#if MOBILEGL_PIPE_PUSH`, following P2's precedent exactly
|
||
(`PipeMutation.h:46-48`: counters are members of the owning container *"all under `MOBILEGL_PIPE_PUSH` so the pull
|
||
build's state objects do not change size (G1)"*). Specifically:
|
||
|
||
- `MGPipeResourceOps`, `MGPipeSetResourceOps`, `MGPipeGetResourceOps`, every new `MGPipeApply*` and the applier's new
|
||
records are inside `PipeApply.h`'s existing `#if MOBILEGL_PIPE_PUSH` (`:30`).
|
||
- The eight `BufferObject.cpp` dispatch branches are `#if MOBILEGL_PIPE_PUSH` guarded and fall through to the
|
||
unmodified `g_bufferBackendOps` call in a pull build.
|
||
- `CallClass::MapPersistentRoundtrips` is inside `PipeStats.h`'s existing push-only block (`:101-116`).
|
||
- `MG_Impl/Pipe/{ResourceTracker.h, VertexInputEmit.h}` compile to nothing in a pull build, like `Tracker.h`,
|
||
`CsoCache.h` and `SetHashSuppressor.h`.
|
||
- `MGPVertexAttribWire` / `MGPVertexBindingPointWire` are new **types**, not new members of an existing type, so they
|
||
add no size anywhere. `MGPVertexBuffers` growing 16 → 24 is a push-only payload; the pull build never instantiates it.
|
||
|
||
**[deviation] `PipeResource::m_backend`, `SetBackendResource`, `ReleaseBackend` and `BackendBufferResource` are NOT
|
||
deleted in P3a**, although `ARCHITECTURE.md:284` says *"`SetBackendResource` 删除(server 拥有资源表)"*. Deleting them
|
||
changes `sizeof(BufferObject)` and its symbol set in the **pull** build, which is a straight G1 break, and it would
|
||
also break the `MOBILEGL_PIPE_LEGACY_MEMOS` arm that `ARCHITECTURE.md:367` requires to stay alive through P3a/P4a.
|
||
**Under push they simply stop being written**: `SetBackendResource` is never called, `m_backend` stays null, and
|
||
`~BufferObject`'s `OnDestroy` branch (`BufferObject.cpp:40-42`) therefore never fires — the death crosses as
|
||
`ResourceDestroy` instead. Cost: one dead pointer per `BufferObject` in a push build. The deletion retires with the
|
||
pull path at **P13**; the integrator records that against `ARCHITECTURE.md:284`.
|
||
|
||
### D-L — The buffer death signal: `ResourceDestroy`, not a seventh raiser
|
||
|
||
`scout-pipe-and-tests.md` §1.5 leaves this open. Today six kinds raise `NotifyStateObjectDestroyed`
|
||
(`StateObjectDeathNotice.h:55`) — Framebuffer, ShaderCso, Renderbuffer, SamplerCso, Texture, VertexElementsCso — and
|
||
`Managers.cpp:212-215`'s `default:` arm says *"Buffer already has its own death signal
|
||
(`BufferBackendOps::OnDestroy`)"*.
|
||
|
||
**Decision: the buffer's death crosses as `ResourceDestroy` (`PipeCalls.def:77`), which is the catalogue call for it.
|
||
No seventh `NotifyStateObjectDestroyed` raiser is added.** `StateObjectDeathNotice.h` exists precisely for kinds that
|
||
have *no* such call (`:23-27`: it carries `{kind, lifetimeId}` and not the object, and one entry point serves all kinds
|
||
because the backend's answer is the same — free the slot, drop the twin). `SlotTables.h:38`'s *"All six re-keyed object
|
||
classes raise …"* therefore stays true, and package C updates `Managers.cpp:213-214`'s comment to
|
||
*"Buffer death crosses as ResourceDestroy (P3a)"*.
|
||
|
||
The **order** at `~BufferObject` is fixed and is not negotiable, because `MGPipeSlotAllocator::Free` erases its
|
||
`lifetimeId → slot` mapping (`SlotAllocator.h:60`) and a notice resolved twice finds nothing the second time
|
||
(`SlotTables.h:49-59`, `Managers.cpp:180-185`):
|
||
|
||
```
|
||
~BufferObject() -> MGPipeEmitResourceDestroy(handle) // applier clears Live, calls ops->Destroy(handle)
|
||
-> MGPipeSlots().Free(MGPipeKind::Buffer, handle)
|
||
```
|
||
|
||
The `Gen` bump happens on the **next handout** of the slot, not in `Free` (`SlotAllocator.h:60`), so a double free
|
||
cannot skip a generation. `HandleRecycleScenario` gains
|
||
`ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents`, shaped on the existing
|
||
`AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInput` (`:483`), with all three arms
|
||
(`.Handles` / `.Legacy` / `.AbaControl`).
|
||
|
||
### D-M — Subsystem bits, the A/B, and `MOBILEGL_PIPE_LEGACY_MEMOS`
|
||
|
||
`MGPipe.h:79`: *"bits 7..62 reserved for the later phases, allocated in ROADMAP order"* and never reused — an
|
||
operator's recorded `0x7f` has to keep meaning what it meant. P3a takes the next two:
|
||
|
||
```cpp
|
||
inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7; // P3a: the resource_* family
|
||
inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8; // P3a: vertex elements / buffers / index
|
||
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP3a = 0x1ffull; // bits 0..8
|
||
```
|
||
|
||
`ConfigLoader.cpp:245-256`'s push-build default moves from `kMGPipeSubsystemsMigratedAtP2` to
|
||
`kMGPipeSubsystemsMigratedAtP3a`; `Config.h:319-358`'s bit-list comment gains the two rows.
|
||
`MGPipeSubsystemForDirty` (`Tracker.h:119-135`) gains three arms — bit 5 → `kMGPipeSubsystemVertexInput`,
|
||
bit 9 → `kMGPipeSubsystemVertexInput`, bit 10 → `kMGPipeSubsystemVertexInput` — and `PipeFill.cpp`'s
|
||
`SubsystemForEmitter` gains the matching arms, **plus the pairing `static_assert`s** (`:645-663`), **plus**
|
||
`kMGPipeWiredSubsystems` (`:669-672`) gaining both bits. All four in one commit — see the merge trap in C.5.
|
||
|
||
`ARCHITECTURE.md:367` requires `MOBILEGL_PIPE_LEGACY_MEMOS` (default ON, tri-state) to keep the pre-handle arm alive
|
||
*"在 P3a/P4a 期间"*, because after phase C a cleared subsystem bit is no longer a true A/B on its own. P3a's legacy arm
|
||
is: the frontend dispatch falls through to `g_bufferBackendOps`; Espryt's `Ops_*` keep their `BufferObject&` bodies;
|
||
the VAO twin keeps `m_syncedConfigVersion` / `m_syncedAttributeVersions` / `m_syncedIndexBufferObject` and reads
|
||
`MGB_CTX->GetBoundVertexArray()`. Both arms compile in every push build and every `§C` verification runs the suite
|
||
twice, so the legacy arm cannot silently stop compiling or stop passing.
|
||
|
||
### D-N — What P3a does **not** do
|
||
|
||
Written down so nobody re-opens it mid-package:
|
||
|
||
- No client-side content-addressed vertex-elements cache, no 1024-entry LRU (D-G1 — P7).
|
||
- No `hasLiveHostWrites` producer; the field exists and stays false (D-A4 — P5).
|
||
- No `MGHostSpan`, no `SEG_*`, no chunked G3 path, no `kCapNeedsHostIndexBytes`, no index host mirror (P8).
|
||
- No move of `SyncPersistentMappedRange` / `SyncGpuWrites` off their 8 + 3 Espryt sites
|
||
(`DirectGLES.cpp:297, 4710, 4964, 4965, 5066, 5067`, `Managers.cpp:1700`, `MultiDraw.cpp:511`; and
|
||
`DirectGLES.cpp:4711`, `Managers.cpp:2774`, `MultiDraw.cpp:512`) — the per-site attribution table
|
||
(`ARCHITECTURE.md:235-241`) is P8's to execute, and `ROADMAP.md:88` forbids "fixing" the `*IndirectCount`
|
||
gap in flight.
|
||
- No Magma work of any kind. Magma's buffer path is P7 (`ROADMAP.md:24`); its VAO memo re-key landed in P2.
|
||
- Nothing added to `ResidualValueBlock`. `MGL_RESIDUAL_BLOCK_SIZE` is **8** (`MGPipeTypes.h:546`), it only ever goes
|
||
**down**, and growing it at all is a build break by design (`:535-539`). Anything P3a is not ready to carry stays in
|
||
the per-verb residual **fill loop** (`kMGPipeFieldEmittedBy` / `CopyField`), which is a different mechanism.
|
||
- No new namespace. Everything is `MobileGL::MG_Pipe` (`BRIEF-P2.md:456-458`), and
|
||
`python3 scripts/check_include_closure.py` runs after any file is added to `MG_Pipe/`; if the closure gate objects,
|
||
the file moves to `MG_Impl/Pipe/`.
|
||
- No `Tracker.cpp`. `Tracker.h` is header-only for an ownership reason (`Tracker.h:40-44`): the root `CMakeLists.txt`
|
||
that would name a new `.cpp` belongs to the contract package. P3a's new client files are headers for the same reason,
|
||
except those the contract package itself adds to `CMakeLists.txt`.
|
||
- No hot-path timer anywhere (`ROADMAP.md:7`, `Tracker.h:35-38`). Absolute ns comes from DriverBench, from outside the
|
||
library.
|
||
|
||
---
|
||
|
||
## C. Packages
|
||
|
||
**Four packages on four branches, four WSL worktrees.** The split is chosen by the code: the payloads, the applier, the
|
||
op table and the generated interfaces are one shared contract everything else compiles against; the client emitters and
|
||
the tracker touch no backend file; Espryt is one backend directory; the gates, scenarios, CI file and measurement
|
||
plumbing are cross-cutting tooling that must be able to land before the thing it gates.
|
||
|
||
**Branch points.** `p3a/contract` from `feat/disaggregated@55d2af9b`. Its single commit `c0` is the contract;
|
||
`p3a/{client, espryt, gates}` all branch from the **tag** `p3a/contract`, so their push builds compile and link while A
|
||
finishes. `p3a/wire` continues on from `c0`.
|
||
|
||
**Tree creation** (`~/w7/notes/tools/wsl_p2_tree.sh` → `wsl_p3a_tree.sh`, `p2`→`p3a` throughout):
|
||
|
||
```sh
|
||
bash ~/w7/wsl_p3a_tree.sh contract 55d2af9b verify # first, and it must finish before the rest
|
||
bash ~/w7/wsl_p3a_tree.sh wire p3a/contract verify
|
||
bash ~/w7/wsl_p3a_tree.sh client p3a/contract verify
|
||
bash ~/w7/wsl_p3a_tree.sh espryt p3a/contract
|
||
bash ~/w7/wsl_p3a_tree.sh gates p3a/contract verify
|
||
```
|
||
|
||
Each tree gets the same three build directories, flags identical to `~/w7/pipe/build-linux` (the `COMMON` line of
|
||
`wsl_p2_gate.sh:10`):
|
||
|
||
```sh
|
||
# pull (the G1 build)
|
||
cmake -S . -B build-linux -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
|
||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_BUILD_TYPE=Release \
|
||
-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_INTEGRATION_TEST=ON \
|
||
-DMOBILEGL_BUILD_BENCHMARK=OFF -DMOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/50_mesa.json \
|
||
-DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||
# push
|
||
cmake -S . -B build-push ... same ... -DMOBILEGL_PIPE_PUSH=ON
|
||
# verify
|
||
cmake -S . -B build-verify ... same ... -DMOBILEGL_PIPE_VERIFY=ON -DMOBILEGL_ITEST_REQUIRE_GPU=ON
|
||
# benchmark (package D and the integrator only; DriverBench is Linux-desktop only, CMakeLists.txt:33-35
|
||
# force-disables the whole benchmark tree on Android)
|
||
cmake -S . -B build-bench ... same ... -DMOBILEGL_BUILD_BENCHMARK=ON
|
||
```
|
||
|
||
`export CCACHE_BASEDIR=/home/swung/w7` in every shell.
|
||
|
||
### C.0 Package A — `p3a/contract` then `p3a/wire` (trees `~/w7/p3a-contract`, `~/w7/p3a-wire`)
|
||
|
||
**Commit `c0` — the contract. Nothing in it may change after the tag without telling the other three packages.**
|
||
|
||
| action | file | what |
|
||
|---|---|---|
|
||
| MODIFY | `MobileGL/MG_Pipe/MGPipeValueTypes.h` | D-G2 — `MGPVertexAttribWire` (24 B) and `MGPVertexBindingPointWire` (16 B), placed next to `VertexAttribute` / `VertexBufferBindingPoint` (`:491-538`) with the trip-wire size asserts in the `:547-...` block |
|
||
| MODIFY | `MobileGL/MG_Pipe/MGPipeTypes.h` | D-H1 — `MGPVertexBuffers` gains `Uint32 BaseInstance; Uint32 Pad0;`, `MGP_ASSERT_POD` 16 → **24**, with the comment naming the `ContentHash` requirement and the `MGPDrawInfo::StartInstance` distinction; D-A5 — `MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc&)` next to `MGPipeSetSubDataBufferRange` (`:601-617`) |
|
||
| MODIFY | `MobileGL/MG_Pipe/PipeCalls.def` | D-A5 — `ResourceRespecify` (`:76`) flags `kNone` → `kNeedsAck`; the legend at `:18` gains *"`kNeedsAck` on a call means records of this call MAY require an ack; a per-record predicate decides"*. **No row is added, moved or reordered**; `MGP_CALL_LIST_DOCUMENTED_COUNT` stays 71 |
|
||
| MODIFY | `MobileGL/MG_Pipe/PipeFields.def` | `MGP_FIELDS_MGPVertexBuffers` (`:99-100`) gains `F(BaseInstance)`; new `MGP_FIELDS_MGPVertexAttribWire` and `MGP_FIELDS_MGPVertexBindingPointWire`; both appended to `MGP_VERIFY_PAYLOAD_LIST` (`:300-315`) — **`gen_pipe.py` reads that list to know what to emit** |
|
||
| MODIFY | `MobileGL/MG_Pipe/MGPipe.h` | D-M — `kMGPipeSubsystemResources` (bit 7), `kMGPipeSubsystemVertexInput` (bit 8), `kMGPipeSubsystemsMigratedAtP3a = 0x1ff` |
|
||
| MODIFY | `MobileGL/MG_Pipe/PipeApply.{h,cpp}` | D-A1 — `MGPipeResourceOps` + `MGPipeSetResourceOps`/`MGPipeGetResourceOps`; D-G4 — `MGPipeResourceRecord`, `MGPipeVertexElementsRecord` and the ten new `MGPipeApplierState` members; the **nine new apply entry points** with complete signatures and **stub bodies** so C and D link: `MGPipeApplyResourceCreate`, `…ResourceRespecify`, `…ResourceSubData`, `…BufferSubDataResident`, `…ResourceFlushRange`, `…ResourceReadback`, `…ResourceDestroy`, `…MapPersistent`, `…UnmapPersistent`, plus `…CreateVertexElements`, `…BindVertexElements`, `…DeleteVertexElements`, `…SetVertexBuffers`, `…SetIndexBuffer`. `MGPipeApplierReset()` (`:93`) clears all of them |
|
||
| MODIFY | `MobileGL/MG_Pipe/Coverage.def` | the **complete** P3a rows: `MGP_COVERAGE_EMITTED_LIST` gains the vertex-input fields; `GetBufferBindingSlot`'s accessor row (`:42`) splits per target now that the target argument is known (`:37-41` says the split waits on the re-vendored inventory — **P3a supplies the target from the emission site instead, and the comment is rewritten to say so**); the `Buffer ops delta → ResourceRespecify` delta row (`:130`) is left alone |
|
||
| MODIFY | `MobileGL/MG_Impl/Pipe/PipeFill.cpp` | **contract-commit only**: the generated-enum-coupled block — `SubsystemForEmitter`'s new arms (`:624-638`), the new pairing `static_assert`s (`:645-663`), `kMGPipeWiredSubsystems` (`:669-672`) — plus **stub emitter functions that emit nothing**. See the merge trap in C.5 |
|
||
| MODIFY | `scripts/gen_pipe.py` | whatever the two new payloads and the flag-word change require; regenerate |
|
||
| MODIFY | `MobileGL/MG_Pipe/generated/*.inc` | regenerate and commit (all eight; the build never depends on python, `ARCHITECTURE.md:424`) |
|
||
| MODIFY | `MobileGL/MG_Backend/DirectGLES/SlotTables.h` | D-A4 — `BackendSlotTable::GetOrCreate(MGPipeHandle)` / `FindByHandle` overloads that never call `MGPipeSlots()`, so the client can mint. **Header-only, both arms compile, nothing uses the new one yet.** [This is the one `MG_Backend` file A touches; C owns the rest of the directory and branches from the tag.] |
|
||
| MODIFY | `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | D-B2 — `CallClass::MapPersistentRoundtrips` **inside the existing `#if MOBILEGL_PIPE_PUSH` block** (`:101-116`), name `"map-persistent-roundtrips"`, short name `mpr` in `FormatWindowLine` |
|
||
| MODIFY | `MobileGL/MG_Test/Util/PipeStatsTest.cpp` | pin `mpr` in `CounterNamesAreStable` (`:260`) and `SummaryLineCarriesEveryClassAndGate` (`:137`) |
|
||
| MODIFY | `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` | `MGPVertexBuffers` size 16 → 24; the new `ResourceRespecifyAcksOnlyImmutableStorage` case (D-A5) |
|
||
| MODIFY | root `CMakeLists.txt`, `MobileGL/Config.h`, `MobileGL/ConfigLoader.cpp` | the new client sources inside `if (MOBILEGL_PIPE_PUSH)` (`:462-468`); the push default `0x7f` → `0x1ff`; the bit-list comment |
|
||
| CREATE (stubs) | `MobileGL/MG_Test/Pipe/{ResourceEmitTest, VertexInputEmitTest}.cpp` | each a compiling file with one placeholder `TEST`, so the package that owns their contents never edits `MG_Test/Pipe/CMakeLists.txt` |
|
||
| MODIFY | `MobileGL/MG_Test/Pipe/CMakeLists.txt` | register the two stubs, `LABELS unit` |
|
||
|
||
**Ordered steps for `c0`**: value types and payloads first (build pull; confirm `symbol_report --threshold 0` is
|
||
clean and the two new `MGP_ASSERT_POD`s hold) → `PipeCalls.def` flag word + `PipeFields.def` + generator + regenerate +
|
||
`git diff --exit-code -- MobileGL/MG_Pipe/generated` → `MGPipe.h` / `Config` / `CMakeLists` → `PipeApply.{h,cpp}` with
|
||
complete signatures and stub bodies → `PipeFill.cpp`'s enum-coupled block with stub emitters → `SlotTables.h`
|
||
overloads → `PipeStats` + the three tests. **Then tag `p3a/contract` and tell the other three packages.**
|
||
|
||
Message: `[Feat] (Pipe): land the P3a contract - the handle-shaped resource op table, the applier's resource and vertex-elements records, the two vertex wire views, an explicit baseInstance on set_vertex_buffers, and the first kNeedsAck`
|
||
|
||
**Then `p3a/wire` continues** with the work only A can do:
|
||
|
||
- `c1`: the nine `resource_*` apply entry points in full — the record updates, the `Serial` bumps, the bounds gate that
|
||
refuses a blob outside its declared segment (`Fatal{ProtocolCorruption}`, `ARCHITECTURE.md:119`), and the dispatch
|
||
into `MGPipeResourceOps`. Message:
|
||
`[Feat] (Pipe): apply the resource calls into a per-context slot-indexed record and dispatch them to the backend by handle`
|
||
- `c2`: the five vertex-input apply entry points — `CreateVertexElements`'s blob unpack with its count validation,
|
||
`BindVertexElements`, `DeleteVertexElements`, `SetVertexBuffers` (including `VertexFetchBaseInstance`) and
|
||
`SetIndexBuffer`, each bumping its `Serial`. Message:
|
||
`[Feat] (Pipe): apply vertex elements, vertex buffers and the index buffer into the server's own working state`
|
||
- `c3`: `MG_Test/Pipe/ResourceEmitTest.cpp` in full — the applier's record lifecycle: a create marks the slot live; a
|
||
respecify replaces the descriptor and bumps `Serial`; a destroy clears Live and a stale handle's `Gen` compare fails;
|
||
`HasLiveHostWrites` is false for every path P3a has; `MGPipeSetSubDataBufferRange` returns false exactly at the
|
||
2^31-1 / 2^32-1 bounds and the emitter's splitter produces contiguous, non-overlapping records that reassemble to the
|
||
original range. Message:
|
||
`[Test] (Pipe): pin the resource record's lifecycle and the buffer sub-data range encoding at both of its bounds`
|
||
|
||
**Verification (`~/w7/p3a-wire`)**
|
||
|
||
```sh
|
||
python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test
|
||
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
|
||
cmake --build build-linux --parallel 28 && ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
|
||
python3 scripts/symbol_report.py --before ~/w7/p3a-before-libMobileGL.so --after build-linux/libMobileGL.so \
|
||
--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0
|
||
# expect added 0 / removed 0 / renamed 0 / resized 0 - P3a's admitted set is EMPTY (G1)
|
||
cmake --build build-push --parallel 28 && ctest --test-dir build-push -L unit --no-tests=error --output-on-failure
|
||
cmake --build build-verify --parallel 28 && ctest --test-dir build-verify -L unit --no-tests=error
|
||
bash scripts/p3a_untouched_regions.sh 55d2af9b HEAD # G5: nine functions unmoved
|
||
```
|
||
|
||
### C.1 Package B — `p3a/client` (tree `~/w7/p3a-client`, from `p3a/contract`)
|
||
|
||
**Files**
|
||
|
||
| action | file | what |
|
||
|---|---|---|
|
||
| CREATE | `MobileGL/MG_Impl/Pipe/ResourceTracker.h` | D-A3's sticky `BindMask` table and its `static_assert` over `BufferTarget`; the `lifetimeId → slot` mint and the `slot → BufferObject*` inverse (D-D); the nine `MGPipeEmitResource*` helpers; the `MGPSubData` range splitter; the `mpr` counting site |
|
||
| CREATE | `MobileGL/MG_Impl/Pipe/VertexInputEmit.h` | the `VertexAttribute`/`VertexBufferBindingPoint` → wire conversion (D-G2), the per-handle `m_lastEmittedConfigVersion` table (D-G3), the `MGPVertexBuffer[]` build (D-H3), the `ContentHash` including `BaseInstance` (D-H2.3), and `MGPipeEmit{VertexElements,VertexBuffers,IndexBuffer}` |
|
||
| MODIFY | `MobileGL/MG_Impl/Pipe/Tracker.h` | D-M — three `MGPipeSubsystemForDirty` arms; D-I — bit 10's narrowed shutter; the comment at `:64-78` corrected (bits 9/10 are **P3a**, not P3b/P4b) and `:245-247`'s "P3b narrows it" note replaced by what P3a did |
|
||
| MODIFY | `MobileGL/MG_Impl/Pipe/PipeFill.{h,cpp}` | the three emitter bodies replacing A's stubs, in `ARCHITECTURE.md:194`'s order after the four P2 emitters (`:1117-1128`); `MGPipeSetPendingBaseInstance` + its reset in `MGPipeLeaveVerb` (`:607`); `EmittedCallSuppliesTheWholeField` (`:693+`) extended |
|
||
| MODIFY | `MobileGL/MG_Impl/Pipe/SetHashSuppressor.h` | comment only: slot `SetVertexBuffers` (`:37`) is marked wired at P3a instead of P3b |
|
||
| MODIFY | `MobileGL/MG_State/GLState/BufferState/BufferObject.{h,cpp}` | D-A1's eight `#if MOBILEGL_PIPE_PUSH` dispatch branches at `:39-43, 45, 53, 63, 73, 186, 291, 319, 371, 493`; `ResourceCreate` from the constructor (`:35-37`); `ResourceDestroy` + `MGPipeSlots().Free` from `~BufferObject` in that order (D-L) |
|
||
| MODIFY | `MobileGL/MG_State/GLState/BufferState/BufferState.{h,cpp}` | the sticky `everBoundAs` mask's bump points at the buffer bind entry points (D-A3), all `#if MOBILEGL_PIPE_PUSH` |
|
||
| MODIFY | `MobileGL/MG_Pipe/DirtySurface.def` | rows for anything the widened scan root (G9) newly sees; `MarkBufferObjectForDeletion → kExplicitDestroy` (`:252`) and `MarkVertexArrayForDeletion → kExplicitDestroy` (`:260`) are now **true** rather than aspirational, and their comments say which call publishes them |
|
||
| MODIFY | `scripts/gen_pipe_dirty_surface.py` | G9 — widen the scan root from `MG_Impl/GLImpl` to `MG_Impl/GLImpl` + `MG_State/GLState`; keep `--check` failing in both directions and `--self-test`'s two canned negative controls |
|
||
| MODIFY | `MobileGL/MG_Pipe/FillPoints.def` | discharge the item `:60-64` records as P3a's: run `MOBILEGL_PIPE_POISON_OMIT` across the full CTS caselist on both devices, then either retire the class-field-mask rows the omission proves dead or record per row why it stays. **Off the critical path — the verdict lands with G15, and the def's comment is updated either way.** |
|
||
| MODIFY | `MobileGL/MG_Test/Pipe/{ResourceEmitTest, VertexInputEmitTest}.cpp` | contents where A's `c3` did not already cover them (the stubs and the CMake registration are A's) |
|
||
|
||
**Ordered steps**
|
||
|
||
1. `b1`: `ResourceTracker.h` + the `BindMask` table + the `BufferObject` dispatch branches, **emitting into the applier
|
||
but with `MGPipeGetResourceOps() == nullptr`**, so every dispatch still falls through to `g_bufferBackendOps` and
|
||
the tree behaves exactly as before. This step is the safety net: it proves the emission is semantically free before
|
||
anything is switched over. Message:
|
||
`[Feat] (Pipe, State): mint a {slot, gen} handle for every buffer object and publish its create, respecify, sub-data, flush, readback and destroy as pipe calls`
|
||
2. `b2`: `VertexInputEmit.h` + the three emitters + `MGPipeSetPendingBaseInstance` + the tracker's three subsystem arms
|
||
and bit 10's narrowed shutter. Still nothing consumes them. Message:
|
||
`[Feat] (Pipe): push the bound VAO's format, its vertex buffers with an explicit baseInstance, and its index binding as their own calls`
|
||
3. `b3`: `gen_pipe_dirty_surface.py`'s widened scan root and the `DirtySurface.def` rows it produces. Message:
|
||
`[Feat] (Pipe): widen the dirty-surface scan to MG_State so the MGP_NOTE_MUTATION sites stop being outside the gate`
|
||
4. `b4`: the two unit-test files.
|
||
|
||
**Tests to add**
|
||
|
||
- `ResourceEmitTest.EveryBufferTargetSetsItsBindMaskBit` — D-A3, on create **and** on a following respecify.
|
||
- `ResourceEmitTest.ABindMaskBitIsStickyAcrossARespecifyThatDoesNotRebind`.
|
||
- `ResourceEmitTest.ADestroyedBufferReleasesItsSlotAndAStaleHandleResolvesToNothing` — D-L's order.
|
||
- `ResourceEmitTest.AWholeBufferSubDataBeyondTheRecordBoundIsSplitIntoContiguousRecords`.
|
||
- `VertexInputEmitTest.EveryAttributeFieldSurvivesTheWireConversion` — **G6**: all 32 slots, every member of
|
||
`VertexAttribute` that the wire form carries, driven through the format entry points, the binding-model entry points
|
||
and the legacy pointer entry points.
|
||
- `VertexInputEmitTest.ABindingModelStrideOfZeroSurvivesAsZero` — the
|
||
`KHR-GL43.vertex_attrib_binding.basic-input-case7/8` regression (`MGPipeValueTypes.h:496-503`).
|
||
- `VertexInputEmitTest.IsLongAndFloat64TravelSeparately` — `ARCHITECTURE.md:130`, and the input to D-J's workaround 1.
|
||
- `VertexInputEmitTest.ABaseInstanceChangeAloneStillEmitsTheVertexBufferSet` and
|
||
`…AnUnchangedSetWithAnUnchangedBaseInstanceEmitsNothing` — D-H2.3, the suppressor trap.
|
||
- `VertexInputEmitTest.RebindingTheSameVaoEmitsABindAndNoCreate` and
|
||
`…PingPongingBetweenTwoVaosNeverRecreatesEither` — D-G3's per-handle latch.
|
||
- `TrackerTest.TheIndexBufferBitDoesNotFireOnAnUnrelatedBufferWrite` — **the narrowing of D-I, and the one case the old
|
||
shutter could not pass.**
|
||
- `TrackerTest.TheIndexBufferBitFiresWhenTheSlotVersionWrapsOntoADifferentBuffer` — the wrap hole identity closes.
|
||
|
||
**Constraints**: B touches **no** file under `MG_Backend/`. B does not edit
|
||
`MG_Pipe/{MGPipeValueTypes.h, MGPipeTypes.h, MGPipe.h, PipeCalls.def, PipeFields.def, Coverage.def, PipeApply.*,
|
||
generated/*.inc}` or `scripts/gen_pipe.py` (A owns them), and does not edit `.github/workflows/test.yml` or
|
||
`MG_IntegrationTest/**` (D owns them) — so B's scanner change must keep `--summary` working until D lands.
|
||
|
||
**Verification (`~/w7/p3a-client`)**
|
||
|
||
```sh
|
||
python3 scripts/gen_pipe_dirty_surface.py --check && python3 scripts/gen_pipe_dirty_surface.py --self-test
|
||
python3 scripts/gen_pipe.py --check
|
||
cmake --build build-linux --parallel 28 && python3 scripts/symbol_report.py \
|
||
--before ~/w7/p3a-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change
|
||
cmake --build build-push --parallel 28 && ctest --test-dir build-push -L unit --no-tests=error --output-on-failure
|
||
ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8
|
||
MOBILEGL_PIPE_PUSH=0 ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8 # all-pull arm
|
||
MOBILEGL_PIPE_PUSH=0x7f ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8 # P3a subsystems off (G12)
|
||
cmake --build build-verify --parallel 28 && ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4
|
||
MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py --tree ~/w7/p3a-client --lib build-verify/libMobileGL.so \
|
||
--out ~/w7/retrace-out/p3a-client -j 4
|
||
grep -l 'Fatal{' ~/w7/retrace-out/p3a-client/*.log # empty
|
||
grep -L 'MGPipe verify:' ~/w7/retrace-out/p3a-client/*.log # empty: every case armed
|
||
```
|
||
|
||
### C.2 Package C — `p3a/espryt` (tree `~/w7/p3a-espryt`, from `p3a/contract`)
|
||
|
||
**Files**: `MobileGL/MG_Backend/DirectGLES/{DirectGLES.cpp, Managers.h, Managers.cpp, MultiDraw.cpp, SlotTables.h,
|
||
Utils.cpp}` and `MobileGL/MG_Test/SanityTest.cpp`. **Nothing else.**
|
||
|
||
**Ordered steps**
|
||
|
||
1. `e1`: the seventh slot table — `BufferImpl::g_backendBufferResources` of kind `MGPipeKind::Buffer`, holding
|
||
`GLESBufferResource`, keyed by the client-minted handle through A's `GetOrCreate(MGPipeHandle)` overload
|
||
(D-A4). `EnsureProcessTeardownSentinel()` arming moves onto its first insertion (`Managers.h:52-60`,
|
||
`Managers.cpp:161-170` — a destructor hook is wrong and `:52-57` says why). Both arms compile; nothing uses the new
|
||
one yet. Message:
|
||
`[Refactor] (Espryt): give the buffer resource its own {slot, gen} table instead of hanging it off the frontend object`
|
||
2. `e2`: the nine `MGPipeResourceOps` implementations and `MGPipeSetResourceOps` at bring-up
|
||
(`BackendObject_DirectGLES.cpp:849` and `DirectGLES.cpp:10402`, beside `RegisterBufferBackendOps`), cleared at
|
||
shutdown beside `UnregisterBufferBackendOps` (`Managers.cpp:1512`). Each body is its `Ops_*` counterpart with the
|
||
substitutions D-A2's table names and **nothing else**; the seven `*Tracked` wrappers and their epoch bumps
|
||
(`:1449-1480`) are duplicated for the new table, including `AcquirePersistentMap`'s bump-even-on-decline. The old
|
||
table and its bodies stay compiled under `MOBILEGL_PIPE_LEGACY_MEMOS`. **This is the commit that switches the buffer
|
||
path over.** Message:
|
||
`[Refactor] (Espryt): take the buffer ops by handle and payload instead of by frontend object reference`
|
||
3. `e3`: the reverse channel — `OnBufferWriteback` in `Ops_Readback` (D-D) replacing
|
||
`bufferObject.WritebackFromBackend` at `Managers.cpp:1410`, in the fixed order writeback → unmap → serial stamp →
|
||
`BumpBufferMutationEpoch()`; `OnGpuWritten` replacing the three `MarkGpuWritten` sites
|
||
(`DirectGLES.cpp:500, 544, 2012`). Message:
|
||
`[Refactor] (Espryt): answer a readback through the reverse channel and bump the mutation epoch after it, never before`
|
||
4. `e4`: the VAO half — `SyncToBackend`'s gate re-keyed onto the applier's three `Serial`s (D-G4's table), the
|
||
per-attribute walk reading `rec.Attributes[]` / `st.VertexBuffers[]`, `BindAttributeBuffer` resolving the driver id
|
||
from the slot table, `ResolvedDrawBuffers` re-keyed onto handles, and the deletion of
|
||
`g_pendingFetchBaseInstance` / `SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` /
|
||
`ScopedFetchBaseInstance` and its three scopes (`DirectGLES.cpp:5135, 5172, 5224`) in favour of
|
||
`MGPipeApplierState::VertexFetchBaseInstance` (D-H2). `m_syncedConfigVersion`, `m_syncedAttributeVersions`,
|
||
`m_syncedIndexBufferVersion` and `m_syncedIndexBufferObject` are deleted; `m_syncedBufferIdGeneration`,
|
||
`m_hasConvertedFloat64Attribute` and `m_syncedFetchBaseInstance` stay. Every one of them stays compiled under
|
||
`MOBILEGL_PIPE_LEGACY_MEMOS`. Message:
|
||
`[Refactor] (Espryt): drive the driver VAO from the pushed vertex-elements record and retire the wrapping index-slot version and its identity patch`
|
||
5. `e5`: `ConvertedFloat64Stream`'s re-key from `sourceLifetimeId` to the buffer handle (`Managers.h:901-909`,
|
||
`Managers.cpp:2812-2820`) — `ARCHITECTURE.md:363`'s *"`ConvertedVertexStreamKey` 的 `sourcePin`"* deletion — plus
|
||
`Managers.cpp:213-214`'s comment update (D-L). Message:
|
||
`[Refactor] (Espryt): key the narrowed fp64 vertex stream on the buffer's handle instead of its lifetime id`
|
||
6. `e6`: `SanityTest.cpp` — extend the `DirectGLESSlotTable` suite (`:3311-4044`) to the buffer kind:
|
||
`EveryReKeyedObjectClassAnnouncesItsOwnDeath` (`:3706`) gains the buffer arm,
|
||
`EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm` (`:3857`) likewise, and
|
||
`PixelPackBindingCacheSkipsRedundantBindsAndRestsAtZero` (`:2535`) keeps passing unchanged.
|
||
|
||
**Must not break** — D-J's table, every row with its named test. In addition: `IsBufferDrawClean`'s five reads become
|
||
four applier reads with **identical** semantics (D-A4); `SyncNeccessaryBuffers`' memo hit/miss/repair structure
|
||
(`DirectGLES.cpp:601-706`) is unchanged in shape; the four `EnsureBufferResource` call sites in `Managers.cpp`
|
||
(`:2337, 2393, 2622, 4724`) and the twelve in `DirectGLES.cpp` (`:411, 463, 526, 560, 612, 645, 681, 4185, 4247, 4631,
|
||
9411, 9837`) all keep their behaviour, taking a handle where they took a `SharedPtr`.
|
||
|
||
**Verification (`~/w7/p3a-espryt`)**
|
||
|
||
```sh
|
||
cmake --build build-linux --parallel 28 && python3 scripts/symbol_report.py \
|
||
--before ~/w7/p3a-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change
|
||
grep -rc 'pGLContext' MobileGL/MG_Backend | grep -v ':0$' # empty (G13)
|
||
grep -n 'MG_State' MobileGL/MG_Pipe/PipeApply.h # no hit inside the MGPipeResourceOps block (G13)
|
||
bash scripts/p3a_untouched_regions.sh 55d2af9b HEAD # G5: the nine functions are byte-identical
|
||
grep -n 'g_pendingFetchBaseInstance\|ScopedFetchBaseInstance' MobileGL/MG_Backend/DirectGLES/*.{h,cpp} # empty
|
||
grep -n 'm_syncedConfigVersion\|m_syncedAttributeVersions\|m_syncedIndexBufferObject' \
|
||
MobileGL/MG_Backend/DirectGLES/Managers.h
|
||
# every remaining hit must be inside a MOBILEGL_PIPE_LEGACY_MEMOS arm
|
||
cmake --build build-push --parallel 28
|
||
ctest --test-dir build-push -L unit --no-tests=error --output-on-failure
|
||
ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8 -R DirectGLES
|
||
MOBILEGL_PIPE_PUSH=0 ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8 -R DirectGLES # legacy arm
|
||
MOBILEGL_PIPE_PUSH=0x7f ctest --test-dir build-push -L integration-gpu --no-tests=error -j 8 -R DirectGLES # G12
|
||
ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4 \
|
||
-R 'LargeArenaAdoption|StorageBufferRegrow|VertexAttribBinding|MultiDraw|PrimitiveRestart|CrossFrameBuffer|ResidentIndex|BufferTexture|AtomicCounter|XfbCaptureBufferReuse|PackedWordReadback|DoublePrecision|VertexArrayEnableDisable|DrawParameters'
|
||
python3 ~/w7/retrace_gate.py --tree ~/w7/p3a-espryt --lib build-push/libMobileGL.so --out ~/w7/retrace-out/p3a-espryt -j 4
|
||
```
|
||
|
||
### C.3 Package D — `p3a/gates` (tree `~/w7/p3a-gates`, from `p3a/contract`)
|
||
|
||
Cross-cutting tooling. D branches from the contract and integrates **last**, but writes the extended
|
||
`HandleRecycleScenario` arms **first**, so the buffer arm's `AbaControl` red is proven before C exists — that is the
|
||
"重键前红" evidence, recorded with its run log.
|
||
|
||
| action | file | what |
|
||
|---|---|---|
|
||
| MODIFY | `MobileGL/MG_IntegrationTest/Scenarios/HandleRecycleScenario.cpp` | `ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents`, shaped on the VAO case at `:483`, in all three arms (`.Handles` / `.Legacy` / `.AbaControl`); the VAO case extended to cover the vertex-input path (a recycled VAO must not inherit a predecessor's `set_vertex_buffers`) |
|
||
| MODIFY | `MobileGL/MG_IntegrationTest/Scenarios/StorageBufferRegrowScenario.cpp` | **G10** — N storage definitions of an adopted store publish exactly N `map-persistent-roundtrips` and **not** one per draw; `AGrownStoreIsVisibleThroughItsExistingIndexedBinding` (`:124`) is unchanged and keeps guarding `InvalidateIndexedBufferBindingShadowsForId` |
|
||
| MODIFY | `MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp` | its three cases (`:204, 227, 245`) run in both arms and additionally assert `mpr` moves exactly once per adoption |
|
||
| CREATE | `MobileGL/MG_IntegrationTest/Scenarios/ResourceSubsystemControlScenario.cpp` | **G12** — two entries, `MOBILEGL_PIPE_PUSH` with and without bits 7|8, asserting the emission counters move in the expected direction and the pixels do not. A dead switch must not pass silently |
|
||
| MODIFY | `MobileGL/MG_IntegrationTest/CMakeLists.txt` | register the new scenario and its environments through `mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})` (`:311, 464-471`) — **ctest `ENVIRONMENT` replaces rather than appends, `;` must be escaped, and the property overrides the job env** (`ARCHITECTURE.md:566`); follow the SKIP-warning precedent at `:418, 431, 445, 456, 459` so a configuration that cannot run an arm says so |
|
||
| CREATE | `scripts/p3a_untouched_regions.sh` | **G5** — takes two refs, extracts the nine named functions from `Managers.cpp` by brace matching, `sha256sum`s them, diffs. Non-zero rc names the first function that moved. `--self-test` proves it can go red by perturbing one extracted body in a temp copy |
|
||
| CREATE | `scripts/p3a_vertex_input_negative_control.sh` | **G7** — patches `VertexInputEmit.h` to stop copying `IsBgra`, rebuilds, expects `ctest -R 'VertexInputEmit\.'` to fail naming `IsBgra`, reverts and rebuilds. Exit 0 = tripped and named; 1 = did not answer (green on a dropped field, or red without naming it — both are findings about the test, and both leave the tree restored and rebuilt); 2 = could not run |
|
||
| MODIFY | `.github/workflows/test.yml` | `pipe-gates` (`:1538`) gains `p3a_untouched_regions.sh $BASELINE HEAD` and its `--self-test`; the two new scenario entries; the `include-graph-check` job (`:738`) is unchanged |
|
||
| CREATE | `~/w7/notes/tools/wsl_p3a_gate.sh` | D.3 — `wsl_p2_gate.sh` adapted; see D.3 for the exact diff |
|
||
| MODIFY | `~/w7/notes/tools/p2_ab.sh` → `p3a_ab.sh` | D.4 — the four default cases replaced by P3a's named set; everything else (reboot-clean, the three-try pin with `check` after each and exit 43, the `mkdir` device-lock mutex with its 900 s timeout and exit 42, the `trap … EXIT` unpin, `--benchmark-repeats 3 --benchmark-tail-frames 200`, the `AB_DONE` terminator) verbatim |
|
||
|
||
Messages: `[Test] (Pipe): reproduce the buffer handle ABA through public GL and prove the pre-rekey guards are what stops it`;
|
||
`[Test] (Pipe): assert a regrown adopted store costs one map-persistent round trip per storage definition, not one per draw`;
|
||
`[CI] (Pipe): gate that the buffer pool, the deferred-release drain and the three rings did not move`
|
||
|
||
**Verification (`~/w7/p3a-gates`)**
|
||
|
||
```sh
|
||
cmake --build build-verify --parallel 28
|
||
ctest --test-dir build-verify -R HandleRecycle --no-tests=error --output-on-failure
|
||
# on the contract tree, before C lands: .Legacy and .AbaControl pass, .Handles SKIPs with
|
||
# "subsystem not implemented on this tree" - a visible skip, never a vanishing test
|
||
bash scripts/p3a_untouched_regions.sh --self-test
|
||
bash scripts/p3a_vertex_input_negative_control.sh build-push # must report "tripped, named IsBgra"
|
||
cmake --build build-bench --parallel 28 && ctest --test-dir build-bench -L benchmark --no-tests=error
|
||
```
|
||
|
||
### C.4 What each package may assume of the others
|
||
|
||
- **B assumes** A's `MGPipeApply*` signatures, `MGPipeResourceOps`' shape, the two wire structs, `MGPVertexBuffers`'
|
||
24-byte layout, the two subsystem bits and `MGPipeResourceRespecifyNeedsAck`. It assumes **nothing registers**
|
||
`MGPipeResourceOps`, so every dispatch falls through to `g_bufferBackendOps` and the tree is behaviourally
|
||
unchanged. That is what makes B landable on its own.
|
||
- **C assumes** the same contract plus A's `BackendSlotTable::GetOrCreate(MGPipeHandle)` overload. It assumes **the
|
||
applier's records are populated** — which is only true after B lands, hence the integration order in D.1. C is
|
||
written against the contract and rebased onto B.
|
||
- **D assumes** both, and its `HandleRecycleScenario` buffer arm is written and proven red on the contract tree first.
|
||
|
||
### C.5 File-ownership table
|
||
|
||
No two packages edit the same file **after the tag**. The contract is one commit that all three other branches descend
|
||
from, so the integrator's rebase never sees an edit on both sides of a file.
|
||
|
||
**Generated and `.def` interfaces are owned artefacts and are listed as such**, because of the semantic-merge trap
|
||
INTEGRATOR-DECISIONS records and `Coverage.def:484-494` documents from commit `bb2a236d`: *`Coverage.def`'s emitted
|
||
list is generated-enum input.* The spans work (`d1a7c5f1`) removed `GetPixelStoreParameters` from the emitted list, so
|
||
the generated `MGPipeFieldEmitter` **no longer had a `SetPixelPackState` enumerator** — but `PipeFill.cpp`'s
|
||
`SubsystemForEmitter` switch and its `static_assert`, written against the contract, still named it, and **the push and
|
||
verify builds did not compile on the integrated tree**. Green on two branches separately is not green on the merge.
|
||
|
||
**The rule this imposes on P3a**: *a package that retires a `.def` row must not leave another package referencing the
|
||
generated enumerator.* It is discharged structurally — **package A owns `Coverage.def` AND, in the contract commit
|
||
only, the enum-coupled block of `PipeFill.cpp` (`SubsystemForEmitter`, its `static_assert`s, `kMGPipeWiredSubsystems`).
|
||
Package B owns `PipeFill.cpp` from the tag onward.** A's edit is already in B's base, so the two can never diverge, and
|
||
a mistake is a compile error at `c0`, not at the merge.
|
||
|
||
| file / glob | A contract+wire | B client | C espryt | D gates |
|
||
|---|---|---|---|---|
|
||
| `MobileGL/MG_Pipe/{MGPipeValueTypes.h, MGPipeTypes.h, MGPipe.h, PipeApply.{h,cpp}}` | **owner** | – | – | – |
|
||
| `MobileGL/MG_Pipe/PipeCalls.def` *(generated interface: opcodes + flag words → `PipeTables.inc`, `PipeThunks.inc`, `PipeWire.inc`)* | **owner** | – | – | – |
|
||
| `MobileGL/MG_Pipe/PipeFields.def` *(generated interface: field lists + `MGP_VERIFY_PAYLOAD_LIST` → `PipeVerify.inc`)* | **owner** | – | – | – |
|
||
| `MobileGL/MG_Pipe/Coverage.def` *(generated interface: `MGP_COVERAGE_EMITTED_LIST` → `MGPipeFieldEmitter` enumerators in `PipeFilled.inc`)* | **owner** | – | – | – |
|
||
| `MobileGL/MG_Pipe/generated/*.inc` *(all eight, committed; CI diffs them)* | **owner** | – | – | – |
|
||
| `scripts/gen_pipe.py` | **owner** | – | – | – |
|
||
| `MobileGL/MG_Impl/Pipe/PipeFill.{h,cpp}` — the `SubsystemForEmitter` / `static_assert` / `kMGPipeWiredSubsystems` block | **owner (contract commit only)** | **owner (after the tag)** | – | – |
|
||
| `MobileGL/MG_Pipe/{FillPoints.def, PipeMutation.h, DirtySurface.def}` | – | **owner** | – | – |
|
||
| `scripts/gen_pipe_dirty_surface.py` | – | **owner** | – | – |
|
||
| `scripts/{p3a_untouched_regions.sh, p3a_vertex_input_negative_control.sh}` | – | – | – | **owner** |
|
||
| `MobileGL/MG_Impl/Pipe/{ResourceTracker.h, VertexInputEmit.h, Tracker.h, SetHashSuppressor.h}` | – | **owner** | – | – |
|
||
| `MobileGL/MG_Impl/Pipe/{SlotAllocator.*, CsoCache.*}` | **owner** (unchanged in P3a; listed so nobody edits them) | – | – | – |
|
||
| `MobileGL/MG_State/GLState/BufferState/**` | – | **owner** | – | – |
|
||
| `MobileGL/MG_State/GLState/VertexArrayState/**` | – | – | – | – (unchanged in P3a) |
|
||
| `MobileGL/MG_Backend/DirectGLES/SlotTables.h` | **owner (contract commit only)** | – | **owner (after the tag)** | – |
|
||
| `MobileGL/MG_Backend/DirectGLES/**` (everything else) | – | – | **owner** | – |
|
||
| `MobileGL/MG_Backend/DirectVulkan/**` | – | – | – | – (**untouched in P3a**) |
|
||
| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}`, `MobileGL/MG_Test/Util/PipeStatsTest.cpp` | **owner (contract)** | – | – | – |
|
||
| root `CMakeLists.txt`, `MobileGL/Config.h`, `MobileGL/ConfigLoader.cpp` | **owner (contract)** | – | – | – |
|
||
| `MobileGL/MG_Test/Pipe/CMakeLists.txt` and the two stubs' creation | **owner (contract)** | – | – | – |
|
||
| `MobileGL/MG_Test/Pipe/{ResourceEmitTest, VertexInputEmitTest}.cpp` contents | **owner** (`c3`) | **owner** (`b4`, disjoint cases) | – | – |
|
||
| `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` | **owner (contract)** | – | – | – |
|
||
| `MobileGL/MG_Test/SanityTest.cpp` | – | – | **owner** | – |
|
||
| `MobileGL/MG_IntegrationTest/**`, `.github/workflows/test.yml`, `tools/**`, `android-plugin/**`, `MobileGL/MG_Benchmark/**` | – | – | – | **owner** |
|
||
| `docs/Disaggregated/*.md` | – | – | – | – (integrator only) |
|
||
| everything else | nobody | nobody | nobody | nobody |
|
||
|
||
The one file two packages both *touch conceptually* is `MG_Test/Pipe/ResourceEmitTest.cpp`: A's `c3` writes the
|
||
applier-side cases, B's `b4` writes the emitter-side cases. **They are disjoint `TEST` bodies in one file and A lands
|
||
first**, so B's rebase is an append. If that ever collides, it is a rebase conflict at integration, which is exactly the
|
||
shape the P2 wave-1/wave-2 collision took (`cts-waves-landed`) and it is resolved by union, never by choosing a side.
|
||
|
||
---
|
||
|
||
## D. Integration order, and what the integrator runs
|
||
|
||
### D.0 Preconditions
|
||
|
||
- The §A baseline captures (`~/w7/p3a-before-libMobileGL.so`, `~/w7/p3a-before-ctest-names.txt`,
|
||
`~/w7/p3a-before-untouched.sha`, `~/w7/p3a-before-dirty-surface.txt`), taken in `~/w7/pipe` at `55d2af9b`
|
||
**before any package lands**.
|
||
- `~/w7/pipe` clean at `55d2af9b`; fixture binaries hidden the way `wsl_p3a_tree.sh` does it
|
||
(`git update-index --assume-unchanged` on `tools/trace_replay/fixtures`) — **a fixture left visible gets pulled back
|
||
to its LFS pointer by a `rebase --abort`**.
|
||
- `~/w7/retrace_gate.py` present (flags: `--tree --lib --out -j --only`; **no `--ssim`, no `--backend`**).
|
||
- Both devices reachable and **not** locked by the GL4.6 Wave-7 campaign
|
||
(`ls .../8ab6b3cb-.../scratchpad/w7/locks/`).
|
||
- A worktree created for a package must have its submodules initialised and `3rdparty/glslang/External` copied, or the
|
||
build fails in a way that looks like a code error.
|
||
|
||
### D.1 Order: **contract → client → espryt → gates**
|
||
|
||
Reason, and it is not P2's by default:
|
||
|
||
- `contract` first because everything else compiles against it, and because its `PipeFill.cpp` block is what makes the
|
||
generated-enum coupling a compile error at `c0` rather than at a merge (C.5).
|
||
- `client` second because **it is the only package that can land without changing behaviour**: with nothing registering
|
||
`MGPipeResourceOps`, every buffer dispatch still falls through to `g_bufferBackendOps` and the VAO twin still reads
|
||
`MGB_CTX`. If the emission is wrong, the verify lane says so before any backend has been touched — P2's `b2` logic,
|
||
applied to a phase where the backend change is the risky half.
|
||
- `espryt` third: it is the commit that switches the path over, and it lands against a client whose emission is already
|
||
proven by the verify lane.
|
||
- `gates` last, so its lanes assert against the finished tree — except the `HandleRecycleScenario` buffer arm, whose
|
||
red is recorded on the contract tree at D.2.
|
||
|
||
Copy `~/w7/notes/tools/wsl_integrate_p2.sh` → `wsl_integrate_p3a.sh` (`p2`→`p3a`). Per slug: rebase `p3a/<slug>` onto
|
||
`feat/disaggregated` in its own worktree, `git merge --ff-only` in `~/w7/pipe`, then **after every merge**:
|
||
|
||
```sh
|
||
cmake --build build-linux --parallel 28 && ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
|
||
cmake --build build-push --parallel 28 # push AND verify, both, every time - that is where bb2a236d surfaced
|
||
cmake --build build-verify --parallel 28
|
||
python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test
|
||
python3 scripts/gen_pipe_dirty_surface.py --check # from `client` onward
|
||
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all
|
||
python3 scripts/symbol_report.py --before ~/w7/p3a-before-libMobileGL.so --after build-linux/libMobileGL.so \
|
||
--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0 --markdown ~/w7/p3a-symbol-<slug>.md
|
||
# 0 added / 0 removed / 0 renamed / 0 RESIZED after every merge. P3a's admitted set is EMPTY (G1).
|
||
grep -rc 'pGLContext' MobileGL/MG_Backend | grep -v ':0$' # empty
|
||
bash scripts/p3a_untouched_regions.sh 55d2af9b HEAD # G5, after EVERY merge
|
||
ctest --test-dir build-linux -N | grep -E "^[[:space:]]*Test[[:space:]]+#[0-9]+:" \
|
||
| sed -E "s/^ *Test +#[0-9]+: //" | LC_ALL=C sort | LC_ALL=C comm -23 ~/w7/p3a-before-ctest-names.txt - # empty
|
||
```
|
||
|
||
**Any symbol resize outside an admitted set is a defect, attributed per symbol in the commit message, never waved
|
||
through** (ID-3's precedent). P3a's admitted set is empty; if a resize appears, the cause is an edit that escaped a
|
||
`#if MOBILEGL_PIPE_PUSH` guard (D-K).
|
||
|
||
### D.2 On the contract tree, before `client`: record the "red before" evidence
|
||
|
||
```sh
|
||
cd ~/w7/p3a-gates && cmake --build build-verify --parallel 28
|
||
ctest --test-dir build-verify -R HandleRecycle --no-tests=error --output-on-failure | tee ~/w7/p3a-handlerecycle-before.log
|
||
# the buffer arm: .AbaControl PASSES (it asserts the corrupted contents), .Legacy PASSES, .Handles SKIPs
|
||
```
|
||
|
||
That log is the artefact `ROADMAP.md`'s "重键前红" asks for, for the **buffer** kind. Keep it; it goes into
|
||
`MEASUREMENTS.md`.
|
||
|
||
### D.3 After every package has landed: the five-part gate on `~/w7/pipe`
|
||
|
||
`~/w7/notes/tools/wsl_p3a_gate.sh` is `wsl_p2_gate.sh` with these changes and **no others**:
|
||
|
||
- `p2`→`p3a` in every path and in the `P2_GATE_DONE` terminator (→ `P3A_GATE_DONE`).
|
||
- **The fail-fast rule at `:4` and `:19` is kept verbatim**: the three builds are made first and a non-zero rc prints
|
||
`BUILD FAILED - gate aborted (no stale-binary verdicts)` and exits 1. *Results from a stale binary must never look
|
||
like a verdict.*
|
||
- `:24`'s echo of the admitted resizes becomes `G1 symbols (admitted resizes: NONE for P3a)`.
|
||
- `:27-28`'s `SyncRenderState` sha comparison is **replaced** by `bash scripts/p3a_untouched_regions.sh 55d2af9b HEAD`
|
||
(G5). The `RenderStateImpl` sha is P2's invariant and stays true trivially; the P3a invariant is the pool/ring set.
|
||
- `:34`'s `ctest -R 'RenderStateSpans\.|Residual'` gains `|VertexInputEmit\.|ResourceEmit\.` (G6).
|
||
- `:45`'s render-state-sensitive subset is replaced by the **buffer/VAO family**:
|
||
`-R 'LargeArenaAdoption|StorageBufferRegrow|VertexAttribBinding|MultiDraw|PrimitiveRestart|CrossFrameBuffer|ResidentIndex|BufferTexture|AtomicCounter|XfbCaptureBufferReuse|PackedWordReadback|DoublePrecision|VertexArrayEnableDisable|DrawParameters'`
|
||
— called out by name because these are where a vertex-input or buffer mistake shows up first.
|
||
- `:46`'s `g7_negative_control.sh build-push` is kept **and** joined by
|
||
`bash scripts/p3a_vertex_input_negative_control.sh build-push` (G7).
|
||
- `:47`'s `CsoContentAddressing` is joined by `ResourceSubsystemControl` (G12).
|
||
- `:44` gains a third arm: `MOBILEGL_PIPE_PUSH=0x7f ctest -L integration-gpu` (P3a's subsystems off) beside the
|
||
existing `MOBILEGL_PIPE_PUSH=0` (everything off).
|
||
- The two `retrace_gate.py` sweeps at `:54, 57` are unchanged in form; a third, named sweep is added:
|
||
`--only 'create-indirect,create-instancing,rd12-odinlite,improved-transparency-minecraft-26.3,fabric-sodium'`
|
||
(G3b), whose summary is copied out before the output trees are deleted.
|
||
- ID-4's ctest-name extractor at `:8` and the arming-line greps at `:55` are kept **verbatim**.
|
||
|
||
**Part 1 — interface purity.**
|
||
```sh
|
||
python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all # gate A
|
||
nm --undefined-only build-linux/libMobileGL.so | grep -E 'MG_State::GLState::|glslang' # gate B (n/a in monolith; recorded)
|
||
grep -rc 'pGLContext' MobileGL/MG_Backend | grep -v ':0$' # gate C: empty
|
||
grep -n 'MG_State' MobileGL/MG_Pipe/PipeApply.h # no hit in the ops block (G13)
|
||
ctest --test-dir build-verify -R HandleRecycle --no-tests=error --output-on-failure # the {slot,gen} memo-key assertion (G8)
|
||
```
|
||
|
||
**Part 2 — the semantic shadow comparison (the decisive one).**
|
||
```sh
|
||
ctest --test-dir build-verify -L unit --no-tests=error --output-on-failure
|
||
ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4 --output-on-failure
|
||
MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py --tree ~/w7/pipe \
|
||
--lib ~/w7/pipe/build-verify/libMobileGL.so --out ~/w7/retrace-out/p3a-verify -j 4
|
||
grep -L 'MGPipe verify:' ~/w7/retrace-out/p3a-verify/*.log # empty: every case armed
|
||
grep -l 'Fatal{' ~/w7/retrace-out/p3a-verify/*.log # empty
|
||
```
|
||
A `Fatal{UnmigratedPipeInput, "F@V"}` is a missing `FillPoints.def` row — add the row, **never a sticky row**
|
||
(`Coverage.def:106-115`, `:465-466`: none of the version/generation accessors is sticky and `gen_pipe.py` refuses a
|
||
name that is not an accessor), regenerate, rerun. A `Fatal{PipeVerifyDiffer, …, where=read}` is a real push/pull
|
||
divergence and names its field; it is fixed, never silenced. A `Fatal{ProtocolCorruption}` is a malformed
|
||
`CreateVertexElements` blob or an out-of-range `MGPSubData` box (D-G2, D-A2).
|
||
|
||
**Part 3 — behavioural A/B.** As `wsl_p3a_gate.sh` parts 3 above, plus the three retrace sweeps.
|
||
|
||
**Part 4 — performance. RECORDED, NOT GATED (rule (a)).** D.4.
|
||
|
||
**Part 5 — coverage, poison, handle discipline.**
|
||
```sh
|
||
python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test # 0 UNMAPPED
|
||
python3 scripts/gen_pipe_dirty_surface.py --check && python3 scripts/gen_pipe_dirty_surface.py --self-test
|
||
ctest --test-dir build-push -R 'RenderStateSpans\.|Residual|VertexInputEmit\.|ResourceEmit\.' \
|
||
--no-tests=error --output-on-failure
|
||
bash scripts/p3a_untouched_regions.sh --self-test
|
||
```
|
||
|
||
**CTS (G15).** P3a **is** one of the five architecture boundaries (`ROADMAP.md:34`), so the **full `gl44to46` caselist
|
||
(~56,271 cases) runs on both devices**, scheduled off the critical path at the `dev` merge. Per-backend conformance
|
||
within 0.5 pp of the `$BASE` reading; report shape per the house rule (rows = GL version/extension, columns = status
|
||
counts, rate = Pass/(Pass+Fail), NS not in the denominator). The `MOBILEGL_PIPE_POISON_OMIT` sweep that
|
||
`FillPoints.def:60-64` assigns to P3a rides the same caselist run.
|
||
|
||
**CI.** `gh workflow run test.yml --ref feat/disaggregated`, then once green
|
||
`gh workflow run test.yml --ref feat/disaggregated -f baseline_sha=55d2af9b` for `monolith-symbol-report`, and once more
|
||
with `-f baseline_sha=087685d1` for the four-resize reading of G1. Record all three run URLs. Expected green:
|
||
`pipe-gates`, `include-graph-check`, `flatc-check`, `test`, `integration`, `benchmark`, `build-linux-verify`,
|
||
`integration-verify`, `retrace`, `retrace-verify`, `monolith-symbol-report`. **The exit evidence is the local WSL run
|
||
(`~/w7/pipe`), not CI; CI green is required before the `dev` merge, not before the phase verdict**
|
||
(`BRIEF-P2.md:965`).
|
||
|
||
**A decision the CI file forces, carried over from P2.** `test.yml:520-526` records that `integration`'s second,
|
||
`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` filtered pass (186 entries) is not run under the comparator; P2 deferred
|
||
it to P3b saying *"the 186 entries are a buffer/readback filter that P2 changes nothing in"*. **P3a changes exactly
|
||
that filter's subject.** Decision: **P3a runs it, once, under push (not under the 5–10× comparator)**, as a named step
|
||
in part 3 — `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --test-dir build-push -L integration-gpu -j 8` — because
|
||
it is the kill-switch arm of `FlushPendingRangesNow`'s tier 1 (`Managers.cpp:1017`) and P3a rewrites that function's
|
||
caller. Running it under the comparator stays deferred to P3b, and the integrator records the split decision in
|
||
`MEASUREMENTS.md` rather than leaving the note dangling a second time.
|
||
|
||
### D.4 The measurement runs P3a owes — recorded, not gated
|
||
|
||
Rule (a): **every number below is published; none of them can stop the phase.** A regression is written into
|
||
`MEASUREMENTS.md` with its suspected cause and carried into P4a's planning.
|
||
|
||
#### D.4.1 The pull baseline to record against
|
||
|
||
Already in `MEASUREMENTS.md`, no new collection needed:
|
||
- §3, the two-device / two-backend / four-trace boundary-counter table (`:53-62`), whose headline is **steady-state
|
||
dynamic accessor cost 6.5–9.3 per draw** (`:64`) — for `improved-transparency-minecraft-26.3` specifically, a
|
||
1200-frame window at 1320 draws/frame with Espryt acc/draw **8.44** and Magma **6.53**, Espryt buf 333 K B/f, gates
|
||
ers 156925/2791, etl 148606/11110, eub 148246/11470.
|
||
- The six memo gates' hit/miss pairs and the `stage-*` byte classes, including `stage-ubo-named`'s 331 KB/frame
|
||
asymmetry (`:68`) and the union-box vs region-list 635 K vs 40 K, 16× (`:70`).
|
||
- **The one number `ROADMAP.md:19` names**: `MEASUREMENTS.md:87` — *"**persistent map 采纳的既有基线**(`dev`,MC 26.3,
|
||
Adreno):≥16 MiB 可变 store 定义时采纳为 coherent persistent map 后 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB"*.
|
||
- P2's own §8 table (the two-device p50/p99, T1/T2, the blend-toggle table, the CSO negative control).
|
||
|
||
**The caveat that must travel above the numbers, not below them** (`BRIEF-P2.md:417-419`, D17):
|
||
`CallClass::AccessorCalls` is a set of **static tallies at ~10 hot entry points** (`PipeStats.cpp:16-100`, and *"那份
|
||
清单是契约"*), so a change that merely **relocates** reads scores a lower `acc/draw` without removing work. Lead with
|
||
the **gate hit/miss pairs** and the **CPU-time series**; quote `acc/draw` only alongside a re-audit of the tally
|
||
constants at `DirectGLES.cpp:1421, 1524, 2043, 2053, 2977` and `VulkanRenderer.cpp:5009, 5016, 5274, 5927, 5934, 6416,
|
||
6449, 6462`.
|
||
|
||
#### D.4.2 The paired two-device A/B
|
||
|
||
Protocol per device, unchanged from P2 and driven by `p3a_ab.sh`:
|
||
|
||
- **reboot-clean**: `adb -s $SER reboot`, wait for `sys.boot_completed`, poll for root (24 × 5 s), sleep 20, wake +
|
||
unlock, uninstall the package, then **pin up to 3 times with a `check` after each and exit 43 if none verified**.
|
||
- **CPU/GPU pinned** via `tools/device_bench/pin_device.sh <serial> pin|unpin|check` — **not** `bench.sh`'s
|
||
`pin_freqs()`, which is MediaTek-legacy-only and **writes into nothing and exits 0 on both campaign devices**.
|
||
`check` is three-valued: **0 PINNED**, **1 DRIFT** (*the dangerous state — a run overlapping it is not comparable,
|
||
discard it*), **2 UNPINNED**. Sequence: `pin` → `bench.sh … --no-pin` → `check || "PIN DRIFTED - discard this run"` →
|
||
`unpin`. Big 1.96 GHz / little 1.55 GHz, GPU maxed, 40 °C gate.
|
||
- **serially from one tree**: `run_android_retrace_local.py` shares one `.trace-work/android-retrace-result` root per
|
||
tree and `rmtree`s it per invocation (`MEASUREMENTS.md:93`), so the two devices **must not** be driven concurrently
|
||
from one worktree.
|
||
- **device lock** ≤ 25 min per acquisition, `owner` file written, `rm -rf` on the way out; a lock held > 60 min is not
|
||
broken — report "blocked by device lock".
|
||
- `MSYS_NO_PATHCONV=1` on every invocation (`/data/...` inside an `--env` value is path-converted otherwise); but **do
|
||
not export `MSYS_NO_PATHCONV` / `MSYS2_ARG_CONV_EXCL` globally in `p3a_ab.sh`** — the runner passes `/c/...` paths to
|
||
Windows python and relies on MSYS converting them.
|
||
- **ColorOS traps** on `3B159D009VZ00000`: the first install of a not-yet-installed package blocks on
|
||
`com.oplus.appdetail InstallGuideActivity` until "继续安装" is tapped; a foreign-signed APK must be uninstalled first.
|
||
- Git Bash cannot create directories on `//wsl.localhost`; outputs go to a Windows-local scratchpad and `copy_ab.sh`
|
||
moves them into `~/w7/notes/p3a/ab/<serial>/`.
|
||
|
||
Arms: **{pull APK, push APK} × {`--benchmark-no-finish`, finish} × 4 cases × 2 backends**, per device. Cases, chosen to
|
||
match `ROADMAP.md:19`'s named set minus the blocked one:
|
||
|
||
```
|
||
improved-transparency-minecraft-26.3 # the p99 number ROADMAP.md:19 names
|
||
minecraft-1.21.4-rd12-odinlite-in-world
|
||
minecraft-1.21.4-fabric-sodium-in-world
|
||
minecraft-1.21.1-neoforge-create-instancing-in-world # a coherent_as_flush fixture
|
||
```
|
||
|
||
**Excluded**: `minecraft-1.21.1-neoforge-create-indirect-in-world` — it fails on both devices on the `dev@81b17c0b`
|
||
baseline APK itself (`MEASUREMENTS.md:96`, `ROADMAP.md:90`), so it is not this branch's regression. It stays in the
|
||
desktop SSIM corpus and out of the device A/B, exactly as in P2.
|
||
|
||
```sh
|
||
LOCKS=.../8ab6b3cb-.../scratchpad/w7/locks
|
||
ANDROID_SERIAL=$SER MSYS_NO_PATHCONV=1 python3 tools/trace_replay/run_android_retrace_local.py \
|
||
--case "$c" --backend "$b" --benchmark --benchmark-no-finish --benchmark-repeats 3 --benchmark-tail-frames 200 \
|
||
--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
|
||
```
|
||
|
||
Then, host-side, reduce `frameCpuTimesMs[]` over the trailing 200 frames to **p50 and p99** per arm and publish the
|
||
delta `push − pull` per (device, case, backend). `benchmark.json` carries the whole array on purpose, so p99 is a
|
||
**host-side reduction over an artefact that already exists**; `nearest_rank_percentile` uses nearest rank so every
|
||
number printed is a frame that was actually observed, and `series_median` recomputes p50 by the device's own rule so it
|
||
agrees with `medianFrameCpuMs` rather than merely sitting beside it. `--benchmark-no-finish` is the **primary** arm
|
||
because P3a's question is CPU cost; the finish-ON arm is the sanity check that GPU time did not move.
|
||
|
||
Read out of the same runs: the six memo gate hit/miss pairs (`ers etl eub mfp mpm mdt`) from the **last complete
|
||
window** of `grep 'MGPipe stats:' mobilegl.log`, the `stage-buffer` / `stage-vertex-client` / `stage-index-client` byte
|
||
classes, and **`mpr`** (G10).
|
||
|
||
**Trap**: `MOBILEGL_PIPE_STATS_FILE` never produces a file on device — the JSON is written from `PipeStats::Shutdown()`
|
||
which runs from `MobileGL::Destroy`, and the trace app never reaches that teardown. Only the periodic `mobilegl.log`
|
||
lines exist, and a run shorter than one period yields nothing; hence `MOBILEGL_PIPE_STATS_PERIOD=120`, or lower for a
|
||
short fixture.
|
||
|
||
#### D.4.3 The headline number: MC 26.3 p99 on Adreno
|
||
|
||
Published as: p99 and p50 of `frameCpuTimesMs[]` for `improved-transparency-minecraft-26.3` on `35d0befa`, both
|
||
backends, pull vs push, against `MEASUREMENTS.md:87`'s adoption baseline (p99 163→21 ms, 40→115 fps, ~400 MB) and
|
||
against P2's own §8 reading of the same case. **Recorded, not gated** (rule (a)). If p99 moved, the report says by how
|
||
much, on which arm, and names the suspected cause; the phase proceeds.
|
||
|
||
#### D.4.4 DriverBench T1/T2
|
||
|
||
```sh
|
||
cd ~/w7/pipe && cmake --build build-bench --parallel 28
|
||
for lib in build-linux build-push; do for be in espryt magma; do
|
||
DRIVERBENCH_EGL_LIB=$PWD/$lib/libMobileGL.so bash MobileGL/MG_Benchmark/Driver/run_driver_bench.sh $be $PWD/$lib/libMobileGL.so
|
||
done; done
|
||
bash MobileGL/MG_Benchmark/Driver/run_driver_bench.sh native # the control
|
||
```
|
||
|
||
- **T1** = `ns_per_op(push, default bitmask 0x1ff) − ns_per_op(pull)` on `mc_vanilla_draw` — the whole boundary cost per
|
||
draw.
|
||
- **T2** = `ns_per_op(push, MOBILEGL_PIPE_PUSH=0x7f) − ns_per_op(pull)` — P2's boundary alone, so **T1 − T2 isolates
|
||
exactly what P3a added and what it removed.**
|
||
- `mc_state_toggle` is published beside them as the non-P3a control (a blend toggle touches no buffer and no VAO, so it
|
||
must not move).
|
||
|
||
`DriverBench` `dlopen`s exactly one EGL provider through `DRIVERBENCH_EGL_LIB` with **no `LD_LIBRARY_PATH` shadowing**,
|
||
and pins the vendor json / ICD explicitly — a bare `libEGL.so.1` on a glvnd system resolves to Mesa/llvmpipe, i.e. *a
|
||
software rasteriser silently replacing the GPU under a benchmark*. `wsl_p3a_gate.sh` configures with
|
||
`-DMOBILEGL_BUILD_BENCHMARK=OFF`, so this runs in `build-bench`, not in the gate build. **No ceiling is pinned and none
|
||
is enforced** (rule (a)); the numbers are published so P4a can pin one if it wants to.
|
||
|
||
#### D.4.5 The Track H unit-cost census
|
||
|
||
`ROADMAP.md:56`'s criterion is *"Track H 单位成本不超出估计的 50%"*, measured in calendar days. Record the elapsed days
|
||
per package, the diff sizes, and the memos actually retired against `ARCHITECTURE.md:363`'s census (11 direct
|
||
deletions, 2 moved to client debounce, 7 re-keys, 1 unchanged). **P2 paid 11 of the 11 direct deletions and 2 of the 7
|
||
re-keys.** P3a pays: the twelfth-through-thirteenth entries of the direct-deletion list are already gone, so P3a's
|
||
contribution is **`ConvertedVertexStreamKey`'s `sourcePin`** (the last of the 11, D-J) plus **two of the seven
|
||
re-keys** — the VAO twin's index-slot `{Uint16 version + raw pointer}` → one server `Serial`, and
|
||
`ResolvedDrawBuffers`' raw `BufferObject*` entries → `{slot, gen}`. Also record the **27-day trip wire**
|
||
(`ROADMAP.md:64`): if P3a exceeded it, `ROADMAP.md:68` says to run the `inproc` falsification numbers before P4a.
|
||
|
||
#### D.4.6 What is published even though it is not asked for
|
||
|
||
- `CreateVertexElements` blob bytes per frame on the four A/B cases — the cost of D-G2's second view, so P13's trim
|
||
decision starts from data rather than from the argument in this brief.
|
||
- The per-dirty-bit fire rates for bits 5, 9 and 10 before and after the narrowing, so P3b/P4b's object-class bits
|
||
start from data the way P2's table let P3a start from data (`BRIEF-P2.md:956`).
|
||
- Peak RSS, push vs pull, over the 79-case retrace — the seventh slot table is new memory and a leak in it would be
|
||
invisible to every correctness gate.
|
||
|
||
### D.5 Docs, and the merge to `dev` (integrator, one commit)
|
||
|
||
`scripts/check_doc_citations.py docs/Disaggregated/*.md` must stay clean — every `file:line` a doc edit adds has to be
|
||
true. The scout corrections in this brief's preamble are **not** doc bugs (the scouts mis-cited, the docs are right)
|
||
except where noted below.
|
||
|
||
- `README.md:3` — status → P3a landed, with the hash.
|
||
- `ROADMAP.md:19` — ✅, with the real numbers: the nine `resource_*`/vertex calls wired, the seventh slot table, the
|
||
memos retired, `map-persistent-roundtrips`' first readings, and the MC 26.3 p99 pair.
|
||
- `ROADMAP.md:83` (open question 10) — restated: P3a kept the `ResidentSubData` asymmetry and did **not** give Magma an
|
||
implementation.
|
||
- `ROADMAP.md:90` (open question 17) — the `create-indirect` state at the phase exit, honestly: fixed on `dev`, or
|
||
still blocking G3b.
|
||
- `ARCHITECTURE.md:63` — **[deviation] D-G1**: the vertex-elements CSO is identity-addressed for Espryt; the
|
||
content-addressed 1024-entry cache is P7's, when Magma's `VertexInputStateFactory` takes the CSO over.
|
||
- `ARCHITECTURE.md:130` — the stated reason for carrying both views is corrected: the frontend already resolves
|
||
pointer-stride-0 (`MGPipeValueTypes.h:496-503`), so the reason both views travel is record self-consistency
|
||
(`BindingPointCount` describes its own blob), not stride disambiguation.
|
||
- `ARCHITECTURE.md:284` — **[deviation] D-K**: `SetBackendResource` is not deleted in P3a; under push it is simply
|
||
never called, and the deletion retires with the pull path at P13 (G1).
|
||
- `ARCHITECTURE.md:293` — `kNeedsAck` has landed on `ResourceRespecify` **with a per-record predicate**, and the
|
||
sentence "目录里目前没有条目携带 `kNeedsAck`" is now false and is replaced.
|
||
- `ARCHITECTURE.md:367` — `MOBILEGL_PIPE_LEGACY_MEMOS`'s P3a arm is named.
|
||
- `ARCHITECTURE.md:474` — `map-persistent-roundtrips`' definition is written down: **every `MapPersistent` emission,
|
||
mint or decline**, which is why it is non-zero and assertable in monolith (D-B2).
|
||
- `ARCHITECTURE.md:504` — the citation `tools/bench.sh` → `tools/device_bench/bench.sh` if P2's integrator did not
|
||
already fix it.
|
||
- `MEASUREMENTS.md` — a new numbered section with: the D.4.2 two-device p50/p99 table (with D.4.1's `acc/draw` caveat
|
||
**above** it), the MC 26.3 p99 pair against `:87`, T1/T2 and the `mc_state_toggle` control, the per-dirty-bit fire
|
||
rates for bits 5/9/10, the `mpr` readings, the `CreateVertexElements` blob bytes, the D.2 "red before" log, peak RSS,
|
||
the Track H unit-cost census, the `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH` decision of D.3, **and the user's
|
||
2026-09-08 rule (a) transcribed verbatim**, since it is not written anywhere in `docs/Disaggregated/`.
|
||
- `MEASUREMENTS.md` also records the `g_uploadRing`-not-reset asymmetry (D-F) as a `dev`-side follow-up that P3a
|
||
deliberately preserved.
|
||
|
||
Then `[Merge]` to `dev` per the milestone flow, and schedule the full `gl44to46` caselist on both devices **off the
|
||
critical path** (`ROADMAP.md:34`, G15). Commit messages are single-line `[Type] (Scope): description`; **never** a
|
||
`Co-Authored-By` or any other attribution line.
|
||
|
||
---
|
||
|
||
## E. Risks and mitigations
|
||
|
||
| risk | mitigation |
|
||
|---|---|
|
||
| **`MGPVertexBuffers` growing 16 → 24 breaks something no test covers** — a wire `static_assert`, a `PipeFields.def` row, a generated table. | It is a **build break at `c0`**, not a silent drift, and `c0` is the first thing built. `gen_pipe.py` refuses a field list that does not name exactly the struct's direct members (`PipeFields.def:16-19`) **in both modes, hence in `pipe-gates`**, and `git diff --exit-code -- MobileGL/MG_Pipe/generated` catches an unregenerated table. The `MGP_VERIFY_PAYLOAD_LIST` row is the one a compiler cannot catch — hence its explicit line in C.0's table. |
|
||
| **The `Coverage.def` ↔ `SubsystemForEmitter` semantic-merge trap fires again** (commit `bb2a236d`: an emitted row removed on one branch, its generated enumerator still named on another, and the push and verify builds did not compile on the integrated tree). | Discharged **structurally**: A owns `Coverage.def` **and**, in the contract commit only, `PipeFill.cpp`'s enum-coupled block; B branches from the tag, so A's edit is already in B's base and the two cannot diverge (C.5). On top of that, **every merge in D.1 builds push AND verify**, which is the only place the trap surfaces, and `gen_pipe.py --self-test`'s control at `:1230-1234` refuses an emitted row naming a call that does not exist. |
|
||
| **A `kNeedsAck` on `ResourceRespecify` acks every `glBufferData`** — Minecraft's chunk upload turns into a round trip per store in split. | The flag is declared on the call, but the **per-record predicate** `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)` decides (D-A5), the legend at `PipeCalls.def:18` says so, and `PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage` pins it against both idioms. In monolith the ack is `((void)0)`, so P3a cannot ship the mistake even if the predicate were wrong; P5 is where it would bite, and P5 inherits a pinned test. |
|
||
| **The vertex-elements CSO is identity-addressed, deviating from `ARCHITECTURE.md:63`** — and P7 then finds it cannot content-address Magma's without redoing P3a's work. | The deviation is *narrower*, not wider: P3a mints one CSO per VAO, which is a **valid content-addressed cache with a hit rate of one**. P7 adds the hash-probe-`memcmp` layer above the same `CreateVertexElements`/`BindVertexElements`/`DeleteVertexElements` calls and the same slot allocator; nothing in P3a's wire shape or applier record forbids it. The reason it is not done now is that Espryt's twin is a driver VAO name plus 64 scratch buffer ids that two frontend VAOs cannot share, so content addressing would be strictly slower on the only backend P3a touches. Recorded against `ARCHITECTURE.md:63` at the phase exit. |
|
||
| **`BaseInstance` is not in the `ContentHash`, so a baseInstance-only change is suppressed and the server keeps the previous fetch shift** — silently wrong geometry on instanced draws, and no SSIM gate on a desktop corpus that may not exercise it. | It is stated as a hard requirement in D-H2.3 with two named unit tests (`ABaseInstanceChangeAloneStillEmitsTheVertexBufferSet`, `AnUnchangedSetWithAnUnchangedBaseInstanceEmitsNothing`), and the behavioural gate is `VertexAttribBindingScenario`'s two dedicated cases — `BaseInstanceMovesTheInstancedArraysStartElement` (`:406`) and `BaseInstanceLeavesPerVertexArraysWhereTheyWere` (`:451`) — which capture the vertex shader's inputs with XFB under `GL_RASTERIZER_DISCARD` from a poison-prefilled buffer, plus all eight `DrawParametersScenario` cases (`:213-327`). |
|
||
| **Deleting the ambient `g_pendingFetchBaseInstance` breaks a path nobody remembered** — an indirect or multi-draw entry point that relied on it. | `grep` says there are **exactly three** scope sites (`DirectGLES.cpp:5135, 5172, 5224`) and all three are direct `*BaseInstance` entry points; `DrawElementsIndirect` (`:5216`) and the multi-draw tiering never used it. The check is mechanical and is in C.2's verification (`grep -n 'g_pendingFetchBaseInstance\|ScopedFetchBaseInstance' … # empty`), and `MultiDrawScenario`'s ten cases plus `DrawParametersScenario`'s `MultiDrawElementsIndirectCarriesEveryCommandsParameters` (`:301`) are the behavioural backstop. |
|
||
| **`BindMask & ELEMENT_ARRAY` is set wrong and nothing in P3a can see it** — it only matters in split, where a wrong bit silently disables restart rewriting and multi-draw flattening (P8 expectation 1). | The mask is built from one `constexpr` `BufferTarget` → bit table with a `static_assert` that it covers every enumerator, so a new target cannot be silently unmapped; `ResourceEmitTest.EveryBufferTargetSetsItsBindMaskBit` walks all of them on create **and** on respecify; and it is sticky, so a buffer bound as an EBO once keeps the bit forever. This is the one P3a deliverable whose only real gate is a unit test, and it is called out here so a reviewer knows to look at it. |
|
||
| **The seventh slot table has no garbage collector, so a buffer nobody destroys leaks its twin forever** — worse than today's `weak_ptr`-in-`PipeResource` ownership, which dies with the frontend object. | `ResourceDestroy` is emitted from `~BufferObject` **unconditionally**, not from a sweep, and the ordering (emit, then `MGPipeSlots().Free`) is fixed in D-L with `HandleRecycleScenario`'s three arms as the gate. Peak RSS over the 79-case retrace, push vs pull, is published (D.4.6) precisely so a leak is visible. `Managers.h:306-314` quantifies what the old lazy sweep cost, which is the comparison. |
|
||
| **The reverse-channel `OnBufferWriteback` resolves a stale or dangling handle** — the client's `slot → BufferObject*` inverse is a raw pointer. | The entry exists only between `ResourceCreate` (constructor) and `ResourceDestroy` (destructor), and a readback is only ever issued for a live, bound buffer; a `MGPipeHandle::Gen` compare against `MGPipeSlots().GenOfSlot` guards it in debug builds. If that assumption is ever wrong it is a debug abort, not silent corruption. |
|
||
| **The epoch bump lands before the writeback** and the server's draw-clean memo goes stale behind it. | `ARCHITECTURE.md:288` makes the order a correctness rule and D-D writes it as a five-step sequence. The existing coverage is `AtomicCounterScenario.SubDataAfterDispatchSurvivesAnImmediateReadback` (`:242`) and `LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack` (`:245`), both of which fail if the bump moves. `Managers.h:556-590` is the exhaustive bump-site contract and must be read before touching anything in that file. |
|
||
| **`AcquirePersistentMap` is "untouched" in principle but its signature change perturbs it in practice** — and D-B4 is the one thing the whole monolith conversion is supposed not to break, worth p99 163→21 ms and ~400 MB on the headline device. | D-E enumerates the **only two** permitted changes and names every statement that must stay byte-identical, down to the locally defined `0x0040/0x0080/0x0100` constants and the stale-generation wipe ordering. The behavioural gate is `BufferTest.cpp`'s ~30-case adopted-store family (`:1658-2943`), `LargeArenaAdoptionScenario`'s three cases, and MC 26.3's p99 on Adreno. A reviewer should diff the function against `$BASE` by eye; it is 76 lines. |
|
||
| **`FlushPendingRangesNow`'s three-tier drain silently changes tier** because its caller now supplies bytes differently — tier 1 (`INVALIDATE_BUFFER` map + memcpy) is taken only for a whole-buffer flush or ≥128 KiB, and dropping into tier 3 costs the Mali WAR stall back. | The function is in **G5's byte-identical set** and its kill switch (`Features.EsprytDisableInvalidateFlush`, `Managers.cpp:1017`) gets its own integration pass in D.3. `CrossFrameBufferScenario`'s thirteen cases (`:409-518`) plus `StreamedArenaScenario`'s two recycle cases (`:675, 731`) are the behavioural gate, and MC 26.3's p99 is the number that moved when this last regressed. |
|
||
| **`create-indirect` is still red on `dev` at the phase exit**, so a named acceptance case cannot be run. | It is a **pre-existing `dev` bug** (`MEASUREMENTS.md:96`, `ROADMAP.md:90`), reproduced on the `dev@81b17c0b` baseline APK, and it is not P3a's to fix. G3b is recorded as "partial, blocked on open question 17" rather than claimed as a pass, the fixture stays in the desktop SSIM corpus, and it stays out of the device A/B. P8 asserts `roundtrips-per-frame == 0` on precisely that fixture, so the `dev` fix is on someone's critical path — just not P3a's. |
|
||
| **The 27-day trip wire fires.** | `ROADMAP.md:64` and `:68`: run the `inproc` falsification numbers, then decide. The desktop half of every correctness gate can NO-GO the phase without touching a device, so the schedule's long pole (two shared devices under a `mkdir` lock, ~32 lock holds of ~10 min for D.4.2) sits **after** the verdict, not before it. Budget two days for the device work and hold ≤ 25 min per acquisition. |
|
||
| **A gate goes green for the wrong reason** — `ROADMAP.md:7`'s **每个门必须能因它存在的理由变红**. | Every P3a gate ships its negative control: G5 has `p3a_untouched_regions.sh --self-test`; G6 has G7's scripted field-drop; G8 has the `.AbaControl` arm that asserts the corruption and whose D.2 log proves it was red before the re-key; G9 has the scanner's two canned controls; G10's `mpr` would read zero if the counter were never incremented, which the scenario asserts against; G12 has `ResourceSubsystemControlScenario` proving the switch changes behaviour rather than being dead; G4 has the verify lane's existing `MOBILEGL_PIPE_VERIFY_CORRUPT` and `MOBILEGL_PIPE_POISON_OMIT` controls, whose steps pass **when ctest fails**. |
|
||
| **A test that drives a backend helper directly aborts on the per-verb poison** because no verb filled the block. | `MG_Test/ScopedPipeVerb.h` exists for exactly this and eleven tests already use it (`ef6227e1`). Any new test declares its verb the same way; the poison is never weakened and **no test name changes** (G14). |
|
||
| **A worktree builds against a stale `.cxx` or a half-initialised submodule** and a green looks like a verdict. | `wsl_p3a_gate.sh` keeps `wsl_p2_gate.sh`'s fail-fast rule verbatim: three builds first, and any non-zero rc prints `BUILD FAILED - gate aborted (no stale-binary verdicts)` and exits 1. Tree creation initialises submodules and copies `3rdparty/glslang/External`; fixtures are hidden with `git update-index --assume-unchanged`, because a `rebase --abort` on a visible fixture pulls it back to its LFS pointer. |
|
||
| **Performance regresses and someone stops the phase for it.** | Rule (a) is explicit and is transcribed into `MEASUREMENTS.md` (D.5): **performance is recorded against the pull baseline, never a blocking gate in P3a; correctness gates stay.** A regression is written down with its suspected cause and carried into P4a's planning. The one thing that is *not* negotiable is that the number is actually collected — an unmeasured regression is not a recorded one. |
|
||
|