[MG_Impl, MG_IntegrationTest, ci] (P7): the compare-at-read hook gets a negative control of its own

- MGPipeVerifyReadHook honours MOBILEGL_PIPE_VERIFY_CORRUPT: it perturbs the ORACLE after CopyField, and AGAIN after the ID-49 neutral-pack overwrite, because that overwrite replaces the pack half wholesale and CorruptStorage perturbs an array's element 0 - which is the pack half
- until now the knob only ever reached EntryCompare, so every red it could produce was the client thread's where=entry and the hook that runs on the server's apply thread at every backend read had no falsifier at all
- new entry per backend, DirectGLES/DirectVulkan.VerifySplitReadCorrupted., with CORRUPT=GetPixelStoreParameters and VERIFY_FATAL=0 - the field is in the kReadback fill mask, so the client entry compare would otherwise abort the process before the server reached the read
- the case reads the SERVER role's half of its private log alone: with the knob armed a union search over both halves would be satisfied by the arm that is not under test
- PipeStatsWindow.h gains ReadServerLogSince for that
- test.yml grows the matching control step - run the two entries, then re-read both server halves from outside for the where=read report
- measured: 2/2 green, server halves 3 and 2 lines, client halves 0 lines; revert-and-run with the two corruption blocks removed gives 2/2 red and both server halves 0 lines
- the lane goes 1070 to 1072
This commit is contained in:
2026-09-22 21:35:06 -04:00
parent ca8cbe5c34
commit e9b3f5fe04
8 changed files with 206 additions and 8 deletions
+38
View File
@@ -1056,6 +1056,44 @@ jobs:
fi
echo "the corrupted field turned ${matched} selected split entries red, as it must"
# NEGATIVE CONTROL A ON THE OTHER COMPARATOR (P7 wave 3, V1). The step above proves the
# ENTRY compare - client thread, verb boundary, `where=entry`. The compare-at-read hook runs
# on the SERVER's apply thread at every backend read and is the only comparator that arm ever
# runs, and until MG_Impl/Pipe/PipeFill.cpp's hook honoured MOBILEGL_PIPE_VERIFY_CORRUPT it
# had no control at all: a hook that had stopped comparing was indistinguishable from a hook
# with nothing to report.
#
# NOT THE "ctest MUST FAIL" SHAPE, and the reason is mechanical rather than a preference:
# GetPixelStoreParameters is in kReadback's fill mask, so with MOBILEGL_PIPE_VERIFY_FATAL at
# its default the client's entry compare aborts the process at the ReadPixels boundary and
# the server never reaches the read. The entries therefore pin FATAL=0, survive their own
# divergence, and each asserts from inside the process that the SERVER role's half of its
# private log carries the report. This step re-reads those logs from outside, so the claim
# does not rest only on the assertion that produced them.
#
# THE GREEN HALF is the lane run above: the same hook, the same window, with no corruption
# armed, across 1070 entries at zero divergences.
- name: Negative control A (split, read side) - the server's own read must report it
working-directory: build-verify-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
run: |
FILTER='(DirectGLES|DirectVulkan)\.VerifySplitReadCorrupted\.'
matched=$(ctest -N -L '^integration-verify-split$' -R "${FILTER}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 2 ]; then
echo "::error::split read-side control A selected ${matched} tests; its filter no longer matches both backends"
exit 1
fi
ctest --output-on-failure -L '^integration-verify-split$' -R "${FILTER}" --no-tests=error
for backend in DirectGLES DirectVulkan; do
log="MobileGL/MG_IntegrationTest/pipe-verify-split-read-corrupt-${backend}.server.log"
if ! grep -q 'Fatal{PipeVerifyDiffer, "GetPixelStoreParameters@ReadPixels".*where=read}' "${log}"; then
echo "::error::${log} carries no compare-at-read report for the corrupted pack field: the hook is not comparing on the server's apply thread, or the ID-49 neutral-pack window swallowed the perturbation"
exit 1
fi
done
echo "both backends reported GetPixelStoreParameters@ReadPixels where=read in the SERVER half, as they must"
# NEGATIVE CONTROL B ON THE SPLIT ARM, WITH THE SPLIT ARM'S PAIR. The monolith pair
# (GenerateMipmap:GetActiveTextureUnit) is inert here - the split backend's generate_mipmap
# resolves its texture by the verb's handle and never reads the active unit - so this step
+20
View File
@@ -722,6 +722,17 @@ namespace MobileGL::MG_Pipe {
// never recurses (and never reports the inner read against a half-copied scratch).
g_verify.InHook = true;
MGPipeFillAccess::CopyField(g_readScratch, *ctx, field);
// P7 wave 3 (V1): NEGATIVE CONTROL A REACHES THIS ARM TOO. Until this line the knob only
// ever perturbed EntryCompare's snapshot, so every red it could produce said `where=entry`
// and was produced on the CLIENT thread - which left the compare-at-read hook, the arm
// that runs on the server's apply thread and does the whole of the split lane's per-read
// work, with no falsifier at all: a hook that had stopped comparing would have looked
// exactly like a hook with nothing to report. The perturbation goes on the ORACLE, for
// EntryCompare's reason: the arm under test is the stored value, so corrupting THAT would
// be testing the corruption.
if (g_verify.Corrupt && *g_verify.Corrupt == field) {
MGPipeApplyVerifyCorruption(g_readScratch, field);
}
const Bool equal = MGPipeInputsFieldEqual(field, self, g_readScratch);
g_verify.InHook = false;
if (equal) return;
@@ -737,6 +748,15 @@ namespace MobileGL::MG_Pipe {
}
return true;
});
// AND THE CONTROL IS RE-APPLIED, because the overwrite above replaces the pack half
// WHOLESALE - the corruption included, since CorruptStorage perturbs an array's FIRST
// ELEMENT and the pack half is element 0. Without this line the one window in which
// this arm is most load-bearing (the server's read_pixels, 8530 reads over 220 of the
// lane's entries) would be the one window MOBILEGL_PIPE_VERIFY_CORRUPT=
// GetPixelStoreParameters cannot turn red.
if (g_verify.Corrupt && *g_verify.Corrupt == field) {
MGPipeApplyVerifyCorruption(g_readScratch, field);
}
const Bool equalToTheNeutralPack = MGPipeInputsFieldEqual(field, self, g_readScratch);
g_verify.InHook = false;
if (equalToTheNeutralPack) return;
@@ -3967,6 +3967,28 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)
ENVIRONMENT "${mglItestVerifyArmEnv}\;MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters\;MOBILEGL_PIPE_VERIFY_FATAL=0\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-split-corrupt-${mglItestVerifyBackend}.log"
RESOURCE_LOCK "pipe-verify-split-corrupt-${mglItestVerifyBackend}.log"
)
# A ON THE OTHER ARM (P7 wave 3, V1). Control A above perturbs EntryCompare's snapshot,
# which runs on the CLIENT thread at a verb boundary and reports `where=entry`; the
# compare-at-read hook, which runs on the SERVER's apply thread at every backend read and
# is the only comparator that arm ever runs, had no control at all until this entry. The
# knob names GetPixelStoreParameters because ReadPixels is where the hook does its one
# piece of arm-specific reasoning - the ID-49 neutral pack window, whose oracle it rewrites
# - so a control aimed anywhere else would leave exactly that branch unproven.
#
# FATAL=0 for the corrupt entry's reason and one more: this field IS in kReadback's fill
# mask (FillPoints.def), so the client's entry compare fires first and with FATAL at its
# default the process would die before the server reached the read. The case reads the
# SERVER role's half of its private log and nothing else.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "${mglItestVerifyBackend}.VerifySplitReadCorrupted."
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReportedOnTheServerRead"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-verify-split"
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
ENVIRONMENT "${mglItestVerifyArmEnv}\;MOBILEGL_PIPE_VERIFY_CORRUPT=GetPixelStoreParameters\;MOBILEGL_PIPE_VERIFY_FATAL=0\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-split-read-corrupt-${mglItestVerifyBackend}.log"
RESOURCE_LOCK "pipe-verify-split-read-corrupt-${mglItestVerifyBackend}.log"
)
# B (G5): the child brings up its own inproc session from the inherited environment, and
# PoisonOmissionScenario's ReadChildLog reads BOTH role halves of the child's log, because
# under a transport the poisoned read can fire on either side of the seam.
@@ -147,6 +147,20 @@ namespace MGITest::PipeStatsWindow {
inline std::string ReadLaneLog() { return ReadLaneLogSince(LogMark{}); }
// THE SERVER HALF ALONE, for a claim whose whole content is WHICH ROLE said it. The
// concatenation above answers "did anyone report this", which is the right question for a
// diagnostic that could honestly come from either side; it is the wrong question for a
// control that exists to prove the SERVER's apply thread still runs the comparator, because
// the client's own entry compare reports the same field on the same verb and would satisfy a
// union search all by itself. Empty in a pull build (there is no server half), which a caller
// must read as "could not look" rather than as "the server said nothing".
inline std::string ReadServerLogSince(const LogMark& mark) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeSyncPeerLog();
#endif
return ReadFileSince(ServerLibraryLogPath(), mark.server);
}
// The last summary line in the log, verbatim. `found` is false when the library never emitted
// one, which is a different failure from "the counter read zero" and has to be reported as
// one: it means the stats channel never reached the process, not that the workload did
@@ -25,6 +25,15 @@
// perturbed before the entry compare, so a comparator that works must
// report Fatal{PipeVerifyDiffer, "<Field>@<Verb>"}. A comparator that
// compares nothing stays quiet and this case goes red.
// CorruptedFieldIsReportedOnTheServerRead
// - the same control aimed at the OTHER comparator (P7 wave 3, V1).
// The entry compare runs on the client thread at a verb boundary; the
// compare-at-read hook runs on the server's apply thread at every
// backend read, and until this case existed nothing could tell a hook
// that had stopped comparing from a hook with nothing to say. It
// reads the SERVER role's half of the log, because with the knob
// armed the client reports the same field and a union search would be
// satisfied by the arm that is not under test.
//
// The observable is the library's own log, because MG_Config is not reachable from this module
// (on Android it links the SHIPPING libMobileGL.so, built -fvisibility=hidden) and the arming
@@ -265,5 +274,83 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
<< appended;
}
// NEGATIVE CONTROL A ON THE OTHER ARM (P7 wave 3, V1): the COMPARE-AT-READ hook, on the
// SERVER's apply thread, inside the ID-49 neutral pack window.
//
// WHY THE CASE ABOVE DOES NOT COVER IT. MOBILEGL_PIPE_VERIFY_CORRUPT used to perturb only
// EntryCompare's snapshot, so every red it could produce came from the CLIENT thread at a
// verb boundary and said `where=entry`. The hook that runs on every backend read - the
// whole of the split lane's per-read work, and the only comparator the server's apply
// thread ever runs - had no falsifier: a hook that had stopped comparing looked exactly
// like a hook with nothing to report, and the lane would have been just as green.
//
// WHY THIS FIELD AND THIS VERB. GetPixelStoreParameters@ReadPixels is where the hook does
// its one piece of ARM-SPECIFIC reasoning (PipeFill.cpp's
// ServerReadsInsideTheNeutralPackWindow): the applier reads the backend with the neutral
// pack, so inside that window the oracle for the pack half is the neutral pack rather than
// the live context. A control aimed anywhere else would leave exactly that branch unproven.
//
// THE SERVER HALF, NOT THE UNION. With this knob armed the client's own entry compare
// reports `GetPixelStoreParameters@...` too, in the client's log, so a whole-lane search
// would be satisfied without the server having run anything at all. The assertion below
// reads the server role's half and nothing else.
//
// MOBILEGL_PIPE_VERIFY_FATAL=0, for CorruptedFieldIsReported's reason and one more: the
// client's entry compare on the ReadPixels verb fires FIRST (kReadback's fill mask carries
// this field, FillPoints.def), so with FATAL at its default the process would abort before
// the server ever reached the read.
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReportedOnTheServerRead) {
if (!Ready()) return;
constexpr const char* kPackField = "GetPixelStoreParameters";
const char* knob = std::getenv("MOBILEGL_PIPE_VERIFY_CORRUPT");
if (knob == nullptr || std::string(knob) != kPackField) {
GTEST_SKIP() << "this case is the compare-at-read hook's negative control and needs "
"MOBILEGL_PIPE_VERIFY_CORRUPT=" << kPackField << " for the whole "
"process, which is what the VerifySplitReadCorrupted. ctest entries "
"set; no other field is read by the server inside a window whose "
"oracle the hook rewrites";
}
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is set but MOBILEGL_PIPE_VERIFY is not, so "
"the comparator is dormant and there is nothing to corrupt";
}
if (LibraryLogPath().empty()) {
GTEST_SKIP() << "MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to report "
"the divergence; the VerifySplitReadCorrupted. ctest entries set both";
}
if (MGITest::PipeStatsWindow::ServerLibraryLogPath().empty()) {
GTEST_SKIP() << "this build writes no server-role log, so there is no half in which the "
"server's own report could be told apart from the client's; the hook's "
"read arm is registered on the split (inproc) lane only";
}
const MGITest::PipeStatsWindow::LogMark before = LibraryLogMark();
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
const std::string server = MGITest::PipeStatsWindow::ReadServerLogSince(before);
const std::string expected =
std::string(kDifferPrefix) + ", \"" + kPackField + "@ReadPixels\"";
const std::size_t at = server.find(expected);
ASSERT_NE(at, std::string::npos)
<< "MOBILEGL_PIPE_VERIFY_CORRUPT=" << kPackField << " perturbs the hook's ORACLE at "
"every backend read of that field, and the server's read_pixels reads it under "
"the server's own stamp - so the server role's log had to carry " << expected
<< ", ...}. It carried nothing: either the compare-at-read hook is not running on "
"the apply thread, or the neutral-pack window swallowed the perturbation, and "
"in both cases every per-read green in this lane is green for no reason. Server "
"half of the log appended by this case:\n"
<< server;
const std::size_t eol = server.find('\n', at);
const std::string line =
server.substr(at, eol == std::string::npos ? std::string::npos : eol - at);
EXPECT_NE(line.find("where=read"), std::string::npos)
<< "the server half reported the corrupted field, but not from the compare-at-read "
"arm - `where=` says which comparator spoke, and only `read` is this case's "
"subject. Line:\n"
<< line;
}
} // namespace
} // namespace MGITest
+1 -1
View File
@@ -102,7 +102,7 @@ P7 收官 = 五道出口门同时成立(§2–§7),而不是路线图 P7
## §6 出口门 2:verify 零分歧
verify 今天只在 monolith 注册(`MG_IntegrationTest/CMakeLists.txt:1684-1760`)。门 2 = **verify × {monolith, split(inproc)} × Magma 零分歧**,且两条既有负控(`VERIFY_CORRUPT`、`POISON_OMIT`)在 split 臂上各红一次。前置债:verify+split 撞旧 `PipeRespecifyScope` 断言(`notes/p65/validation-status.md:118`)——放宽为「按 level 的 producer 各自覆盖其宣告范围」,正反单测各一。trace 侧:8 个 verify case × DirectVulkan × inproc 一轮,分歧计数 0。**状态(wave 3 V1,`p7/verify-split`,`notes/p7/verify-split.md`)**:前置债已清(`70f86064`),`integration-verify-split` 双后端 1070/1070 绿、两条负控在 split 臂上各红一次(POISON_OMIT 用 split 臂自己的 `ReadPixels:GetPixelStoreParameters` 配对)、trace 侧 8 case 分歧 0,门 2 在主机上成立;唯一的真实分歧是 server `read_pixels` 的 ID-49 中性 pack 窗口,按预言而非规则处理,spawn 不在门内也无法测(比对器是同进程仪器)。
verify 今天只在 monolith 注册(`MG_IntegrationTest/CMakeLists.txt:1684-1760`)。门 2 = **verify × {monolith, split(inproc)} × Magma 零分歧**,且两条既有负控(`VERIFY_CORRUPT`、`POISON_OMIT`)在 split 臂上各红一次。前置债:verify+split 撞旧 `PipeRespecifyScope` 断言(`notes/p65/validation-status.md:118`)——放宽为「按 level 的 producer 各自覆盖其宣告范围」,正反单测各一。trace 侧:8 个 verify case × DirectVulkan × inproc 一轮,分歧计数 0。**状态(wave 3 V1,`p7/verify-split`,`notes/p7/verify-split.md`)**:前置债已清(`70f86064`),`integration-verify-split` 双后端 1072/1072 绿、两条负控在 split 臂上各红一次(POISON_OMIT 用 split 臂自己的 `ReadPixels:GetPixelStoreParameters` 配对)、trace 侧 8 case 分歧 0,门 2 在主机上成立;唯一的真实分歧是 server `read_pixels` 的 ID-49 中性 pack 窗口,按预言而非规则处理,spawn 不在门内也无法测(比对器是同进程仪器)。
---
@@ -104,7 +104,7 @@ TCP harness 的 fork pre-flight 也曾先占用远端单会话,使真正测试
这是既有 verify-only “全资源 respecify”断言与后来合法 per-level producer 的冲突,
不是 TCP 前送失败。证据 `/home/swung/p65-lf-verify/` 与 `/home/swung/p65-verify-controls/`;
此项尚未宣称通过。
(P7 wave 3 V1 更新:断言已放宽为「按 level 的 producer 覆盖其宣告范围」,verify × split 车道 1070/1070,见 [`notes/p7/verify-split.md`](../p7/verify-split.md)。)
(P7 wave 3 V1 更新:断言已放宽为「按 level 的 producer 覆盖其宣告范围」,verify × split 车道 1072/1072,见 [`notes/p7/verify-split.md`](../p7/verify-split.md)。)
CI 的 `runtime_mode_proof.py` 曾读取无角色后缀的日志名,导致真实两后端用例通过后仍 FileNotFoundError。
修复为读取 client/server 两份,并保留任一缺失为硬失败。除了正反 self-test,
+23 -6
View File
@@ -90,8 +90,8 @@ upload,所以描述符宣告 `(target, level)` 而调用递空指针,会走
(3) 仅 DirectGLES 半边的四条 Magma decline 用例(§2.3 发现 B)。GLES 的 texture-view 用例照 monolith
verify 车道的拼法带 `MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1` 另注册一次。
- 手工注册(机制过滤器把它们一起拿掉了):每后端 `VerifySplitArming.`(arming 断言,私有日志)、
`VerifySplitCorrupted.` / `VerifySplitPoisonOmitted.`(§3)、`VerifySplit.PoisonOmissionScenario.WithoutOmissionCompletes`
(负控 B 的 CI 靶子)。
`VerifySplitCorrupted.` / `VerifySplitReadCorrupted.` / `VerifySplitPoisonOmitted.`(§3)、
`VerifySplit.PoisonOmissionScenario.WithoutOmissionCompletes`(负控 B 的 CI 靶子)。
- **不设 `MGITEST_SPLIT_LANE=1`**:它武装 ScenarioFixture 析构的「本用例必须推动过 client 编码器序号」,对整二进制
集合是错的(tier 3 实测 13 红全是 CapsMirror 回答的查询)。取而代之的是**逐条目私有日志**
(`SplitLogPaths.cmake.in` 的 `MGL_VERIFY_SPLIT_TEST_LISTS`)和 CI 的逐条目臂证明:每个 client 日志都必须有
@@ -167,9 +167,10 @@ DirectVulkan 半边保留且绿。
| 车道 | 臂 | 基线 `3c2867d3` | 本包之后 | 结果 |
|---|---|---|---|---|
| `integration-verify` | monolith,DirectGLES / DirectVulkan | 568 / 568 = 1136(本配置下 2 红,§2.4) | 568 / 568 = **1136**(名不变) | 1136/1136,271 skip |
| `integration-verify-split` | inproc,DirectGLES / DirectVulkan | 0 / 0(不存在) | 533 / 537 = **1070** | 1070/1070,117 skip(GLES 60、Magma 57) |
| `integration-verify-split` | inproc,DirectGLES / DirectVulkan | 0 / 0(不存在) | 534 / 538 = **1072** | 1072/1072,117 skip(GLES 60、Magma 57) |
DirectGLES 533 = 530 环境 + Arming + Corrupted + PoisonOmitted;DirectVulkan 537 = 534 + 3;两者差 4 = §2.3。
DirectGLES 534 = 530 环境 + Arming + Corrupted + ReadCorrupted + PoisonOmitted;DirectVulkan 538 = 534 + 4;
两者差 4 = §2.3。(V1 修复轮把 `VerifySplitReadCorrupted.` 加进来,每后端 +1,1070 → 1072。)
---
@@ -188,6 +189,22 @@ DirectGLES 533 = 530 环境 + Arming + Corrupted + PoisonOmitted;DirectVulkan
2 × MGPipe: Fatal{PipeVerifyDiffer, "GetRenderStateParameters@DrawArrays", verb=1, where=entry}
```
**A′(G4,读臂;V1 修复轮补)** 入口比对跑在 **client 线程的动词边界**上,报 `where=entry`;**compare-at-read
hook 跑在 server 的 apply 线程上**,是这条臂每次 backend 读唯一运行的比对器,而在本轮之前 `VERIFY_CORRUPT`
**根本到不了它**——一个停止比对的 hook 和一个没东西可报的 hook 长得一模一样。修复:hook 在
`CopyField(g_readScratch, …)` 之后按旋钮扰动**预言**,并在 ID-49 中性 pack 覆写之后**再扰动一次**(覆写是整
个 pack 半边的,连扰动一起抹掉——`CorruptStorage` 扰的正是数组的第 0 个元素,也就是 pack 半边)。新条目
`{DirectGLES,DirectVulkan}.VerifySplitReadCorrupted.`(`MOBILEGL_PIPE_VERIFY_CORRUPT=GetPixelStoreParameters`、
`FATAL=0`,因为该字段在 `kReadback` 填充掩码里、client 的入口比对会先死),用例只读**server 半边**:
```
[mgl-srv-apply/FATAL]: MGPipe: Fatal{PipeVerifyDiffer, "GetPixelStoreParameters@ReadPixels", verb=6, where=read}
```
本机实测:**2/2 绿**,server 半边 DirectGLES 3 行 / DirectVulkan 2 行,client 半边 **0 行**(入口比对在这个
字段上不报,所以这条红线**只能**来自读臂)。反证(把 hook 里的两处扰动去掉、重编、再跑、再还原):两条条目
**2/2 红**,两个 server 半边 `PipeVerifyDiffer` **各 0 行**。
**B(G5)** `MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters` 打
`(DirectGLES|DirectVulkan)\.VerifySplit\.PoisonOmissionScenario\.WithoutOmissionCompletes`:**2/2 红**,子进程
server 半边日志:
@@ -196,7 +213,7 @@ server 半边日志:
[mgl-srv-apply/FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "GetPixelStoreParameters@ReadPixels"}
```
正形条目 `VerifySplitCorrupted.` / `VerifySplitPoisonOmitted.` 两后端 4/4 绿。
正形条目 `VerifySplitCorrupted.` / `VerifySplitReadCorrupted.` / `VerifySplitPoisonOmitted.` 两后端 6/6 绿。
### 3.2 发现 C:monolith 的 POISON_OMIT 配对在 split 臂上是哑的
@@ -273,7 +290,7 @@ divergences, zero unmigrated reads` 与 `MGPipe split: <case> DirectVulkan trans
| `^integration-spawn$` | 219/219 |
| `^integration-tcp$` | 222/222 |
| `^integration-verify$`(monolith) | 1136/1136 |
| `^integration-verify-split$` | **1070/1070** |
| `^integration-verify-split$` | **1072/1072** |
| `^integration-magma-split$` / `-spawn$` / `-tcp$` | 93/93 · 72/72 · 74/74 |
以上 lane 均在 `build-split`(verify 构建)上跑,比基线口径更严(每个 backend 读都过比对器的 hook)。