[MG_Remote] (P6/lk): regroup RingControl by writer, and make the asserts that pin it falsifiable

The shared control page both roles map had a mixed-writer cache line:
{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial, presentAckSerial}
sat together, but submittedSeq is producer-written (Ring.h's "THE FIVE
WATERMARKS") while the other four are consumer-written. That is why no
single-writer progress block could be aliased over it, and it is what this
package fixes: submittedSeq moves up beside cmdHead, the other thing the
producer writes, and the four that remain become a named LinkProgress member -
the struct c6 declared in ILink.h - so a data plane with no shared page can own
the same four values and every wait predicate in SessionRings.h keeps ONE
implementation at source level.

The two event flags deliberately stay in the doorbell group. Promoting them
would put the client's per-drain exchange(0) on eventRingFull (EventRing.h:200)
onto the line the client's own spin predicate reads 1e5-1e6 times a frame.

73 call sites rewritten, 15 in the library and 58 in tests. The rewrite is
receiver-scoped rather than field-scoped, because SplitRuntimePeek.h:77 has its
own struct with a field of the same name and SplitRuntimePeek.cpp:146 has both
on one line - a field-scoped rename would have silently broken the peek struct
that MagmaRunAheadScenario reads nine times.

THE ASSERTS ARE THE PART THAT NEEDED A SECOND PASS. The first revision shipped
four aggregate asserts and an adversarial review broke them by counterexample:
swapping retiredSeq and completedFrameSerial inside LinkProgress compiled
clean, passed all four, and passed the runtime layout test too - because
appliedSeq stays at offset 0. Three wire-visible offsets on a shared page were
pinned by nothing. Nothing else in the tree covers it either: the ABI
fingerprint takes no input from this page, and sizeof(RingControl) is 4096
whatever the members do, because alignas(4096) makes it so.

So the block is now per-field as CONTRACT-P6 §8.3 actually asked, plus a
standard-layout precondition, a tail bound that alignas cannot satisfy for us,
and the LinkEventFlags mirror. Each was run red: the field swap reds the
per-field asserts by name, a fifth member inside LinkProgress reds sizeof.

One of them cannot go red, and Ring.h now says so rather than implying
otherwise. A SMALL member inserted between Progress and serverEpoch lands at
offset 288, inside Progress's own line, and serverEpoch's alignas(64) absorbs
up to 32 bytes of it - so both line asserts still pass. The review's proposed
fix does not catch it either; I tried it. A gate nobody can falsify is worse
than no gate, so the limit is written down beside the asserts.

Also: RingTest.ControlPageLayoutIsTheSharedContract said nothing about the one
field this package moved, and now asserts the producer/consumer separation and
the event flags' exclusion itself rather than borrowing the claim from Ring.h.
Ring.h grew 30 lines, so the Ring.h:NN citations this package and c6 authored
are re-anchored to named blocks instead of line numbers. ARCHITECTURE.md §11.2
is updated for the new layout, and a factual error introduced in the previous
revision of that paragraph - submittedSeq annotated as releasing staging, which
retiredSeq does - is corrected.

Gates: pull 0 added / 0 removed, .text size 10822147 unchanged; split 0/0,
.text size 11681395 unchanged; unit suite 2326/2328 with the same two
pre-existing environmental failures as the baseline. The five edits behind
MGITEST_SPLIT_RUNTIME_PEEK were proven compiled by a red-once, not assumed.

Note on the .text claim: that is a SIZE, not the bytes - field offsets moved,
so immediates moved with them. And this page's layout IS observable across
binaries; what is unobservable is behaviour within one binary. The contract
already books the layout change deliberately, before spawn exists to care.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 09:23:48 -04:00
co-authored by Claude Opus 5
parent a54ae47ab9
commit f4cbaa8945
17 changed files with 250 additions and 97 deletions
+1 -1
View File
@@ -375,7 +375,7 @@ P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL
### 11.2 `RingControl`(`Ring.h`)
一页 4 KiB,每个争用组各占一条 cache line:`SEG_CMD` 游标三元组 `cmdHead / cmdAppliedTail / cmdRetiredTail`;`SEG_STAGE` 独立三元组;三个水位 `appliedSeq`(释放 `*AppliedTail`)/ `submittedSeq`(释放 staging)/ `retiredSeq` + `completedFrameSerial`(释放 `*RetiredTail` 与 `SEG_ADOPT`)+ `presentAckSerial`;`serverEpoch`、`ringGeneration`、`consumerParked`/`producerParked`、`eventRingFull`、`eventDropped`。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次。游标是单调字节计数、2 的幂掩码、永不重置。
一页 4 KiB,每个争用组各占一条 cache line。**P6 包 `lk` 按写者重组过这一页**(`CONTRACT-P6.md` §8.3):producer 线 `cmdHead` + `submittedSeq`(producer 写、诊断量,`Ring.h` 的 "THE FIVE WATERMARKS" 段写明无人等待它);consumer 线 `cmdAppliedTail / cmdRetiredTail`;`SEG_STAGE` 独立三元组(P5 起故意全零,见 `Ring.h`);**四个 consumer 写的水位收成具名成员 `Progress`**(`Transport::LinkProgress`,`ILink.h`)——`appliedSeq`(释放 `*AppliedTail`)/ `retiredSeq`(释放 staging)/ `completedFrameSerial`(释放 `*RetiredTail` 与 `SEG_ADOPT`)/ `presentAckSerial`,访问写作 `control.Progress.appliedSeq`;最后是 `serverEpoch`、`ringGeneration`、`consumerParked`/`producerParked`、`eventRingFull`、`eventDropped`。**两个 event 标志刻意留在最后这一组**:`eventRingFull` 有两个写者(server `store(1)`、client 每次 drain 一次 `exchange(0)`),放进 `Progress` 会把 client 的 RMW 落到 client 自己自旋读的那条线上。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次。游标是单调字节计数、2 的幂掩码、永不重置。
记录头 `RingRecordHeader{kind, flags, size}`,kind 0 保留给 wrap 填充;`RingProducer::Reserve` 在记录会跨 wrap 边界时自动发 pad 记录;`RingConsumer::Pop` 拒绝不可能的头并置 corrupt → `Fatal{ProtocolCorruption}`;`HardDrainRing` 只在两侧静默且 ring 全空时 bump generation。
@@ -53,3 +53,52 @@
| `MG_Test/Wire/SessionTest.cpp` | test | 1 |
**合计 41 处**(传输内 14 / 生产 9 / 测试 18)。
---
## C 实际结果(重组落地后补记)
两个口径在本文里曾经打架:表 B 的 41(受体限定为 `control|m_control|ctrl`)与
`a54ae47a` 提交信息里的 44(受体不限)。**以表 B 的口径为准**,差额是三处受体不是
控制页对象的匹配;而实际改写时受体集合还要再宽一档,因为测试里普遍写作
`session.Control()->appliedSeq` / `owner.CmdControl()->appliedSeq`。
最终改写:**库 15 处 + 测试 58 处 = 73 处**(按 `re.subn` 计,一行可含多处)。
**"开集成测试构建"的真正理由不是计数,是守卫**:`MG_IntegrationTest/Harness/SplitRuntimePeek.cpp`
的五处改写躲在 `#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)` 后面。
本轮用 red-once 证明了它确实在编译——往守卫体内塞一个未声明标识符,构建变红:
```
SplitRuntimePeek.cpp:99:13: error: use of undeclared identifier 'MGITEST_DELIBERATE_RED_ONCE_PROBE'
```
**差点踩坑的一处**:`SplitRuntimePeek.h:77` 另有一个结构体同样有 `appliedSeq` 字段,
而 `SplitRuntimePeek.cpp:146` 一行里两个都在——`state.appliedSeq = control->appliedSeq.load(...)`。
不限定受体的改写会静默改坏左边,`MagmaRunAheadScenario.cpp` 的九处访问的正是它(那个文件一行未动)。
## D 验收
| 项 | 结果 |
|---|---|
| pull 构建(G1) | 符号 0 增 / 0 删,`.text` 10822147 不变 |
| split 构建 | 符号 0 增 / 0 删,`.text` 11681395 不变 |
| 单元测试 | 2326 / 2328,与基线同样的两个环境性失败 |
| 新增静态断言 | 逐条 red-once(下表) |
**口径说明**:上面的 `.text` 是**尺寸**不是字节。字段偏移变了,所以指令里的立即数变了,
但代码尺寸没变。另外这套数字用的是 clang 22.1.6 且关闭 test/benchmark,**与路线图记录的
P5f 基线(`.text 10806051`)不可比**,只能做"本包前 vs 本包后、同工具链"的比较。
### 断言的 red-once
| 扰动 | 结果 |
|---|---|
| 交换 `LinkProgress` 里 `retiredSeq` 与 `completedFrameSerial` | 红:`LinkProgress field order: retiredSeq` / `completedFrameSerial` |
| 在 `LinkProgress` 内增加第五个字段 | 红:`sizeof(LinkProgress) == 32` |
| **在 `Progress` 与 `serverEpoch` 之间塞一个小成员** | **不红——已具名记在 `Ring.h` 里** |
最后一行是本轮审查最有价值的产出。`serverEpoch` 是 `alignas(64)` 且紧随 `Progress` 声明,
所以编译器总把它放在下一个 64 倍数处:**一个塞在 288 的小成员会落在 `Progress` 自己那条线上,
而两个断言照样通过**,最多能藏 32 字节。审查建议的修法(`serverEpoch == Progress + 64`)
也抓不住——实测过。`Ring.h` 的注释现在写明了这一点,因为**一道没人能证伪的门比没有门更糟**。