CONTRACT-P6 §8.5 requires lk to publish its blast radius before it starts, because an earlier draft's ratchet baseline was wrong by a factor of three and a ratchet installed on the wrong number is red on its first CI run for reasons that have nothing to do with a leak. The itemisation is mechanical, per file, with the grep spelling stated: 250 seam-identifier references outside Transport/ across 23 files (127 production, 123 test), and 44 member-access sites that the regroup itself must rewrite, across 9 files - 17 of them in MG_IntegrationTest, which is why lk builds with the integration tests ON. The earlier build did not, and a missed edit there would have been green. THE CORRECTION. Measuring that radius turned up a mistake in c6, landed two commits ago. §8.3 and ILink.h both said lk should promote eventRingFull and eventDropped into the watermark group and called the result single-writer. eventRingFull has TWO writers: the server latches it with store(1) when SEG_EVENT fills (EventRing.h:140) and the CLIENT clears it with an exchange(0) on EVERY DRAIN (:200). Promoting it would put a per-drain read-modify-write by the client on the same cache line as appliedSeq - which the server release-stores every 64 records and the client's spin predicate reads 1e5-1e6 times a frame. That is new false sharing on the hottest line in the system, and it would land in exactly the numbers this package's gate has to hold constant. It is also the same class of error the earlier draft made with submittedSeq, one field over. So LinkProgress is now the four consumer-written watermarks and nothing else - genuinely single-writer - and the two flags stay in the doorbell/generation group, where mixed writers already live beside consumerParked/producerParked, crossing the seam through a separate EventFlags() accessor on its own line. The contract records the correction and its reasoning rather than quietly restating the rule. ALSO CORRECTED, from running the baseline suite: a6's link-experiment report said the silently-empty build stamp had "no log and no gate to say so". That is false. SessionHandshakeTest.cpp:140 asserts BuildStamp[0] != '\0' and :180 asserts that perturbing it changes the fingerprint, and BOTH GO RED in this configuration right now - the worktree's .git holds a Windows path WSL's git cannot resolve, so GIT_COMMIT_HASH_SHORT is "". The finding is therefore stronger, not weaker: the gate that catches it already exists and is already red. What is silent is the BUILD, and hs owes a named configure-time failure rather than a new gate. Baseline for lk, recorded: 3878 tests, two failures, both environmental and understood - the fingerprint one above, and PipeCatalogue.FrontendNeverTakes- AnApplierAddress, which walks up from cwd looking for MobileGL/MG_Impl and so fails for any out-of-source build directory. No behaviour change. The headers are still included by nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
a6 第 5 项:server 能否不链 MG_Impl——实验结果
头
b3b9c2ee。P6-SPAWN-PLAN.md§5 的 a6 第 5 项、P6-CONTRACT-DRAFT.md§3.3 都把这条挂着;MG_Backend/MGPipe/PipeInputs.h与P5F-WIRE-COMPLETENESS.md§1.4 断言它成立。 三个阶段没人试过。这次试了。原始数据(184 个符号逐个按定义文件分组)见
a6-link-experiment-data.md。
结论
不能。 今天没有任何一种配置能让 server 镜像不链 MG_State / MG_Impl / MG_Remote/Client。
而且这个问题在今天根本不是一个链接行能回答的问题,这是第一个发现。
1 结构性发现:构建系统里一道缝都没有
CMakeLists.txt 里 SOURCE_FILES 是一张平的源文件列表(211 个 .cpp),
add_library(MobileGL SHARED ${SOURCE_FILES})(:695)与
add_library(MobileGL_s STATIC ${SOURCE_FILES})(:762)都从它构建。
MG_Impl / MG_State / MG_Backend / MG_Pipe / MG_Remote 之间没有任何 target 边界——
没有 object library、没有 static lib、没有 interface target。
唯一叫 MobileGLServer 的 target(:926)是 P0 spike 桩:Android-only、默认 OFF、
只编 tools/spikes/server_stub/main.cpp,一个 MobileGL 库都不链。
mobilegl_server_main 这个符号在整棵树上不存在,只存在于 ARCHITECTURE.md:488-496 的描述里。
所以"改链接行试一次"这个做法本身不成立:没有可分的 target。
c6必须把"建立模块 target 边界"当成一件有成本的具名工作,而不是sm包里顺手的一行。
2 方法(保持 a6 只读)
不加 target、不改一行源码。构建 MobileGL 单个 target,然后对逐个 object 文件做符号闭包:
SERVER 集 = MG_Backend/** + MG_Pipe/** + MG_Remote/{Transport,Protocol,Wire,Server}/** + CapsCodec.o (45 个 object)
FRONTEND 集 = MG_Impl/** + MG_State/** + MG_Remote/Client/** (71 个 object)
SHARED 集 = MG_Util/** + Init/GlobalObjects/ConfigLoader (87 个 object)
取 SERVER 集的未定义符号,减去 SERVER 自己定义的,减去 SHARED 定义的, 与 FRONTEND 定义的取交集——这就是一次真链接会报出来的那份未定义符号表。
构建:clang 22.1.6、Release、DISAGGREGATED=ON、DISAGGREGATED_INPROC=ON、PIPE_PUSH=ON。
3 数字
| SERVER 集未定义符号总数 | 1531 |
| 其中 SERVER + SHARED 自己满足不了的 | 552 |
| 其中由 FRONTEND 定义的 | 184 |
184 个里:
| 类别 | 数量 |
|---|---|
| 真函数调用 | 177 |
| 析构函数 | 4 |
| typeinfo | 2 |
| vtable | 1 |
耦合是行为性的,不是类型性的。 不是"server 持有一个 SharedPtr<ITextureObject> 所以需要 vtable",
是 server 在调用前端对象的方法。
3.1 按定义方
| 定义模块 | 符号数 |
|---|---|
MG_State/GLState |
157 |
MG_Impl/Pipe(PipeFill、SlotAllocator) |
16 |
MG_Remote/Client |
9 |
MG_Impl/GLImpl |
2 |
前五个文件:TextureObject.cpp 59、SamplerObject.cpp 30、BufferObject.cpp 18、
FramebufferObject.cpp 16、PipeFill.cpp 9。
3.2 按引用方——债主是 P7
| 引用方 | 符号引用数 |
|---|---|
DirectVulkan(Magma) |
227 |
DirectGLES(Espryt) |
111 |
MG_Remote/{Wire,Server} + MG_Pipe |
6 |
去重后按后端归属:
| 符号数 | |
|---|---|
| 只有 Magma 要 | 101 |
| 两个后端都要 | 60 |
| 只有 Espryt 要 | 14 |
单个最大引用方是 DirectVulkan/Renderer/UniformManager.cpp(90),其次
VulkanRenderer.cpp(60)、DirectGLES/Managers.cpp(53)、DirectGLES/DirectGLES.cpp(51)。
这份分布精确地落在路线图已经说没做完的两个阶段上:P7(Magma 全量迁移)和 P3b/P4b(Espryt 深化)。 链接耦合不是一笔新债,它是那两笔旧债在链接器上的影子。
4 这和 "P5c/P5f 已经把跨角色读归零" 矛盾吗?不矛盾
两句话都成立,因为它们量的不是一回事:
- P5c / P5f 关掉的是运行时跨角色内存访问:
rsp逐帧为 0、BARRIER_PULLED分类为 0。 - 这次量的是链接时符号引用。
但这也不只是"死臂的链接残留"。取样核实过一个站点:
DirectVulkan/Renderer/UniformManager.cpp:1039 一带是
const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject();
const auto* effectiveSampler = samplerOverride ? samplerOverride.get()
: texture->GetSamplerObject().get();
…effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear…
MGB_CTX 在 PIPE_PUSH 下是 &gPipeInputs,所以这是 PipeInputs::GetTextureUnitObject——
而它在 MG_Pipe/FieldOwnership.def:33 被分类为 FATAL:
X(GetTextureUnitObject, FATAL, "-", "server reads sampler/view records or verb texture handles; never a client TextureUnit")
也就是说:split 下走到这里是一条具名 Fatal,不是一次错误的读。P5f 的归零是真的。 但"具名拒绝"意味着这条功能没有迁移,代码还在、符号还在、链接还需要它。
所以准确的说法是:
184 个符号是 P5f 那 25 条 FATAL 分类 + 未迁移的对象类字段在链接层面的残留。 运行时是干净的(要么走记录臂,要么 Fatal);链接时不是,而且链接时是 server 镜像能不能存在的那个判据。
5 传输层自己的 6 个——单独拎出来
其余 178 个是后端的债,这 6 个不是,它们在 MG_Remote / MG_Pipe 内部:
| 引用方 | 需要的符号 | 说明 |
|---|---|---|
Server/PipeApplier.cpp |
Client::ClientSession::NoteApplyThreadEnteredApplier() / NoteApplyThreadLeftApplier() |
server 侧 applier 在调 client 的会话对象。inproc 下同进程所以成立;spawn 的 server 进程里根本没有 ClientSession |
Server/PipeApplier.cpp |
MG_Pipe::MGPipeApplierIsUnbarrieredApply() |
定义在 MG_Impl/Pipe |
Wire/PipeWireCodec.cpp |
MG_State::GLState::DecodeProgramArchive(...) |
解码器需要一个住在 MG_State 里的解码助手 |
Wire/PipeWireCodec.cpp、MG_Pipe/PipeApply.cpp |
Client::AdoptTierIsEmulate() |
采纳档位判据住在 client 侧 |
MG_Pipe/PipeApply.cpp |
Client::ClientSession::Active() |
同上 |
这 6 个是 P6 自己的账,不能推给 P7/P3b/P4b。它们数量小、形状清楚,应当在 sm 或 lk 里解决:
前三个是"把函数搬到正确的模块",后三个是"server 不该问 client 要答案"。
6 给 c6 的结论
- 契约不能再写 "server 不链
MG_Impl"。PipeInputs.h与P5F-WIRE-COMPLETENESS.md§1.4 的断言按今天的树是假的,c6要按CONTRACT§3.3 自己写的规矩"做不到就记账"把它记成债。 - 债的归属是 P7(101)+ P3b/P4b(14)+ 两者共有(60)+ P6 自己(6),不是一笔含糊的"以后再说"。
- P6 不需要为此阻塞。P6 的 server 是 fork 出来的同一个二进制,本来就链着整份库——
ARCHITECTURE.md:496的"一份共享库、两个角色"正是这个意思。 所以 P6 可以照常推进,但c6必须明确写下:P6 的 server 镜像 = 整份libMobileGL.so, 而"一个不含前端的 server 镜像"是 P7 / P3b/P4b 完成之后才谈得上的东西。 c6要新增一条具名工作:建立模块 target 边界(object library 或 static lib)。 没有它,这个数字永远只能靠nm事后量,不能变成一道会变红的门。 建议形状:lk包顺带把MG_Remote/Transport切成自己的 target(它本来就是纯度门 A/B/C 的对象), 其余模块边界记成 P13 的账。- 一道可以现在就立的门:把本文的 184 记成棘轮,CI 上重算并断言"只减不增"。 它能因 P7/P3b/P4b 的真实进展而下降,也能因新写的后端代码又去读前端对象而变红—— 这正是这个项目要的那种门。
7 附带发现:ABI 指纹的 build stamp 会静默变成空串
这是实验过程中撞出来的,不在 a6 的清单上,但它直接打在 hs 包上,而且是个真洞。
机制。CMakeLists.txt:188-195:
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
OUTPUT_STRIP_TRAILING_WHITESPACE
)
string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT)
没有 RESULT_VARIABLE,没有 ERROR_QUIET 之外的任何处理,没有兜底。
git 失败时 GIT_COMMIT_HASH_FULL 是空的,SUBSTRING 给出空串,配置继续,构建成功。
本次实测:配置阶段打印 fatal: not a git repository: (null),
生成的 generated/MGGitHash.h 是
#define GIT_COMMIT_HASH_SHORT ""
#define GIT_COMMIT_HASH_FULL ""
为什么会失败:这棵树是 git worktree,.git 是一个文件,内容是
gitdir: C:/Users/geekerwan/.../FoldCraftLauncher/.git/modules/MobileGL/worktrees/MobileGL-disagg
——一条 Windows 路径。WSL 的 git 解析不了它(要 /mnt/c/...)。
本项目正是用 worktree 并行推进各阶段的,所以"从 WSL 构建某个 worktree"这件事会稳定复现。
为什么要紧。AbiFingerprintInputs::BuildStamp(SessionRings.h:563-565)就是它,注释写明它的职责:
"GIT_COMMIT_HASH_SHORT: two builds of the same sizes can still disagree about a FIELD ORDER, which no sizeof can see. nullptr and "" are distinct inputs and neither equals a real stamp."
设计时想过空值——但只想到"空串是一个与真 stamp 不同的输入"。 没想到的是构建会静默地把所有构建的 stamp 都变成同一个空串。 于是:两份来自不同 commit、都在 git 取不到版本的环境里构建的库,ABI 指纹相同, 而指纹里唯一能看见字段重排的那一项就此失效。
更正(
lk跑基线测试时发现)。 本节原先写的是"没有任何日志、任何门会说一声"。这句是错的, 门是存在的:MG_Test/Wire/SessionHandshakeTest.cpp:140断言inputs.BuildStamp[0] != '\0',:180断言扰动BuildStamp = ""必须改变指纹。在这个配置下两条都实测为红:Expected: (inputs.BuildStamp[0]) != ('\0'), actual: '\0' vs '\0'。 所以项目预见到了这个情形,本节的发现因此更强而不是更弱——不是"没人会注意到", 是"会注意到它的门已经存在,而且现在就是红的"。真正静默的是构建期:CMake 无声地产出空串, 红只在跑测试时才出现。hs要补的是构建期的具名失败,不是一道新门。
这不是一个奇异场景。会命中它的至少有:worktree 跨 WSL 构建(已实测)、
源码 tarball 构建、CI 上不带 .git 的 checkout、把 repo 拷进去的 Docker 构建。
给 hs 的要求(在拆分 wireFingerprint / buildFingerprint 之外新增一条):
取 stamp 失败必须是具名的——要么配置期失败并说明,要么写进一个明确表示"本构建无版本标识"的哨兵值
并让握手对它具名拒绝(Dial == Fork 下两端本来就该是同一二进制,无版本标识意味着这个前提无法验证)。
不能继续是空串。
red-once:在一个 git 不可用的环境里构建,握手必须因为"build stamp 缺失"而具名变红,而不是悄悄通过。
8 另一件顺带记录的事
.gitmodules 里 3rdparty/DiligentCore 有重复配置(path 与 url 各两份),
git fetch 会 warn 并跳过第二份。不影响构建,但迟早要清。