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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3. Inconsistencies fixed in passing

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

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

39 KiB
Raw Blame History

INTEGRATOR-DECISIONS-P5E (2026-09-18, base feat/disaggregated @ 2fde7034)

The 19 rulings of BRIEF-P5E.md §6 are ACCEPTED as written (resolution column), with the notes below. Numbering continues the campaign's ID series.

ID ruling
80 P5e is a phase of its own on the IPC track (P5d → P5e → P6): "the client runs ahead of apply on the Espryt draw path". Packages and order per BRIEF-P5E §1: c0e → id → {vi, sb, pg, tx2, fb} ∥ ra; the integration commit flips kMGPipeP5eRunAheadReady.
81 Ruling 1 (monolith arm selection): S1–S5's shape — handle arms only under Transport != Monolith; the push-monolith build keeps its frontend arms and bytes.
82 Ruling 2: client vertex arrays are REFUSED under run-ahead (kDrawClientArrays, Fatal{UnmigratedVerb,"DrawArrays+CLIENT_ARRAYS"} shape); lockstep unchanged; staging is P8's. vi answers open item (a) (DoublePrecisionScenario) itself and adds a scenario if none exercises it.
83 Ruling 3: the wait decision is the wire-derived predicate MGPipeBarriered(op, payload, applierState) computed identically by both roles (static WaitClass column + XFB-active from MGPContextValues + kDrawClientArrays).
84 Ruling 4: rule F is scoped to UNBARRIERED records; barriered records keep P5C semantics (the client is parked in its own wait), which leaves readback / CopyTex / GetTexImage / set_storage_block_binding / XFB barriered and trailing.
85 Ruling 5: set_storage_block_binding is kWaitApplied now; pg's trailing item resolves by ShaderCso.
86 Rulings 6, 7: BindCurrentTextures memo key = SamplerViewsSerial AND {DrawProgram slot/gen, ShaderCso.Serial, BindingsSerial}; the image sweep gate = (ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration), keeping the window/high-water union.
87 Ruling 8: the program archive is ENCODED per link into SEG_STAGE (rule B); pg MEASURES links/frame, bytes/link and GL-thread ms on MC + a shader pack before declaring done; lazy encode at first bind is the fallback if the cost is visible (> 0.2 ms/frame steady).
88 Ruling 9: a failed relink of a bound program re-issues create_shader_state with LinkStatus = 0 iff the frontend reports it unlinked; never object_death.
89 Rulings 10, 11: set_shader_buffers window = touched high-water per target, 84 on the wire, backend clamps; three suppressor slots.
90 Ruling 12: Magma's four apply-thread allocator sites live under ONE renamed backend-kind-keyed scope (MagmaP7AllocatorDebtScope); Magma stays lockstep (no kCapRunAheadApply), every record barriered there.
91 Ruling 13: the deferred-destroy queue stays; an unbarriered enqueue is a finding (ra pins it).
92 Ruling 14: present credit default 1, range 1..8; credit 2 is a device measurement arm.
93 Ruling 15 / 17: resource_subdata buffer half kWaitNone (the reply Bool is discarded today - the hidden per-64 KB lockstep of the persistent-map push goes away); the texture half keeps its reply (trailing, D-D5).
94 Ruling 16: MGPImageView::Access IS encoded on the client (ImageEmit.h); the decode moves to MGPipeValueTypes.h; the stale server comment is fixed by fb.
95 Ruling 19 / A8: the two unit windows (set_sampler_views, set_vertex_buffers) MUST cover [0, MaxTouched…]; the sink checks at every draw/dispatch (CheckUnitWindows); a narrower window is Fatal{ProtocolCorruption, "<row>.Count"}.
96 Open item (c): id enumerates the NoSession server-only fixtures that lose death delivery when the mailbox hop is deleted and REWRITES them to drive object_death by handle through the applier (keep the cases, change the delivery); if a case cannot be expressed that way it is deleted with the reason in the report - the integrator confirms at landing.
97 Gates per package: build + unit + integration-split + gens; the strict lane on the scenarios the package names. Phase exit = BRIEF-P5E §3 + §4. No inter-package adversarial review rounds (ID-66); one cross-family review (Kimi K3) at the phase close.
98 Landing flow: package branches p5e/<slug> in WSL ~/w7/p5e-<slug> (worktrees of ~/w7/pipe's object store, base = the Windows head fetched by sha); landing = fetch into the Windows worktree MobileGL-disagg from remote wslpipe, merge (ff when possible), gate on ~/w7/p5d-gate, push origin.
99 id 落地裁定(2026-09-18,p5e/id@0117d157,unit 2204/2204、isplit 111/111、strict 与基线一致),逐条接受它提的六项:(1) 三个 by-handle resolver 声明/定义在各自 registry 旁(Managers.h/Managers.cpp 的三处),不在 BRIEF 猜的 Managers.h:309-640 区间——范围以代码结构为准,BRIEF 的行号是估计值;(2) DirectGLES.cpp 的 detach walk 落在 fb 的区间内,因为 ForEachLive 改签名后它是唯一调用点,不同提交会断构建——接受,fb 在其上 rebase,该处已具名指向 fb 的反向索引;(3) Managers.cpp 的 HandleOfBuffer 站点留在 MGPipeFrontendKeyedRegistryScope 名下(另一读法会让 Espryt 每条索引 draw 都中止);(4) 四个新公开名按 G2/G14 配 skip twin(unit 2203→2204 即此);(5) ForEachLive 只留 handle 臂、删掉弱引用回退——接受,这正是"server 跨记录持有前端对象"的最后一处;(6) ID-96 的答案是空集:删掉 NoSession mailbox hop 后,没有任何夹具失去死亡投递(探针在被删分支上跨 2204 unit + 111 isplit 从未触发),因此不改写、不删除任何用例。
100 id 未做的 SanityTest 四处搬迁不是欠项:ID-81(handle 臂仅在 Transport != Monolith)把前端键控成员保留为 monolith 胶水,那些用例因此仍能编译、仍覆盖该表面、且跑在非 apply 线程上(守卫本就不该触发)。scout-S7 的原设计假设这些成员被删除,与 ID-81 冲突,以 ID-81 为准。
101 id 额外新增的第二道守卫(MGPipeRefuseFrontendKeyedRegistryFromUnbarrieredApply:按前端身份读 registry 状态 / ForEachLive 的弱引用走查,不经分配器所以旧守卫看不见)接受并进入契约 §4.4:规则是"role 不得按对方身份索引自己的表并交回对方的对象",不只是"不得调对方的分配器"。
102 red-once 的假绿教训(id 自查发现):在 apply 线程上构造前端对象来驱动守卫是无效对照——构造函数自己会 mint,于是把守卫改回去也照样中止、用例照样"通过"。规则:red-once 的被测动作必须是那一处探测本身,前置对象在 client 线程上建好再把探测 post 过去。
103 wave 1 集成裁定(f6cfcbd3):c0e 与 id 并行各写了一份共享 seam,合并后五处重复定义。保 c0e 的 MGPipeBarriered 真实实现(静态 wait-class 列 + XFB 升级 + 客户端数组升级),删 id 的恒真占位;当前记录标志保 c0e 的 thread_local + setter(它描述"本线程此刻在应用哪条记录",不是 applier 的每上下文状态;读它的守卫不该先去找对的 applier),删 id 的 MGPipeApplierState 成员;三个 by-handle resolver 保 id 的真实实现、删 c0e 的 stub(ID-99(1))。并加就绪闸:ApplyOne 在 kMGPipeP5eClientWaitRuleLanded(PipeApply.h,ra 翻)为 false 时一律盖 true——谓词说的是客户端"将会"怎么做,而 ra 未落地前客户端仍逐条等待,此时盖 false 会把 §4.4 的豁免从一个客户端自身等待仍使其安全的探测上收回(无缺陷的误杀);谓词照样每条记录计算,整阶段都在被执行而不是等到它开始决定的那天才首次运行。
104 MGPShaderBuffers::WritableMask 是 Uint32、窗口却是 84 点(c0e 发现,brief 与 scout S5 都漏了):裁定加宽字段,让掩码覆盖所声明的窗口(Uint32 WritableMask[3] 或 Uint64+Uint32),不缩窗口——缩到 32 会改变应用可见行为。POD 尺寸随之增长是可以的:本阶段 ABI fingerprint 已因 opcode 80 变动,PipeCatalogueTest 钉新尺寸。sb 负责改这个 wire 结构(c0e 已落地的类型文件的一处具名例外),与 emitter 同一提交。
105 MGPProgramDesc 192 → 200 字节:接受 c0e 的诚实版本。契约草稿里"塞进现有 pad、尺寸不变"在算术上不可能(固定头在偏移 24 处正好用满,MGPBlobRef 按 8 对齐)。不接受挤 StageMask 位或压窄 ReservedNumSamplesOffset 的替代——用可读的字段换 8 字节是划算的。
106 MG_Backend/Init.cpp 的 consumer bit 13 由 sb 添加,且必须与 ShaderBufferEmit.h 的 wired 常量同一提交(c0e 故意不加:无 emitter 时先不加是安全方向)。否则客户端的 R-8 存活门会把整族发射扣下。合并时按此核对。
107 设备验证发现的 P5e 回归(2026-09-18,头 44f91c74):MOBILEGL_TRANSPORT=monolith 下游戏启动即 SIGSEGV,栈为 Clear → SyncCurrentFBO → SyncToBackend(前端FBO) → SyncAttachmentSurface(MGPSurface) → SyncMipmapsToBackend(SharedPtr) +0x1。根因:SyncMipmapsToBackendByHandle 传空 SharedPtr 并依赖"已记下句柄"把被调函数切到记录臂——有传输时成立,monolith 下无记录臂即空指针解引用(违反 ID-81)。inproc 臂两次均正常(31 s 进世界、60 s 采样、140.0 / 144.9 fps、852 draws/帧、零 Fatal),所以这是 monolith 专属回归。修复包 p5e/fix1 领走,并须补上门禁漏掉的覆盖:monolith 臂上"清一个带纹理附件的 FBO"没有任何 lane 条目走过,这正是 2250 unit + 113 isplit 全绿却漏掉它的原因。
108 设备验证的执行纪律(本轮踩了三个坑,全部是测量污染而非代码问题):(a) adb install 的收尾会在一两分钟后杀掉正在跑的同包进程(am_kill … due to installPackageLI),装包与测量之间必须留出安静期;(b) 派给外部 CLI(Kimi)的任务其进程会活过 harness 的完成通知,派出后不得自己再跑同一件事——本轮两套实验同时对一台手机 force-stop / 清 logcat,得出了假的失败;(c) 不得在脚本正被执行时修改它(bash 边读边执行),要改先冻结副本。另:游戏日志里的 GIT@ 戳记在增量构建下是旧的(android-build-bisect-traps 的老账),判断版本要看库里的符号而不是戳记。
109 fix1 的 lane 标签之问,裁定:标签保留,并把 monolith lane 升为集成分支的常设门禁步骤。 fix1 把 DirectGLES.PushMonolithArm. 挂上 integration-split,理由是该标签指的是"门禁要跑的集合"而不是"跑 inproc"——接受这个读法:ID-81 给 push 构建两条服务端臂,若只有一条在被门禁的集合里,"门禁全绿"就一直等于"两条臂里的一条全绿"。但这不够:fix1 实测 ctest -L integration-gpu 在 44f91c74 上是 1157/1294,137 个 DirectGLES SEGFAULT、横跨 38 个 scenario,而 build/unit/isplit/strict/gens 五步没有一步会看它。因此 p5e_gate.sh 新增 gpu 步(ctest -L integration-gpu,-j 4 约 10 分钟),作为集成分支的落地前必跑步骤,不下放到每个包(太慢,且包级改动的暴露面由 isplit 的两臂条目兜住)。这条同时是 ID-107 那个覆盖漏洞的结构性答案:漏的不是"某个用例没写",而是"整条运行时臂没有被门禁的条目"。
110 fix1 报告 §3.5 求裁的 kNoFrontendBuffer 座(buffer-texture 的后备存储):裁定改成自述式的传输测试,而不是留作"靠链条安全"。 该臂把 kNoFrontendBuffer 交给查找并依赖记录作答,其键是"已记下句柄"而非传输;它在 monolith 下不可达,只因为记下句柄的每一个写入点碰巧都被传输门住了——这正是让 monolith 臂丢掉 38 个 scenario 的那个形状(fix1 的根因就是一处把"记下了句柄"读成"记录臂会被选中",而这是两句不同的话)。新增 MGB_TEXTURE_RECORD_ARM_SELECTED() 把第二句话只说一次(Transport != Monolith 且 TextureResourceSubsystemEnabled(),即 §5.2/§5.8 里每一处可由空前端对象服务的读所共同依赖的两个合取项),RefuseNullFrontendTextureOffTheHandleArm 改为引用它(顺带去掉其内嵌 #if),texBufferByRecord 加为第三个合取项。有传输时该合取项已被 pushedStorage != nullptr 蕴含,故不改变任何行为、不关闭任何门;改的是"这条臂自己说出它依赖什么"。落地为 3cc4e1ec。
111 翻开关的那次提交必须同时修 ApplyOne 的 barriered 盖章,否则 Magma 当场死(scout 发现,已自查证实)。 kCapRunAheadApply 从不在 DirectVulkan 上发布(MGPipeTypes.h 的 MGPipeRunAheadCapBitsFor 对非 DirectGLES 返回 kCapNone),但 PipeApplier.cpp:1180-1182 的盖章只看 kMGPipeP5eClientWaitRuleLanded ? wireSaysBarriered : true,不看后端、不看能力位。而 draw_vbo / blit / clear / launch_grid 都是 kWaitNone。于是开关一翻,Magma 的服务端会把每条 draw 记录盖成"未设障",而它的客户端仍在 lockstep——CountBarrierPull(PipeInputs.cpp:95-97)对未设障记录是无条件 Fatal,不经任何旋钮。后果有二:(a) MagmaP7AllocatorDebtScope 的豁免被撤回,DirectVulkan.Split.NamedBlit.* 在普通 isplit 车道上就红;(b) Magma 的 *MGB_CTX->GetProgramForDraw() 变成首帧必死。裁定:盖章改为与服务端自身能力的合取,ServerPublishesRunAhead()(已存在于 PipeApplier.cpp:40-44,读本会话 CallMask,无掩码时答 false,正是 bring-up 窗口要的答案)。wireSaysBarriered 仍无条件计算(ID-103 的理由:谓词整阶段都在被执行)。红一次:RemoteClientTest 里用一个 CallMask 不含位 10 的会话过一条 DrawVbo,断言 MGPipeApplierCurrentRecordIsBarriered() 为真;去掉新合取项即变假。这是客户端闩的服务端镜像,也是把 ID-90 从散文变成代码的那一行。
112 EmittedCallSuppliesTheWholeField 是 Magma 的命根子,必须上编译期绊线。 任何退役 BARRIER-PULLED 读的包,都只能改 DirectGLES 的消费方,不得动 MG_Impl/Pipe/PipeFill.cpp:2499-2519:残余填充是 Magma 取这些字段的唯一来源,而 Magma 拿到就解引用。七行里有两行(GetBoundVertexArray、GetBufferBindingPoint)目前没有任何编译期保护,删掉它们会让 Magma 首帧空指针——是设备 SIGSEGV,不是构建失败。裁定:把 PipeFill.cpp:2667-2682 的 NoP4aFamilyFieldIsWhollySupplied 扩成覆盖全部七行(或补第二条具名 static_assert),约十行,把 vi/sb 最可能犯的错从设备崩溃变成构建断裂。这是本阶段性价比最高的一处预防性改动,随 wave 3 集成落地。
113 MG_Backend/DirectGLES/MultiDraw.cpp 归 mv 包,答 vi 留下的开放裁定 R2。BRIEF-P5E §2 的包划分里没有任何一包认领这个文件,于是 9 条 GetBoundVertexArray@DrawArrays(全部是 NumDraws != 1 的索引 multi-draw)无人负责。vi 当时报"该字段 14 → 0"并非谎报:在 vi 的头上这 9 条更早死在 fb 的 GetFramebufferBindingSlot@Clear 上,是 fb 与 tx2 落地才把它们暴露出来。
114 lockstep 之下"逐线程 CPU ms/帧"不再是首要指标(perf scout 的反对意见,接受):它把自旋等待算成工作,而等待账本给出的两个方程在代数上退化,解不出"有用工作"。裁定:在 run-ahead 落地之前,首要指标是扣除等待后的 CPU——用 simpleperf 取 Doorbell::Wait 在 GL 线程与 apply 线程上的包含式占比,再乘回逐线程 CPU ms/帧;MOBILEGL_IPC_SPIN_US=0 那一臂作为交叉校验(它把等待移出 CPU,其线程 CPU 是有用工作的上界)。两法相差 15% 以内才采信。run-ahead 落地后自旋消失,旧指标恢复为首要。另:配对 A/B 控制用 MOBILEGL_IPC_RUN_AHEAD=0,不得用 MOBILEGL_IPC_VERB_BARRIER=0——后者是 RunAheadArmed() 的第一个合取项,会把 run-ahead 顺手关掉,而且本轮实测它在几秒内即 Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@ClientWaitSync"},给不出任何性能数字。
115 strict 车道在宣布"硬绿"之前必须有阳性对照。 当前 7 条通过项里没有一条是"带记录的 split GL 场景":三条是 monolith 传输(strict 在那里结构性失效),两条自跳过,一条死亡测试,一条 Python 检查。也就是说今天的"绿"与"strict 根本没武装"在观测上不可区分——这正是 E1/E3(a) 对照存在的理由。裁定:硬绿的定义里加一条断言——至少一条会画东西的 inproc 条目跑完且服务端盖了 verb 边界、rsp = 0。没有这条,116/116 不算数。
116 白名单不是写出来的,是推出来的。 BRIEF §3.3 说的"§7 白名单"确实存在——在 MobileGL/MG_Remote/CONTRACT-P5E.md:475-486(BRIEF 的"§7"指的是契约,不是它自己),而且 ra 已经在 .github/workflows/test.yml:1207-1292 落了一份手抄的 13 行。裁定:手抄的那份删掉,改为生成。 判据是 ID-84 用现有表格的表述:一个 <field>@<verb> 对被采纳,当且仅当该字段行是 BARRIER_PULLED 且该 verb 的线上 op 是静态设障的(MGPipeWaitClassFor(op) != kWaitNone),再与 kMGPipeClassFieldMask[kMGPipeVerbClass[verb]] 取交。由 scripts/gen_pipe_field_ownership.py 生成 MGPipeBarrierPullAdmitted(field, verb)(一张 69 × 128 位的表,运行期零成本),并加 --print-admitted 让 shell 与 C++ 读同一张表。这样:退役一个 verb 的等待类会自动收窄白名单;新增一行 BARRIER_PULLED 只在设障 verb 上放宽;跨包漂移不可能。手抄那份在两个方向上都是错的:GetFramebufferBindingSlot@ReadPixels(21 条,最大的幸存者)本该采纳却不在表里;GetTextureObject@CopyImageSubData(6 条)在表里却不该采纳。
117 被采纳的 pull 是"响而不致命",这才让车道能真正变绿。 今天 CountBarrierPull(MG_Backend/MGPipe/PipeInputs.cpp:95-105)在 strict 下对每一条 barrier-pulled 读都 abort,不论是否在白名单里——所以 CI 里那段白名单比较是不可达代码(rc != 0 先退出),"硬绿"在旋钮不变的前提下根本不可能。裁定:给它第三态——未设障仍旧 Fatal(不动);设障且不被采纳仍旧 Fatal(不动);设障且被采纳则按 (field, verb) 去重记一条 MGPipe: Admitted{UnmigratedPipeInput, "<F>@<V>"} [BARRIER-PULLED, ADMITTED, retires in <phase>]。保留 <field>@<verb> 与 [BARRIER-PULLED, …] 的语法、只换头部标签,这样 P5c 以来写的每一条匹配 Fatal{UnmigratedPipeInput 的过滤器仍然精确地表示"红"。去重是必须的:一帧 852 次 draw 不能写 852 行。这正是 PipeInputs.cpp:61-65 那个 why 参数被引入的第三个理由。ID-81 无风险:CountBarrierPull 只在服务端盖了 verb 边界之后可达,而 monolith 臂从不盖章——今天那 3 条 PushMonolithArm 条目通过 strict 正是这个原因。
118 ResourceCopyRegion 的等待类要改,CopyImageSubData 才站得住。 车道采纳 GetTextureObject@CopyImageSubData,但 generated/PipeWire.inc:412 说该 op 是 kWaitNone;而 GetTextureObject 的退役相是 P7,不是 P5e。两者不能同时为真:翻开关之后,一条未设障的 copy 记录去读客户端内存,是无条件 Fatal,不经任何旋钮。裁定:把 ResourceCopyRegion 移到设障的等待类,而不是把字段塞进白名单当例外。理由:(a) 这正是 ID-84 说的"设障记录保留 P5C 语义",白名单随之自动采纳它,无需特例;(b) CopyImageSubData 不在逐帧热路径上(Minecraft 场景里每帧零次),一次等待的代价可忽略;(c) 反过来把字段特例化,等于在 P7 之前一直留一条"未设障却读客户端内存"的真实缺陷。
119 "rsp = 0 on unbarriered records"这条钉子按构造就是空的,措辞要改。 CountBarrierPull 的未设障臂在 ++g_residualPulls 之前、且是 [[noreturn]],所以未设障的 pull 永远不会加进 rsp——它直接 abort。于是 rsp 只数设障的 pull,那句话无论代码怎么写都成立(契约 CONTRACT-P5E.md:484 的论证"非零就说明有一次没触发的 Fatal"因此不成立)。而且翻开关之后,readback 与 XFB 场景合法地会有 rsp > 0,CI 现在那条"任何非零 rsp 都告警"会在正确行为上一直响。裁定:钉子改写为"rsp 只数被采纳的设障 pull,且必须等于被采纳标记的计数",并按设备证据的形式断言 rsp 与 draws 的关系而不是与 0 的关系——Redmi 上 inproc 的 rsp 恰等于 draw 数(~852)、monolith 为 0,所以 draw 路径退役的可观测形态是"rsp 不再随 draws 成比例"。
120 契约 §8 修正 8 未落地,以代码为准并在契约里标注。 CONTRACT-P5E.md:503-506 说 GetBufferBindingPointCount 的 sticky forward 在 split 下变 FATAL,但 FieldOwnership.def:147(字段行)与 :179(forward 行)都仍是 BARRIER_PULLED,而生成器拒绝字段/forward 不一致的组合(FieldOwnershipTest.TheSevenStickyForwardsAgreeWithTheirFieldRows 钉住)。按写的落地会要么绊生成器、要么连字段行一起改,并改动推导集里 10 个对。裁定:本阶段不落地该修正;契约里就地标注"UNLANDED,随 P7/P13 落地",代码不动。理由与 ID-105 同源:契约里算术上做不到的话,改契约而不是硬塞代码。
121 BRIEF §3 item 5 的证据形式在当前头上不可执行,改写并重新排序。 它要求"revert 任意一条 handle 臂,strict 车道按 field 与 verb 变红"。但 116 条里今天只有 7 条 strict-绿,而且没有一条会画东西(三条 monolith PushMonolithArm、两条自跳过的 PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares、一条死亡测试、一条 SplitLogPaths 元数据)。于是每一个候选 revert 在 revert 之前就已经是红的——pg 与 fb 的报告早就撞上了这一点("标记不在任何一张表里"、"隔离是按帧的")。裁定:item 5 不是与 item 3 并行的,而是被它门住;证据形式改为"在车道已绿之后,revert 使标记表恰好多出那一个具名 <field>@<verb> 对,且别的对不动"。这也顺带给了 ID-115 的阳性对照一个用途:没有阳性对照,"恰好多出一个"是不可判定的。
122 scripts/ci/split_negative_controls.sh 的 E1 对照今天就是坏的,必须修或重写,不得当作已有证据。 在门禁树上跑两次都自检失败(E1 control: 6/8 selected entries did not fail),而且它索要的 Fatal{BarrierViolation} 那行在 MOBILEGL_IPC_BATCH_WAITS 的默认值 1 下根本不可能触发。一个永远打不出所要求证据的对照,比没有对照更坏:它会在评审里被当成绿的。裁定:随 wave 3 集成一并修——要么把控制改成在 BATCH_WAITS=0 下跑(那是它隐含假设的世界),要么把它索要的 Fatal 换成在默认配置下真能触发的那一条,并在脚本头部写清它证伪的是什么。修不动就删掉并在报告里具名说明,不许留着。
123 G1(pull 构建符号恒等)本阶段的门禁跑不了,如实记账而不是假装跑过。 p5e_gate.sh 只配置 build-split 一种 flavour,没有 pull 也没有 push;fb 与 fix1 都各自报告过这一点。裁定:(a) 本阶段 G1 以 CI 为准,包的报告里写"未在本地验证,理由:门禁只配 split flavour",不得写成"G1 绿";(b) 集成提交之前,由集成者跑一次 -DMOBILEGL_PIPE_PUSH=OFF 的 configure + build 作为一次性核对,把结果记进集成报告。理由与 ID-108 同源:没跑过的东西不许写成跑过了。
124 四个 flavour 里只有 split 编得过;pull 与 push 在 7c6f6886 上都是坏的。 这条经历了两次自我更正,过程本身就是结论。第一次:我加了 p5e_gate.sh flavours 步,pull 臂报 rc=0 / 982 TU——假绿。CMakeLists.txt:496-500 规定 MOBILEGL_BUILD_DISAGGREGATED=ON 强制 MOBILEGL_PIPE_PUSH=ON,所以 -DMOBILEGL_PIPE_PUSH=OFF 被静默覆盖,那一臂其实是 split 构建的第二份拷贝;而 CMakeCache 里那行仍然写着 OFF,所以缓存不是证据。第二次:改成从 compile_commands.json 断言实际生效的宏、不符即失败后,才拿到真数字。根本不存在"disaggregated 的 pull"这个 flavour。 实测(均已断言生效宏):pull(DISAGG=OFF, PUSH=OFF,push=absent)rc=1,743 TU;push(DISAGG=OFF, PUSH=ON,push=1)rc=1,419 TU。四处编译错误、三个站点,共同的病因是"被 MOBILEGL_BUILD_DISAGGREGATED 守着的东西,被没守的代码用了":(a) DirectGLES.cpp:6716 的 keysMatch(声明在 #if MOBILEGL_BUILD_DISAGGREGATED 内,三目的 : 分支与使用点在 #endif 外)——两个 flavour 都中;(b) DirectGLES.cpp:5575 无保护地调用只在 #if MOBILEGL_PIPE_PUSH 里声明的 ComputeShaderStorageBlockBindingSignatureOf——pull 中;(c) Managers.cpp:9081 与 :9344 用 BufferImpl::StagedTextureTargetForPipeTarget,DISAGG=OFF 时不存在——push 中。裁定:(1) 三处由集成者在 wave 3 集成提交里修,排在 pa 合并之后(keysMatch 的修法必须保持惰性求值:现在的三目在 handle 臂上不会求值 legacy 分支,急切求值会在 pa 之后解引用空的 currentProgram,那正是 ID-107 那一类缺陷);(2) flavours 步进入集成分支必跑集合,且必须保留生效宏断言——没有它这一步会永远报绿;(3) 在此之前包报告里写"G1 绿"不成立(ID-123)。这与 ID-107 是同一个病的两种时态:四个 flavour 里只有一个在被编译、两条运行时臂里只有一条在门禁集合里;而且两次我都是先拿到一个"绿"、再发现那个绿是另一条臂冒充的。规则:一条臂的证据必须包含"它确实是那条臂"的证明。
125 ID-116 的采纳规则要补第二个析取项,否则会在一条本阶段无权触碰的行上把车道判红。 pa 退役了逐 draw 的 program 拉取之后,第一故障被揭开,露出 GetTransformFeedbackProgram@DrawArrays(11 条)。按静态规则它不被采纳(DrawVbo 是 kWaitNone),但契约 §5.7 把 transform feedback 整个排除在 P5e 之外,FieldOwnership.def 给该字段的退役相是"P3b/P4b (Espryt), P7 (Magma)"。裁定:设障记录上的 pull,满足以下任一即采纳——(1) 该 verb 的线上 op 静态设障(ID-116 原样);或 (2) 该字段的退役相不指向本阶段。未设障臂不动,仍是先行的 [[noreturn]]。析取项 (2) 才是 ID-84 的诚实表述:设障记录的 pull 按构造就是合法的,剩下的唯一问题是"P5e 是否还欠这次迁移",而退役相那一列正是这个问题的答案——数据早就在生成器读的表里,只多读一列。核对:应采纳 GetFramebufferBindingSlot@ReadPixels(1)、GetTextureUnitObject@CopyTexImage2D(1)、GetTransformFeedbackProgram@DrawArrays(2)、GetTextureObject@CopyImageSubData(2,且 ID-118 之后 (1) 也成立)、ValidateProgramName@ShaderStorageBlockBinding(2)、DirectVulkan 的 GetFramebufferBindingSlot@Clear(2);应拒绝 GetProgramForDraw@DrawArrays 与 GetBoundVertexArray@DrawArrays。
126 pa 的臂选择是合取 ProgramHandleArm() && TextureImpl::UnitTexturesByHandle(),接受,并具名说明它的含义。 后果是"program 位开、texture 位关"的混合 A/B 仍会拉取。这是对的而不是将就:被提升的那个值服务四个消费方,臂必须点到每一个,而 ResolveAndBindUnitTextures 的 legacy 臂(sampledTargetForUnit,DirectGLES.cpp:6175)确实读真实对象。两个子系统位是各自独立的 A/B 控制,混合档是诊断用的臂、不是发布配置;为一个没人发布的配置去给 sampledTargetForUnit 加归档臂(tx2 的活)不划算。若将来有人要发布混合档,先补那个臂再改这里的合取,不得反过来。
127 我自己又踩了 ID-108(c):在 pa 的 gpu 跑步途中改了 p5e_gate.sh,它的驱动打出了解析错误。 ctest 结果未受影响(改动在另一个 case 分支里),但证据被搅浑了,而这正是本轮我反复写在裁定里的那条纪律。裁定:(1) wave 3 剩余时间内该脚本冻结,不再编辑;(2) 更根本的修法,留给下一阶段的工装:包的门禁调用应当先把驱动脚本复制到自己的 .p5e/ 下再执行,这样集成者的编辑与包的执行在时间上解耦——run_arm_frozen.sh 在设备侧已经是这个模式(ID-108(c) 的第一次),只是没有推广到主机侧。
128 采纳规则的第三个析取项:按升级设障的记录。 合并 pa + mv 后在全部 180 条上重跑修正过的逐条普查(每条用它自己的命令与环境,取自 ctest --show-only=json-v1,只在最上层叠加 strict 旋钮与私有日志路径),GetProgramForDraw@DrawArrays 已归零,剩下八行里有七行被 ID-116/ID-125 的两个析取项覆盖,只剩 2 条无人认领:两条都是 ClientVertexArrayScenario.*,即客户端顶点数组。ID-82 把它整个排除在 P5e 之外(run-ahead 下客户端具名拒绝,staging 归 P8),读点 SyncClientSideVertexArraysForDrawArrays(DirectGLES.cpp:2223)自己在被未设障应用时就 Fatal{RoleViolation, "MGPipeSlots"}。所以这次 pull 合法——但析取 1 不成立(DrawVbo 是 kWaitNone),析取 2 也不成立(GetBoundVertexArray 的退役相确实指向 P5e,对普通 draw 路径而言这是对的,而那条路径 vi 与 mv 已经退役了)。裁定:补第三个析取项——记录是按升级而非按静态类设障的,即 wireSaysBarriered(op, payload, st) && MGPipeWaitClassFor(op) == kWaitNone。两半都已存在:ApplyOne 本来就无条件计算 wireSaysBarriered(ID-103 的理由),静态类就是生成器读的那张表;所以只是在现有 barriered 盖章旁边多盖一个位,不是新管线。为什么这是原则性的而不是打补丁:按 ID-83,升级只发生在 XFB 活跃与客户端数组两种 draw 上,而契约把两者都放在 P5e 之外。它同时也是 GetTransformFeedbackProgram@DrawArrays 更真实的理由(目前由析取 2 按相位采纳)——两者都成立之后,保留析取 2,不要替换。"硬绿"的可检查定义:三个析取项 + ID-117 的"响而不致命"落地后,strict 车道应报 180/180 通过,八行为 Admitted{、Fatal{ 为零。达不到就报具体剩余标记,不许为了凑数改规则。
129 "没有 stamp 行的 verb 什么都不采纳"——接受,这是 gl 自己的判断而不是 ID-125 说过的,但它是对的,而且是差 174 对与 491 对的那一刀。 理由:kMGPipeClassFieldMask[verbClass] 是该 verb 的 may-read 表;一个不在自己 verb 类 may-read 掩码里的字段,客户端的残余填充从来没有拷贝过它,所以服务端去读它拿到的是上一条 verb 的值——那正是世代毒化机制存在要抓的陈旧读,与是否设障无关。ID-116 原本就写了"再与 kMGPipeClassFieldMask 取交",gl 的做法是它的忠实实现。两个集合之间取小的那个:采纳集合宁可窄,窄了会多红一条要人解释,宽了会放过真缺陷。
130 "硬绿"的数字按 gl 的车道切分重述:不是"一条标签上 180/180"。 item 7 把两条 DirectVulkan 条目移到自己的标签上,所以判据是两句:(a) Espryt 车道全绿、Fatal{ 为零、Admitted{ 全部与生成表对得上、棘轮两侧都不动;(b) Magma 车道在 strict 下预期红且其标记被具名断言(契约 §7 早就是这么写的,只是没有车道)。合并后的具体条数以集成树实测为准,不预先钉死——ID-128 里"180/180"那个数字作废,判据以本条为准。为什么值得改:把两条 Magma 条目留在 Espryt 的白名单里,会让"Espryt 的白名单"变成一句关于两个后端的话,而 §7 说的不是那个意思。
131 ctest -L 是正则,不是相等——记一条通用规则。 gl 发现我在 brief 里指定的标签 integration-split-magma 仍然会被 -L integration-split 匹配到,也就是那两条条目根本没离开 Espryt 车道;它实测后改名为 integration-magma-split。规则:新标签不得以既有标签为前缀,且新增标签后必须实测 ctest -N -L <旧标签> 的条数没有把新条目算进去。这与 ID-124 的"一条臂的证据必须自证它确实是那条臂"是同一条:过滤器写对了不等于过滤器选中了你以为的集合,要数一遍。
132 strict 车道硬绿是必要条件,不是充分条件——而且是结构性的,不是这次没测到。 把两个常量翻开之后(p5e/flip @ 985ae62d),unit 2254/2254、gens、flavours 全绿,但 isplit 掉到 112/181、gpu 掉到 1290/1359,而且是 flaky 的(-j 4 红 71 条、-j 1 红 68 条,单跑一条先过后崩)。全量普查:70 条红、17 个不同的 <field>@<verb> 对、只涉及四个字段(GetRenderStateParametersVersion 27、GetTextureContextId 25、GetBufferBindingSlot 16、IsCapabilityEnabled 2),横跨十一个 verb,其中包含 ReadPixels、GetSyncStatus 这些本身就设障的。为什么 strict 不可能抓到它:strict 跑在 lockstep 下,ApplyOne 把每条记录都盖成 barriered,所以任何"只在客户端不再等待时才发生"的事,它按构造就看不见。strict 量的是剩余欠债,不是 run-ahead 的正确性。裁定:(1) 阶段出口判据里必须加一条独立的"翻开开关后的车道",不得用 strict 硬绿代替;(2) 一个字段在一个 verb 上稳定失败是漏迁移,四个字段散在十一个 verb 上且 flaky 是竞态——按后者查,不按前者查;(3) 首要嫌疑是 gPipeInputs 这块共享可变内存的互斥本来就是 lockstep 本身:run-ahead 下客户端仍为设障 verb 填充它(PipeFill.cpp:3219,fillOwed = barriered),而 apply 线程可能仍在排空更早的未设障记录并读同一块。这与项目自己早就写下的结论一致——P5d 的研究结论就是"退役这道障需要 gPipeInputs 版本化/双缓冲",并已排到 P11(ROADMAP 的 P11 行)。若查实如此,"run-ahead 不能在 P11 之前落地"是一个完整且可接受的结论,交由 ra2 包带证据回答。
133 间接 multi-draw 的等待类升级,取 ID-118 的先例。 ra2 留的 18 条红全是 GetBufferBindingSlot@DrawArrays [UNBARRIERED],落在 mv 为强制两个间接档位而注册的两条车道上;它犹豫的理由是"间接 draw 是热路径"。这个理由在这里不成立:MOBILEGL_ESPRYT_MULTIDRAW_MODE 默认 auto,而 mv 实测 auto 在本驱动上解析为 ext(它把档位日志提到 INFO 扫了全部六档,每档都解析为自己)。所以间接档是 opt-in,升级它们的等待类对发布路径零成本;非默认档上正确性优先于速度,GetBufferBindingSlot 由 P8 正式退役。
134 两条 ClientVertexArrayScenario 单独成车道并转为"预期红"。 它们不可能绿、也不该绿:ID-82 规定 run-ahead 下客户端内存顶点数组是具名拒绝,所以中止就是设计行为,一条要求它们绿的车道等于在要求那条拒绝不要触发。形制照 gl 的 Magma 切分(其 item 7):自有标签 + 断言具体标记而不是"容忍失败"的预期红步 + 从必须全绿的车道里移除。命名遵 ID-131:ctest -L 是正则,新标签不得被 -L integration-split 匹配到,且要用 ctest -N -L integration-split 数出来正好少两条来证明。
135 ID-132 的自我更正:我点对了对象,点错了机制。 我当时断言 gPipeInputs 的字段值竞态,并据此怀疑要等 P11 的版本化。ra2 实测证明:字段值已经由 SuppliedFieldMask 分区、不竞态;真正竞态的是元数据标量(CurrentVerbSerial、verb 边界标志、m_currentVerb)。而且真正让它不可见的是守卫本身:RefusePipeInputsTouchWhileApplierOwnsIt 的 run-ahead 臂对任何 isBarrieredFill 一律豁免,豁免的依据是一句关于未来的断言("客户端马上就要 park"),而实际顺序是 fill → emit → park。修法是把那句断言变成事实(写块之前强制等待)并让守卫去测事实。第二个缺陷同源:MGP_VERB_OP_LIST 把整个 draw 家族并到一行,于是十九个索引 draw verb 都答"设障"、都去填充、都不 park。记这条的原因:ID-132 的怀疑方向有用,结论不可信;下一个读到 ID-132 的人必须知道它哪半截经受住了证据。P11 的行仍然成立(一般情形),其最小充分形式见 ra2 报告 §5——特别是"只加 per-role 盖章而不做值版本化会比现状更糟"那一段。
136 ID-133 的前提被实测推翻两处,撤回其中的升级,改走 ra2 提的更好解法;并把 MultiDraw.cpp 这一处越界授权给 ra2。 (1) 根本不存在"间接 multi-draw 的 op":二十个 draw 入口全部塌缩到一条 draw_vbo,档位是服务端按批次解析的,所以升级只能键在 NumDraws > 1 && !kDrawIsIndirect 上——代价并不局限在 opt-in 车道,普通 glMultiDraw* 在默认的 ext 档上也开始等了。那是在手机实际发布的那条臂上、为满足一条车道而引入的逐 multi-draw 会合,正是本阶段要消灭的东西,不能作为终局。(2) 那次 pull 是 BoundDrawIndirectBufferId(MultiDraw.cpp:87)在档位自己的 scratch buffer 周围保存/恢复一个 GL 绑定名,不是数据依赖,所以可以直接退役——给它邻居 ResolveBoundIndexBuffer 已有的 handle 臂即可。裁定:退役该 pull 并同时撤回升级 (iii),两半必须同一提交(只做一半的任一中间状态都不是我愿意 gate 或测量的状态);为此把 ID-113 划给 mv 的 MultiDraw.cpp 就这一处授权给 ra2,提交里具名说明,以免边界被误读为漂移。留给全阶段的规则:为了让一条车道变绿而加的等待,是真实路径上的真实代价;对任何升级要问的是**"哪条臂在为它买单"**,而不是"哪条车道绿了"——ID-133 我没问这句,ra2 问了。