diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40dad9f8..4c987113 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -905,8 +905,10 @@ jobs: # The lane's own control entries, by name: each must have run and passed, and a skip is a # failure here although ctest reports it green. VerifyArming asserts in-test that the - # comparator printed its armed line, VerifyCorrupted that a corrupted field is reported by - # name, and PoisonOmitted that an omitted stamp kills a forked child with its own Fatal. + # comparator printed its armed line with fatal=1, under the ambient entries' own environment + # (MG_IntegrationTest/CMakeLists.txt builds both from one list), VerifyCorrupted that a + # corrupted field is reported by name, and PoisonOmitted that an omitted stamp kills a + # forked child with its own Fatal. - name: Verify lane control entries run: | python3 scripts/ci/junit_tally.py "${RUNNER_TEMP}/verify.xml" \ @@ -917,7 +919,6 @@ jobs: --require-entry-passed DirectGLES.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb \ --require-entry-passed DirectVulkan.PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb - - name: Upload verify lane logs if: ${{ always() }} uses: actions/upload-artifact@v7 diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 9d6fe5ff..c3652d57 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -1260,17 +1260,22 @@ if (MOBILEGL_PIPE_VERIFY) # boundary and again at every accessor read, which the design budgets at 5-10x. set(MGL_ITEST_VERIFY_TIMEOUT 900) - mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log" + # What "the monolith verify lane" means, per backend, in one place: the ambient Verify. entries + # and every control entry below are this list plus their own log path and markers. So the + # controls run the configuration the ambient entries run, and an edit that leaves that lane + # unarmed or non-fatal turns PipeVerifyArmingScenario.Armed red, which asserts both. + set(MGL_ITEST_GLES_VERIFY_BASE_ENV + "MOBILEGL_TRANSPORT=monolith" "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" ${MGL_ITEST_COMMON_ENV}) - mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log" + set(MGL_ITEST_VULKAN_VERIFY_BASE_ENV + "MOBILEGL_TRANSPORT=monolith" "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" ${MGL_ITEST_VULKAN_ENV}) + mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV} + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log") + mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV} + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log") + # The arming assertion's own lane, one case per backend, with a log path nothing else writes to. # # PipeVerifyArmingScenario.Armed reads the library's log, and the log is a per-LANE resource: it @@ -1280,33 +1285,23 @@ if (MOBILEGL_PIPE_VERIFY) # the primgen reroute, the point-size demotion) is registered exactly like this for the same # reason. MGITEST_PIPE_ARMING_LANE is a harness marker - the library never reads it - and it is # what makes the case skip in the ambient lane instead of racing there. - mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log" - ${MGL_ITEST_COMMON_ENV}) - mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" "MGITEST_PIPE_ARMING_LANE=1" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log" - ${MGL_ITEST_VULKAN_ENV}) + mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV} + "MGITEST_PIPE_ARMING_LANE=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectGLES.log") + mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ARMING_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV} + "MGITEST_PIPE_ARMING_LANE=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-arming-DirectVulkan.log") # Negative control A (G4). MOBILEGL_PIPE_VERIFY_FATAL=0 so the process SURVIVES its own # divergence and the case can read the report back out of the log; the other half, that with # FATAL at its default of 1 a divergence aborts, is PipeInputsTest's # VerifyCorruptKnobNamesTheFieldAtEntry. - mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" + mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV} "MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log" - ${MGL_ITEST_COMMON_ENV}) - mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log") + mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV} "MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log" - ${MGL_ITEST_VULKAN_ENV}) + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log") # Negative control B (G5). The omission skips the STAMP of one field for one verb while still # copying its value, which is indistinguishable from a fill row nobody wrote; the scenario @@ -1317,18 +1312,12 @@ if (MOBILEGL_PIPE_VERIFY) # the fill (around its neutral-pack readback). GetActiveTextureUnit at GenerateMipmap was the # frontend arm's read; the record arm resolves the texture from the verb's own handle and never # reads it, so that pair could no longer go red for its own reason. - mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1" + mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT ${MGL_ITEST_GLES_VERIFY_BASE_ENV} "MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log" - ${MGL_ITEST_COMMON_ENV}) - mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT - "MOBILEGL_TRANSPORT=monolith" - "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1" + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log") + mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT ${MGL_ITEST_VULKAN_VERIFY_BASE_ENV} "MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters" - "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log" - ${MGL_ITEST_VULKAN_ENV}) + "MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log") # The whole suite again, per backend, with the comparator armed. Same scenarios, same # assertions, but every backend read of frontend state is now checked against a snapshot taken @@ -1350,9 +1339,9 @@ if (MOBILEGL_PIPE_VERIFY) ) # The arming assertion, one entry per backend. This is the entry that fails a lane whose library - # never armed: it runs the same library and the same MOBILEGL_PIPE_VERIFY=1 as the ambient - # entries above, but unlike them it cannot be green against a library with no comparator - # compiled in. Its log is its own, so `-j 4` cannot make it flake. + # never armed: it runs the same library and the same base list as the ambient entries above, + # but unlike them it cannot be green against a library with no comparator compiled in, or with + # one that only logs a divergence. Its log is its own, so `-j 4` cannot make it flake. mgl_itest_lane("DirectGLES.VerifyArming." "PipeVerifyArmingScenario.Armed" LABELS "integration-gpu\;integration-verify\;integration-monolith-control" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} ENV "${MGL_ITEST_GLES_VERIFY_ARMING_ENVIRONMENT}") mgl_itest_lane("DirectVulkan.VerifyArming." "PipeVerifyArmingScenario.Armed" diff --git a/MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp index c61afa65..847abb0d 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp @@ -16,10 +16,11 @@ // clean pass. That is the failure mode this scenario exists to make impossible: // // Armed - the environment says the comparator is on for this process, so the -// library must SAY it armed. It asserts a library observable against -// the environment, the same shape UnlocatedIoBlockScenario's arming -// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration -// use. A lane whose library never armed FAILS here; it never passes. +// library must SAY it armed, with fatal=1. It asserts a library +// observable against the environment, the same shape +// UnlocatedIoBlockScenario's arming case and +// AsyncCompileScenario::ExtensionStringMatchesTheConfiguration use. A +// lane whose library never armed FAILS here; it never passes. // CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With // MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is // perturbed before the entry compare, so a comparator that works must @@ -230,13 +231,28 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); } // of the process, which may already have happened during the harness bring-up. The file // is truncated at this process's first log write, so it still carries nothing else. const std::string whole = LibraryLog(); - EXPECT_NE(whole.find(kArmedLine), std::string::npos) + const std::size_t armedAt = whole.find(kArmedLine); + EXPECT_NE(armedAt, std::string::npos) << "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and " "read back, and the library never reported arming the comparator. Either this " "library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane " "is verifying nothing), or the arming MGLOG_I is gone. Log:\n" << whole; + // Fatal as well as armed: an ambient entry fails on a divergence only through the abort, + // so a lane armed non-fatal logs every divergence and stays green. This entry is the + // ambient entries' environment plus its marker and log (CMakeLists.txt builds both from + // one list), so its arming line answers for theirs. + if (armedAt != std::string::npos) { + const std::size_t eol = whole.find('\n', armedAt); + const std::string line = + whole.substr(armedAt, eol == std::string::npos ? std::string::npos : eol - armedAt); + EXPECT_NE(line.find("fatal=1"), std::string::npos) + << "the comparator armed, but not fatal, so a divergence in any entry of this lane " + "is logged and the entry stays green. Arming line:\n" + << line; + } + const std::string appended = LibraryLogSince(before); EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos) << "the comparator reported a push/pull divergence on an ordinary frame:\n" diff --git a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp index aa284f54..24283e0a 100644 --- a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp @@ -146,7 +146,8 @@ namespace { // Negative control B, layer 1 (P1 brief D6 / G5): the omitted (verb, field) pair is the // ONLY thing that goes stale - every other bit of the verb class's mask is fresh, every // field outside it is not, and the verb after it neither heals the field (not in kDraw's -// mask) nor loses one of its own. The omission is the test's own (FillWithholdingTheStamp). +// mask) nor loses one of its own. The omission is the test's own (FillWithholdingTheStamp); a +// verify build also checks the scope of the library's (MGPipeSetPoisonOmission). TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) { #if !MOBILEGL_PIPE_POISON GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)"; @@ -182,6 +183,24 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) { // And the omission is scoped to its fill: the next verb of the same class stamps it again. MGPipeValidateForVerb(MGPipeVerb::BindImageTexture); EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit)); + +#if MOBILEGL_PIPE_VERIFY && MGTEST_HAVE_FORK + // The library's own omission, the one the PoisonOmitted entries arm, stays armed across fills + // and is scoped to its VERB: a different verb of the same class keeps the stamp. In a child, + // because nothing disarms it. + const ChildResult r = RunInChild([] { + if (!MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit")) ::_exit(3); + MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap); + if (Fresh(MGPipeInputField::GetActiveTextureUnit)) ::_exit(4); + MGPipeValidateForVerb(MGPipeVerb::BindImageTexture); + if (!Fresh(MGPipeInputField::GetActiveTextureUnit)) ::_exit(5); + }); + EXPECT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) + << " (3: the setter refused the names; 4: GenerateMipmap stamped the omitted " + "field; 5: BindImageTexture did not stamp it, so the omission matched the " + "class rather than the verb)\n" + << r.Log; +#endif #endif } @@ -335,6 +354,34 @@ TEST_F(PipeInputsTest, EveryCorruptedFieldIsNamedByTheComparator) { EXPECT_EQ(named, field) << "a corrupted " << name(field) << " was reported as " << name(named); } EXPECT_EQ(corruptedFields, kMGPipeInputFieldCount - kMGPipeForwardedFieldCount); + +#if MGTEST_HAVE_FORK + // And the capability field through the knob and each verb's own mask, at the verbs a clear, + // draw and readback frame crosses: every one whose class fills the field must report it at + // entry, and at least one must fill it. + ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH"; + static constexpr MGPipeVerb kFrameVerbs[] = {MGPipeVerb::Clear, MGPipeVerb::DrawArrays, MGPipeVerb::ReadPixels}; + const ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MG_Config::Features.PipeVerifyFatal = false; + ::setenv("MOBILEGL_PIPE_VERIFY_CORRUPT", "IsCapabilityEnabled", 1); + for (const MGPipeVerb verb : kFrameVerbs) + MGPipeValidateForVerb(verb); + }); + ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + SizeT filling = 0; + for (const MGPipeVerb verb : kFrameVerbs) { + const auto cls = static_cast(kMGPipeVerbClass[static_cast(verb)]); + const Bool fills = MGPipeFieldMaskHas(kMGPipeClassFieldMask[cls], MGPipeInputField::IsCapabilityEnabled); + filling += fills ? 1 : 0; + const std::string report = std::string("Fatal{PipeVerifyDiffer, \"IsCapabilityEnabled@") + + kMGPipeVerbNames[static_cast(verb)] + "\""; + EXPECT_EQ(r.Log.find(report) != std::string::npos, fills) + << report << (fills ? " missing" : " unexpected") << " in\n" + << r.Log; + } + EXPECT_GT(filling, 0u) << "no verb of a clear, draw and readback frame fills IsCapabilityEnabled"; +#endif #endif } diff --git a/docs/ci/ci-cleanup-log.md b/docs/ci/ci-cleanup-log.md index 53ec3c5d..6011309f 100644 --- a/docs/ci/ci-cleanup-log.md +++ b/docs/ci/ci-cleanup-log.md @@ -59,10 +59,10 @@ them wrong. | 2026-10-18 | `scripts/ci/control_smoke_test.sh`, `scripts/ci/testdata/stub_ctest.sh`, `scripts/ci/testdata/split_private_log_smoke.sh`, and the `reset` / `results` / `assertion` / `evidence` modes of `split_log_paths.py` | Test / pipe-gates (R-16) | smoke cases and helpers of the step above only | none needed | | 2026-10-18 | unit `RemoteRunAhead.ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit`, and the `MOBILEGL_IPC_VERB_BARRIER` knob with its arm in the shipped client (`ClientSession`: the free-running `EmitAndWaitTails` return, the run-ahead latch term, the `gPipeInputs` guard's early return, the `verb-barrier=` field of the `Config: IPC` line) | Test / unit (split); product code | `0` was never a product mode, only E1's negative control, retired above; the unit case pinned that the control also disarmed run-ahead | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`; `MOBILEGL_IPC_RUN_AHEAD=0` stays the lockstep A/B arm | | 2026-10-18 | unit `SplitBufferSet.AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited` (and its skip stub in the non-disaggregated half), and the `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` knob with its `0` arm in the shipped tracker (`PersistentMapTracker::PushBlocksForChecked`'s early return and once-per-process warning, `BufferObject::NotePersistentMapStateChanged`'s, the `persistent-block=` field of the `Config: IPC` line) | Test / unit; product code | `0` was never a product mode, only E3(a)'s negative control, retired above; the unit case pinned that `0` shipped nothing; the block size is now the constant 64 KiB (`PersistentMapTracker::BlockBytes()`) | `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer` | -| 2026-10-18 | `Negative control A (corrupted snapshot)` | Test / `integration-verify` | it re-ran the five `DirectGLES.Verify.ClearThenReadPixelsScenario` entries with `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` exported job-wide and accepted any red | the in-lane `DirectGLES/DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported` entries, required by name (`Verify lane control entries`), which require `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@...` in the log; the abort with `MOBILEGL_PIPE_VERIFY_FATAL` at its default is unit `PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry` on the verify runtime | -| 2026-10-18 | `Negative control A2 (corrupted capability)` | Test / `integration-verify` | the same five-entry rerun with `MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled`, accepting any red; it proved that the comparator compares the capability field (P13 W3c moved the residual block's cross-check there) | unit `PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator` (`Unit tests on the verify runtime`): every field with storage, the capability field among them, corrupted in turn must be the field the comparator names | -| 2026-10-18 | `Negative control A (corrupted snapshot)` and `Keep lane logs before controls` | Test / `integration-verify-split` | the first re-ran the ten `VerifySplit.ClearThenReadPixelsScenario` entries of both backends with the same knob and accepted any red; the second only kept the lane's logs from being overwritten by that rerun | the in-lane `VerifySplitCorrupted` and `VerifySplitReadCorrupted` entries of both backends, required by name (`Verify-split control entries`) | -| 2026-10-18 | unit `PipeInputsTest.{PoisonOmitKnobArmsTheOmission, BadPoisonOmitKnobIsFatalNamingTheKnob}`, and the `MOBILEGL_PIPE_POISON_OMIT` knob: `Features.PipePoisonOmit`, its parser on every verb (`MGPipeValidateForVerb`'s `ParsePoisonOmissionKnob`, compiled into every build) and the server stamp's own parse of it | Test / unit (every poison lane); product code | the knob existed only for negative control B, and the two cases pinned its parser and its `Fatal{PipeVerifyBadKnob}` | the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker `MGITEST_POISON_OMIT` to the same pair, and the re-executed child arms it through `MGPipeSetPoisonOmission`, compiled only with `-DMOBILEGL_PIPE_VERIFY=ON` (`Harness/PoisonOmissionArm.h`); `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale`, `ReadingAnOmittedFieldAbortsNamingTheVerb` and `TheMutationNoticeRefreshesTheValueButNotTheStamp` withhold the stamp test-side and still run in every poison lane | +| 2026-10-18 | `Negative control A (corrupted snapshot)` | Test / `integration-verify` | it re-ran the five `DirectGLES.Verify.ClearThenReadPixelsScenario` entries with `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` exported job-wide and accepted any red | the in-lane `DirectGLES/DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported` entries, required by name (`Verify lane control entries`), which require `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@...` in the log; the abort with `MOBILEGL_PIPE_VERIFY_FATAL` at its default is unit `PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry` on the verify runtime; that the ambient entries run the comparator armed and fatal is `VerifyArming.PipeVerifyArmingScenario.Armed`, whose environment is now the ambient entries' list plus its marker and log, and which requires `fatal=1` on the arming line | +| 2026-10-18 | `Negative control A2 (corrupted capability)` | Test / `integration-verify` | the same five-entry rerun with `MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled`, accepting any red; it proved that the comparator compares the capability field (P13 W3c moved the residual block's cross-check there) | unit `PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator` (`Unit tests on the verify runtime`): every field with storage, the capability field among them, corrupted in turn must be the field the comparator names; then, in a forked child with the knob set to `IsCapabilityEnabled`, each of Clear, DrawArrays and ReadPixels whose class fills the field must report it at entry, and one must | +| 2026-10-18 | `Negative control A (corrupted snapshot)` and `Keep lane logs before controls` | Test / `integration-verify-split` | the first re-ran the ten `VerifySplit.ClearThenReadPixelsScenario` entries of both backends with the same knob and accepted any red; the second only kept the lane's logs from being overwritten by that rerun | the in-lane `VerifySplitCorrupted` and `VerifySplitReadCorrupted` entries of both backends, required by name (`Verify-split control entries`); that the ambient entries arm is the per-entry arm proof, and that they run fatal is `VerifySplitArming.PipeVerifyArmingScenario.Armed` (the ambient environment plus its marker and log), which requires `fatal=1` on the arming line | +| 2026-10-18 | unit `PipeInputsTest.{PoisonOmitKnobArmsTheOmission, BadPoisonOmitKnobIsFatalNamingTheKnob}`, and the `MOBILEGL_PIPE_POISON_OMIT` knob: `Features.PipePoisonOmit`, its parser on every verb (`MGPipeValidateForVerb`'s `ParsePoisonOmissionKnob`, compiled into every build) and the server stamp's own parse of it | Test / unit (every poison lane); product code | the knob existed only for negative control B, and the two cases pinned its parser and its `Fatal{PipeVerifyBadKnob}` | the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker `MGITEST_POISON_OMIT` to the same pair, and the re-executed child arms it through `MGPipeSetPoisonOmission`, compiled only with `-DMOBILEGL_PIPE_VERIFY=ON` (`Harness/PoisonOmissionArm.h`); `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale`, `ReadingAnOmittedFieldAbortsNamingTheVerb` and `TheMutationNoticeRefreshesTheValueButNotTheStamp` withhold the stamp test-side and still run in every poison lane; the setter's scope to its verb, not its class, is checked by `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` in a forked child, on the verify runtime only | ## Kept after investigation