[Refactor, Test] (docs/ci, docs/Disaggregated, MG_Backend/MGPipe, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Test/Pipe, MobileGL, scripts/data, tools/trace_replay): delete the MOBILEGL_PIPE_POISON_OMIT knob

- the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker MGITEST_POISON_OMIT; the re-executed child arms the pair through Harness/PoisonOmissionArm.h
- MGPipeSetPoisonOmission, the stamp withhold, the name lookups and the server half (now one atomic word) compile only with MOBILEGL_PIPE_VERIFY
- three PipeInputsTest cases withhold the stamp test-side; PoisonOmitKnobArmsTheOmission and BadPoisonOmitKnobIsFatalNamingTheKnob are retired
- skip census: the two retired cases leave the unit-strict and unit-verify names
This commit is contained in:
2026-10-18 10:02:02 -04:00
parent 9b5a60b2f7
commit b2daab61b7
17 changed files with 276 additions and 284 deletions
-6
View File
@@ -469,12 +469,6 @@ namespace MobileGL::MG_Config {
// negative control, MOBILEGL_PIPE_VERIFY_CORRUPT, has no field here: only a verify
// build reads it, when the comparator arms (MG_Impl/Pipe/PipeFill.cpp, ArmVerify).
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
-1
View File
@@ -452,7 +452,6 @@ namespace MobileGL::MG_ConfigLoader {
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
+42 -59
View File
@@ -6,13 +6,14 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name, the
// name lookups the runtime knobs need, and - in a verify build - the per-field equality,
// the entry comparator and the corruption injector. Compiled only under MOBILEGL_PIPE_PUSH
// (CMakeLists.txt appends it to SOURCE_FILES there), so the pull build never sees it. Spells
// no MG_State global: everything that reads the live context lives in MG_Impl/Pipe/PipeFill.cpp.
// The backend-side half of the PipeInputs block: the poison Fatal with its verb name and -
// in a verify build - the per-field equality, the entry comparator, the corruption injector,
// the name lookups its knob and the poison omission need, and the omission's server half.
// Spells no MG_State global: everything that reads the live context lives in
// MG_Impl/Pipe/PipeFill.cpp.
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <atomic>
#include <cstdint>
#include <cstring>
@@ -29,22 +30,6 @@ namespace MobileGL::MG_Pipe {
MGPipeInputPoisonFatal(field, MGPipeVerbName(verb));
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
// The stamp is the poison's (m_filled, the generations): a build without MOBILEGL_PIPE_POISON
// compiles no read check that could ask any of this. Every transport build has the poison.
#if MOBILEGL_PIPE_POISON
@@ -55,46 +40,24 @@ namespace MobileGL::MG_Pipe {
Uint64 g_residualPulls = 0;
#if MOBILEGL_PIPE_VERIFY
// ---- MOBILEGL_PIPE_POISON_OMIT, server half ----
// ---- negative control B, server half (MGPipeServerSetPoisonOmission) ----
//
// The knob withholds the stamp of one (verb, field) pair, a negative control for the
// The omission withholds the stamp of one (verb, field) pair, a negative control for the
// poison check. Under a transport the backend reads the server's stamp, which re-stamps
// every field the verb's class can answer, so the server must withhold the pair too or
// the control cannot go red. Parsed here because this file is in the server library and
// MG_Impl is not; a malformed value is the client's Fatal{PipeVerifyBadKnob}, and this
// side simply does not arm. Verify builds only, so the shipped stamp stays branch-free.
struct ServerPoisonOmission {
Bool Parsed = false;
String Value;
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
ServerPoisonOmission g_serverOmission;
// the control cannot go red. The harness arms it while the apply thread is already
// running, so the pair is ONE atomic word: the verb in the high half, the field in the
// low one, and no verb at all (0xffff) until something is armed. Verify builds only, so
// the shipped stamp stays branch-free.
constexpr Uint32 kNoServerOmission = ~Uint32{0};
std::atomic<Uint32> g_serverOmission{kNoServerOmission};
const ServerPoisonOmission& ServerOmission() {
const String& knob = MG_Config::Features.PipePoisonOmit;
if (g_serverOmission.Parsed && knob.size() == g_serverOmission.Value.size() &&
(knob.empty() || knob == g_serverOmission.Value)) {
return g_serverOmission;
}
g_serverOmission = ServerPoisonOmission{};
g_serverOmission.Parsed = true;
g_serverOmission.Value = knob;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) return g_serverOmission;
const auto verb = MGPipeFindVerb(knob.substr(0, colon).c_str());
const auto field = MGPipeFindInputField(knob.substr(colon + 1).c_str());
if (!verb || !field) return g_serverOmission;
g_serverOmission.Armed = true;
g_serverOmission.Verb = *verb;
g_serverOmission.Field = *field;
return g_serverOmission;
constexpr Uint32 PackServerOmission(MGPipeVerb verb, MGPipeInputField field) {
return (static_cast<Uint32>(verb) << 16) | static_cast<Uint32>(field);
}
Bool ServerOmitsStamp(MGPipeVerb verb, MGPipeInputField field) {
const ServerPoisonOmission& omission = ServerOmission();
return omission.Armed && omission.Verb == verb && omission.Field == field;
return g_serverOmission.load(std::memory_order_acquire) == PackServerOmission(verb, field);
}
#endif // MOBILEGL_PIPE_VERIFY
@@ -453,12 +416,10 @@ namespace MobileGL::MG_Pipe {
filled.FilledGen[i] = serial & (Uint64{0} - bit);
}
#if MOBILEGL_PIPE_VERIFY
// Negative control B's server half (ServerPoisonOmission above): the one withheld pair.
// Negative control B's server half (g_serverOmission above): the one withheld pair.
{
const ServerPoisonOmission& omission = ServerOmission();
if (omission.Armed && omission.Verb == verb) {
filled.FilledGen[static_cast<SizeT>(omission.Field)] = 0;
}
const Uint32 omission = g_serverOmission.load(std::memory_order_acquire);
if ((omission >> 16) == static_cast<Uint32>(verb)) filled.FilledGen[omission & 0xffffu] = 0;
}
#endif
MGPipeStampAccess::SetServerStamped(inputs, true);
@@ -475,6 +436,12 @@ namespace MobileGL::MG_Pipe {
MGPipeStampAccess::SetServerStamped(gPipeInputs, false);
}
#if MOBILEGL_PIPE_VERIFY
void MGPipeServerSetPoisonOmission(MGPipeVerb verb, MGPipeInputField field) {
g_serverOmission.store(PackServerOmission(verb, field), std::memory_order_release);
}
#endif
Uint64 MGPipeResidualPullCount() {
return g_residualPulls;
}
@@ -691,5 +658,21 @@ namespace MobileGL::MG_Pipe {
return true;
});
}
Optional<MGPipeInputField> MGPipeFindInputField(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (std::strcmp(kMGPipeInputFieldNames[i], name) == 0) return static_cast<MGPipeInputField>(i);
}
return std::nullopt;
}
Optional<MGPipeVerb> MGPipeFindVerb(const char* name) {
if (name == nullptr) return std::nullopt;
for (SizeT i = 0; i < kMGPipeVerbCount; ++i) {
if (std::strcmp(kMGPipeVerbNames[i], name) == 0) return static_cast<MGPipeVerb>(i);
}
return std::nullopt;
}
#endif // MOBILEGL_PIPE_VERIFY
} // namespace MobileGL::MG_Pipe
+7 -4
View File
@@ -46,10 +46,6 @@ namespace MobileGL::MG_Pipe {
[[noreturn]] void MGPipeInputPoisonFatalForVerb(MGPipeInputField field, MGPipeVerb verb);
// kMGPipeVerbNames[verb], or "<none>" for kVerbCount (no verb has been filled yet).
const char* MGPipeVerbName(MGPipeVerb verb);
// Name lookups for the runtime knobs (MOBILEGL_PIPE_VERIFY_CORRUPT names a field,
// MOBILEGL_PIPE_POISON_OMIT a Verb:Field pair). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// The server-side read check (ARCHITECTURE.md §9.2.1). Inside a server-stamped verb
// (PipeInputs::ServerStampedVerb) a stale read is judged by the field's ownership class:
@@ -996,6 +992,13 @@ namespace MobileGL::MG_Pipe {
// dereferenced, the snapshot is only ever compared). Returns false for a forwarded field,
// which has nothing to corrupt.
Bool MGPipeApplyVerifyCorruption(PipeInputs& snapshot, MGPipeInputField field);
// PipeInputs.cpp. Name lookups for MOBILEGL_PIPE_VERIFY_CORRUPT (a field) and
// MGPipeSetPoisonOmission (a verb and a field). Empty on an unknown name.
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
// PipeInputs.cpp. Negative control B's server half, armed by MGPipeSetPoisonOmission: the
// server's verb stamp withholds the stamp of `field` at `verb` too.
void MGPipeServerSetPoisonOmission(MGPipeVerb verb, MGPipeInputField field);
// PipeFill.cpp. Writes the MOBILEGL_PIPE_VERIFY_FATAL=0 summary line ("N divergence(s)
// survived") NOW, while the role's log file is still open. MobileGL::Destroy calls it right
// before MG_Util::Debug::Close(): the line used to come from a namespace-scope static's
+34 -64
View File
@@ -8,9 +8,9 @@
// The client side of the PipeInputs block (ARCHITECTURE.md 9.2 phase A): the only place in
// the push arm that reads MG_State::pGLContext. Holds the per-verb filler, the F-class
// forwarders, IsLive, the MOBILEGL_PIPE_POISON_OMIT knob and - in a verify build - the
// second arm (SnapshotFromGLContext), the entry compare, the compare-at-read hook and the
// MOBILEGL_PIPE_VERIFY_CORRUPT knob, which only this build reads.
// forwarders, IsLive and - in a verify build - the second arm (SnapshotFromGLContext), the
// entry compare, the compare-at-read hook, the MOBILEGL_PIPE_VERIFY_CORRUPT knob, which only
// this build reads, and the poison omission (MGPipeSetPoisonOmission).
#include <MG_State/GLState/Core.h>
#include <MG_State/GLState/BufferState/BufferState.h>
// C-1: the vertex-elements CSO's death path raises the backend notice from here, between the
@@ -457,62 +457,40 @@ namespace MobileGL::MG_Pipe {
return null;
}
#if MOBILEGL_PIPE_VERIFY
[[noreturn]] void BadKnob(const char* knob, const char* value, const char* why) {
MGLOG_F("MGPipe: Fatal{PipeVerifyBadKnob, \"%s=%s\": %s}", knob, value, why);
std::abort();
}
// ---- MOBILEGL_PIPE_POISON_OMIT (negative control B, P1 brief D6) ----
// ---- negative control B (P1 brief D6), armed by MGPipeSetPoisonOmission ----
// The filler skips the STAMP (never the value) of one (verb, field) pair: an omission
// indistinguishable from a forgotten FillPoints.def row, so that verb's read of the
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected.
// field is Fatal{UnmigratedPipeInput, "Field@Verb"} and no other verb is affected. The
// GL thread arms it and the GL thread's fill reads it; the server's stamp has its own.
struct PoisonOmission {
Bool Armed = false;
MGPipeVerb Verb = MGPipeVerb::kVerbCount;
MGPipeInputField Field = MGPipeInputField::kFieldCount;
};
PoisonOmission g_omission;
Bool g_omissionKnobParsed = false;
String g_omissionKnobValue; // the value the last parse saw
// Parsed on the first fill and again only when the value changes. A lane loads
// Features once, before any fill, so that is one parse per process there; a forked
// test child that sets Features after its parent already filled gets its own parse,
// which is what puts the parser and its Fatal{PipeVerifyBadKnob} under a unit test.
// An empty value never clears an omission a test armed through MGPipeSetPoisonOmission.
void ParsePoisonOmissionKnob() {
const String& knob = MG_Config::Features.PipePoisonOmit;
// THE UNSET KNOB IS TWO LENGTH LOADS AND A BRANCH, inline, and that is the whole
// change (P5d r3, package C): this runs at every verb - 852 draws a frame on the
// profiled workload - and `knob == g_omissionKnobValue` is an out-of-line String
// compare even when both sides are empty, which is every shipping configuration.
// Sizes first, contents only when a non-empty value is involved; the re-parse
// condition is unchanged (the value differing from what the last parse latched).
if (g_omissionKnobParsed && knob.size() == g_omissionKnobValue.size() &&
(knob.empty() || knob == g_omissionKnobValue)) {
return;
// The stamp the fill's walk is about to overwrite for the omitted pair, put back after the
// branch-free stamp of the whole walk: the value is copied either way, only the stamp is
// withheld. Index kMGPipeInputFieldCount is "nothing withheld at this verb".
struct WithheldStamp {
SizeT Index = kMGPipeInputFieldCount;
Uint64 Gen = 0;
void PutBack(MGPipeFilledState& filled) const {
if (Index < kMGPipeInputFieldCount) filled.FilledGen[Index] = Gen;
}
g_omissionKnobParsed = true;
g_omissionKnobValue = knob;
if (knob.empty()) return;
const auto colon = knob.find(':');
if (colon == String::npos || colon == 0 || colon + 1 >= knob.size()) {
BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "expected <Verb>:<FieldName>");
}
const String verbName = knob.substr(0, colon);
const String fieldName = knob.substr(colon + 1);
const auto verb = MGPipeFindVerb(verbName.c_str());
if (!verb) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such verb in kMGPipeVerbNames");
const auto field = MGPipeFindInputField(fieldName.c_str());
if (!field) BadKnob("MOBILEGL_PIPE_POISON_OMIT", knob.c_str(), "no such field in kMGPipeInputFieldNames");
MGPipeSetPoisonOmission(verbName.c_str(), fieldName.c_str());
};
WithheldStamp StampToWithhold(const MGPipeFilledState& filled, MGPipeVerb verb) {
if (!g_omission.Armed || g_omission.Verb != verb) return {};
const auto index = static_cast<SizeT>(g_omission.Field);
return {index, filled.FilledGen[index]};
}
[[maybe_unused]] Bool IsOmitted(MGPipeVerb verb, MGPipeInputField field) {
return g_omission.Armed && g_omission.Verb == verb && g_omission.Field == field;
}
#if MOBILEGL_PIPE_VERIFY
// ---- the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8) ----
// Two mechanisms, both active only when Features.PipeVerify is set: the ENTRY compare
// once per verb (the pushed block against a second snapshot of the live context,
@@ -1906,25 +1884,17 @@ namespace MobileGL::MG_Pipe {
return published;
}
void MGPipeSetPoisonOmission(const char* verb, const char* field) {
if (verb == nullptr || field == nullptr) {
g_omission = PoisonOmission{};
return;
}
#if MOBILEGL_PIPE_VERIFY
Bool MGPipeSetPoisonOmission(const char* verb, const char* field) {
const auto v = MGPipeFindVerb(verb);
const auto f = MGPipeFindInputField(field);
if (!v || !f) BadKnob("MOBILEGL_PIPE_POISON_OMIT", verb, "unknown verb or field");
g_omission.Armed = true;
g_omission.Verb = *v;
g_omission.Field = *f;
#if MOBILEGL_PIPE_POISON
if (!v || !f) return false;
g_omission = PoisonOmission{true, *v, *f};
MGPipeServerSetPoisonOmission(*v, *f);
MGLOG_I("MGPipe: poison omission armed - %s@%s", field, verb);
#else
MGLOG_W_ONCE("MGPipe: poison omission %s@%s requested but the poison is not compiled in "
"(MOBILEGL_PIPE_POISON=0): no stamp exists to omit",
field, verb);
#endif
return true;
}
#endif
// ---- liveness ----
Bool PipeInputs::IsLive() const {
@@ -3039,7 +3009,6 @@ namespace MobileGL::MG_Pipe {
// every verb) and the guard below then fires Fatal{RoleViolation, "gPipeInputs"} on
// the first unbarriered verb, by name.
const Bool runAhead = ClientRunsAhead();
ParsePoisonOmissionKnob();
#if MOBILEGL_PIPE_VERIFY
ArmVerify();
#else
@@ -3390,18 +3359,19 @@ namespace MobileGL::MG_Pipe {
MGPipeFillAccess::CopyField(inputs, *ctx, static_cast<MGPipeInputField>(copies[k]));
}
#if MOBILEGL_PIPE_POISON
// The value is copied either way; only the stamp is withheld for the omitted pair,
// which is put back to what it held after the branch-free stamp of the whole walk.
const Uint8* const walk = g_fillPlan.Walk[planClass].data();
const SizeT walkCount = g_fillPlan.WalkCount[planClass];
const Uint64 serial = filled.CurrentVerbSerial;
const Bool omit = g_omission.Armed && g_omission.Verb == verb;
const SizeT omitted = omit ? static_cast<SizeT>(g_omission.Field) : 0;
const Uint64 omittedGen = omit ? filled.FilledGen[omitted] : 0;
#if MOBILEGL_PIPE_VERIFY
// Negative control B: the omitted pair's stamp goes back after the walk (WithheldStamp).
const WithheldStamp withheld = StampToWithhold(filled, verb);
#endif
for (SizeT k = 0; k < walkCount; ++k) {
filled.FilledGen[walk[k] & ~ResidualFillPlan::kCopy] = serial;
}
if (omit) filled.FilledGen[omitted] = omittedGen;
#if MOBILEGL_PIPE_VERIFY
withheld.PutBack(filled);
#endif
#endif
}
if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) {
+7 -8
View File
@@ -81,14 +81,6 @@ namespace MobileGL::MG_Pipe {
// (TrackerWalk.ABaseInstanceSurvivesTheFirstWalkOnAFreshContext).
Uint32 MGPipePendingBaseInstance();
// PipeFill.cpp. Negative control B (P1 brief D6): the filler withholds the STAMP - never
// the value - of `field` at `verb`, so that verb's read of it is
// Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected. The
// MOBILEGL_PIPE_POISON_OMIT knob ("<Verb>:<FieldName>") calls this once, on the first
// fill; tests call it directly. Both null clears the omission. An unknown name is
// Fatal{PipeVerifyBadKnob}.
void MGPipeSetPoisonOmission(const char* verb, const char* field);
// PipeFill.cpp. How many times set_vertex_attrib_defaults' applier failed to reproduce
// the value the call carried, so the client wrote the mirror itself
// (EmitVertexAttribDefaults). It is the ONE observable of that repair: the window it
@@ -119,6 +111,13 @@ namespace MobileGL::MG_Pipe {
// is the branch that survives P13, which is why it is its own function rather than the
// filler's loop.
void SnapshotFromGLContext(PipeInputs& snapshot, const MGPipeFieldMask& mask);
// PipeFill.cpp. Negative control B (P1 brief D6): from now on the filler withholds the STAMP -
// never the value - of `field` at `verb`, and so does the server's verb stamp, so that verb's
// read of it is Fatal{UnmigratedPipeInput, "Field@Verb"} while every other verb is unaffected.
// Names from kMGPipeVerbNames / kMGPipeInputFieldNames; false, arming nothing, for an unknown
// one. Call it on the GL thread. The integration harness arms it (Harness/PoisonOmissionArm.h).
Bool MGPipeSetPoisonOmission(const char* verb, const char* field);
#endif
} // namespace MobileGL::MG_Pipe
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeValidateForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
+12 -11
View File
@@ -56,6 +56,7 @@ add_executable(MobileGLIntegrationTest
Harness/P4aSeamPeek.cpp
Harness/P4aFinalFixPeek.cpp
Harness/PersistentMapPeek.cpp
Harness/PoisonOmissionArm.cpp
Harness/SplitRuntimePeek.cpp
# ---- P3b/P4b wave 2-D package D2 (own block; D1 edits this file elsewhere) --------------
# The client texture emitter's own counters, for TextureViewAliasScenario's half of the
@@ -1309,7 +1310,9 @@ if (MOBILEGL_PIPE_VERIFY)
# 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
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane.
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane. The
# pair is a harness marker, MGITEST_POISON_OMIT: the library has no knob for it, and the
# re-executed child arms it through the verify build's setter (Harness/PoisonOmissionArm.h).
# P13 W5: the pair is the pack state at ReadPixels, which monolith's record arm still reads from
# 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
@@ -1317,13 +1320,13 @@ if (MOBILEGL_PIPE_VERIFY)
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT
"MOBILEGL_TRANSPORT=monolith"
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
"MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"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_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters"
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log"
${MGL_ITEST_VULKAN_ENV})
@@ -3663,15 +3666,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)
# 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.
#
# A DIFFERENT PAIR FROM THE MONOLITH ENTRIES', and the monolith pair is not "also fine
# here": GenerateMipmap:GetActiveTextureUnit is inert on this arm - the split backend's
# generate_mipmap resolves its texture from the verb's handle and never reads the active
# unit, so the child exited 0 with that stamp withheld (measured, both backends).
# ReadPixels:GetPixelStoreParameters is read on the server under the server's stamp,
# which is the stamp the knob has to withhold on this arm (PipeInputs.cpp's
# ServerPoisonOmission). PoisonOmissionScenario.cpp's kOmittedPairs carries both.
# THE MONOLITH ENTRIES' PAIR. ReadPixels:GetPixelStoreParameters is read on the server under
# the server's stamp, which is the stamp the omission has to withhold on this arm
# (PipeInputs.cpp's MGPipeServerSetPoisonOmission). GenerateMipmap:GetActiveTextureUnit is
# inert here: generate_mipmap resolves its texture from the verb's handle and never reads
# the active unit, so the child exited 0 with that stamp withheld (measured, both backends).
mgl_itest_lane("${mglItestVerifyBackend}.VerifySplitPoisonOmitted." "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} LOCK "pipe-poison-omit-split-${mglItestVerifyBackend}.log" ENV "${mglItestVerifyArmEnv}\;MOBILEGL_PIPE_POISON_OMIT=ReadPixels:GetPixelStoreParameters\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-split-${mglItestVerifyBackend}.log")
LABELS "integration-verify-split" TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT} LOCK "pipe-poison-omit-split-${mglItestVerifyBackend}.log" ENV "${mglItestVerifyArmEnv}\;MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-split-${mglItestVerifyBackend}.log")
# B's AMBIENT HALF. `Verify.PoisonOmissionScenario.WithoutOmissionCompletes` runs the
# scenario with nothing withheld, and the mechanism filter took it out of the ambient set
# above along with the rest of the scenario. Registered back by hand, with no knob, so
@@ -0,0 +1,39 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PoisonOmissionArm.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "PoisonOmissionArm.h"
#if !defined(__ANDROID__)
#include <MG_Pipe/MGPipe.h>
#include <MG_Impl/Pipe/PipeFill.h>
#if MOBILEGL_PIPE_VERIFY
#define MGITEST_POISON_OMISSION_ARM_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_POISON_OMISSION_ARM_LIVE)
bool PoisonOmissionArmAvailable() {
return true;
}
bool ArmPoisonOmission(const char* verb, const char* field) {
return MobileGL::MG_Pipe::MGPipeSetPoisonOmission(verb, field);
}
#else
bool PoisonOmissionArmAvailable() {
return false;
}
bool ArmPoisonOmission(const char*, const char*) {
return false;
}
#endif
} // namespace MGITest
@@ -0,0 +1,34 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PoisonOmissionArm.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// NEGATIVE CONTROL B'S OMISSION, ARMED FROM THE HARNESS. PoisonOmissionScenario's re-executed
// child withholds the stamp of one (verb, field) pair before it runs the sequence, so that verb's
// read of the field must be Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. The library has no knob
// for it: only a -DMOBILEGL_PIPE_VERIFY=ON build compiles the setter (MG_Impl/Pipe/PipeFill.h,
// MGPipeSetPoisonOmission), and this file is the one place that calls it.
//
// A SEPARATE TRANSLATION UNIT for PipeApplyPeek's reason, verbatim: the scenario sources include
// the GL headers with prototypes and the pipe headers are not meant to meet them in one file.
//
// Both entry points return false, touching nothing, where the setter is out of reach: a build
// without MOBILEGL_PIPE_VERIFY, or Android, where this module links the shipping libMobileGL.so
// built -fvisibility=hidden.
#pragma once
namespace MGITest {
// True when this build can arm the omission at all.
bool PoisonOmissionArmAvailable();
// Withholds the stamp of `field` at `verb` (kMGPipeVerbNames / kMGPipeInputFieldNames spellings)
// for the rest of this process, in the client's fill and in the server's verb stamp. Call it on
// the thread that issues the GL calls. False when the build cannot, or a name is unknown.
bool ArmPoisonOmission(const char* verb, const char* field);
} // namespace MGITest
@@ -11,22 +11,26 @@
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
// still copying the value, which is indistinguishable from a row that was never written.
// observed when someone forgets a row is a mechanism nobody can trust, so the control forges the
// mistake on purpose: the harness marker MGITEST_POISON_OMIT names one (verb, field) pair, and the
// child arms it through the verify build's setter (Harness/PoisonOmissionArm.h), so the filler skips
// that pair's STAMP while still copying the value - indistinguishable from a row never written.
//
// The scenario asserts both halves of "on THAT verb, and only there":
//
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
// mask, and the draw's own fields are stamped normally) and the
// following glGenerateMipmap must abort naming exactly that pair.
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
// no Fatal at all. Without this half, "it aborted" would say nothing
// about WHY: a poison that fired on every verb would look just as red.
// OmittedFieldAbortsOnThatVerb - with MGITEST_POISON_OMIT=ReadPixels:GetPixelStoreParameters, the
// draw and the glGenerateMipmap must still complete (the stamp is
// withheld at ReadPixels alone, and their own fields are stamped
// normally) and the following glReadPixels must abort naming
// exactly that pair.
// WithoutOmissionCompletes - the identical sequence with the marker unset runs to completion
// with no Fatal at all. Without this half, "it aborted" would say
// nothing about WHY: a poison that fired on every verb would look
// just as red.
//
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
// entries, the second in the ambient Verify. entries (the first skips when the knob is unset).
// The omission is process-wide, so the two cases cannot share a lane: the first runs in the
// PoisonOmitted. entries, the second in the ambient Verify. entries (the first skips when the marker
// is unset).
//
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
@@ -56,6 +60,7 @@
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/HeadlessGL.h"
#include "../Harness/PoisonOmissionArm.h"
#include "../Harness/ScenarioFixture.h"
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
@@ -81,28 +86,23 @@ extern char** environ;
namespace MGITest {
namespace {
// What the PoisonOmitted. ctest entries name, which CI requires to pass by name. Each pair is
// spelled here so the assertion below is about the exact string the poison contracts to
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
// What the PoisonOmitted. and VerifySplitPoisonOmitted. ctest entries put in
// MGITEST_POISON_OMIT, and CI requires both to pass by name. The pair is spelled here so the
// assertion below is about the exact string the poison contracts to print (ARCHITECTURE.md
// 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
//
// TWO PAIRS SINCE P7 WAVE 3, ONE PER ARM, because the first one is structurally inert on
// the split arm. GenerateMipmap:GetActiveTextureUnit is the monolith control: the monolith
// backend resolves the texture to mip through the active unit. Under a transport it does
// not - since P5c hd the server's generate_mipmap resolves the texture from the verb's own
// handle (MGPipeApplier().VerbMipRes) and never reads the active unit - so omitting that
// stamp is omitting a stamp nobody reads, and the child exits 0 (measured on
// integration-verify-split, both backends). ReadPixels:GetPixelStoreParameters is the
// split control: the server's read_pixels backend call DOES read the pack half through the
// accessor, under the server's own stamp, which honours the knob in a verify build
// (MG_Backend/MGPipe/PipeInputs.cpp's ServerPoisonOmission).
// ONE PAIR FOR BOTH ARMS SINCE P13 W5. The record arm resolves glGenerateMipmap's texture
// from the verb's own handle and never reads the active unit, so the old monolith pair,
// GenerateMipmap:GetActiveTextureUnit, withheld a stamp nobody reads. ReadPixels' pack
// state is read on both arms: by the monolith record arm around its neutral-pack readback,
// and by the server's read_pixels under the server's own stamp, which withholds the pair
// too (MG_Backend/MGPipe/PipeInputs.cpp, MGPipeServerSetPoisonOmission).
//
// AND EACH PAIR SAYS WHICH ROLE HAS TO FIRE (`FiresOnTheServer`), which is the whole point
// of the split pair and was not checked at all until P7 wave 3's V1 fix round. The reader
// below concatenates the child's two role halves and the assertion searched the union, so
// `VerifySplitPoisonOmitted.` would have been just as green if the stamp the knob withheld
// had been the CLIENT's - which is the case it exists to rule out, because the mechanism
// under test is MG_Backend/MGPipe/PipeInputs.cpp's server-side stamp honouring the knob.
// The monolith pair has one role and one log half, so it keeps the union.
// AND THE PAIR SAYS WHICH ROLE HAS TO FIRE (`FiresOnTheServer`, P7 wave 3's V1 fix round).
// The reader below concatenates the child's two role halves, so `VerifySplitPoisonOmitted.`
// would be just as green if the withheld stamp had been the CLIENT's - the case it exists to
// rule out. A build without MOBILEGL_BUILD_DISAGGREGATED has one role and one log half, so
// there the union is all there is.
struct OmittedPair {
const char* Verb;
const char* Field;
@@ -110,7 +110,6 @@ namespace MGITest {
bool FiresOnTheServer; // the Fatal must be in the child's SERVER half, not merely present
};
constexpr OmittedPair kOmittedPairs[] = {
{"GenerateMipmap", "GetActiveTextureUnit", {"@DrawArrays", "@DrawArrays"}, false},
{"ReadPixels", "GetPixelStoreParameters", {"@DrawArrays", "@GenerateMipmap"}, true},
};
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
@@ -120,6 +119,11 @@ namespace MGITest {
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
constexpr const char* kWorkerFilter = "--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
// The pair to withhold, as <Verb>:<Field>, set by the PoisonOmitted. ctest entries and
// inherited by the child. A HARNESS marker (hence MGITEST_): the library never reads it,
// and the worker arms the omission itself (Harness/PoisonOmissionArm.h).
constexpr const char* kOmissionMarker = "MGITEST_POISON_OMIT";
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
@@ -265,14 +269,14 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
glFinish();
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
// A texture verb between the draw and the readback; the omission does not name it.
glBindTexture(GL_TEXTURE_2D, texture);
glGenerateMipmap(GL_TEXTURE_2D);
glFinish();
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
// The split arm's verb (kOmittedPairs[1]): a readback, which is a round trip, so
// the server has applied it - and read the pack state - before this returns.
// The verb the omission names (kOmittedPairs): a readback, which is a round trip,
// so the server has applied it - and read the pack state - before this returns.
unsigned char centre[4] = {};
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, centre);
std::fprintf(stderr, "[itest] poison worker: glReadPixels returned\n");
@@ -382,6 +386,18 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
}
if (!Ready()) return;
// The omission the marker names, armed on this thread - the one that issues the GL calls
// - before the sequence's first verb. Without the marker nothing is armed, which is
// WithoutOmissionCompletes' half.
if (const char* marker = std::getenv(kOmissionMarker); marker != nullptr && *marker != '\0') {
const std::string pair(marker);
const std::string::size_type colon = pair.find(':');
ASSERT_NE(colon, std::string::npos) << kOmissionMarker << "=" << pair << " is not <Verb>:<Field>";
ASSERT_TRUE(ArmPoisonOmission(pair.substr(0, colon).c_str(), pair.substr(colon + 1).c_str()))
<< "could not arm the omission " << kOmissionMarker << "=" << pair
<< ": the build has no setter (-DMOBILEGL_PIPE_VERIFY=ON compiles it) or a name is unknown";
}
RunSequence();
#if MGITEST_POISON_HAVE_FORK
@@ -404,21 +420,22 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
if (!Ready()) return;
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
GTEST_SKIP() << "this case is the poison's negative control and needs "
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
"is what the PoisonOmitted. ctest entries set";
if (!StringKnobIsSet(kOmissionMarker)) {
GTEST_SKIP() << "this case is the poison's negative control and needs " << kOmissionMarker
<< "=<Verb>:<Field>, which is what the PoisonOmitted. ctest entries set";
}
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
const std::string marker = std::getenv(kOmissionMarker);
const OmittedPair* pair = nullptr;
for (const OmittedPair& candidate : kOmittedPairs) {
if (knob == std::string(candidate.Verb) + ":" + candidate.Field) pair = &candidate;
if (marker == std::string(candidate.Verb) + ":" + candidate.Field) pair = &candidate;
}
if (pair == nullptr) {
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
<< "how to provoke " << kOmittedPairs[0].Verb << ":" << kOmittedPairs[0].Field << " and "
<< kOmittedPairs[1].Verb << ":" << kOmittedPairs[1].Field;
GTEST_SKIP() << kOmissionMarker << " is " << marker << ", but this case only knows how to provoke "
<< kOmittedPairs[0].Verb << ":" << kOmittedPairs[0].Field;
}
ASSERT_TRUE(PoisonOmissionArmAvailable())
<< kOmissionMarker << " is set, but only a -DMOBILEGL_PIPE_VERIFY=ON build compiles the setter "
<< "the child arms the omission with (Harness/PoisonOmissionArm.h), so this control cannot run here";
const std::string expectedPair = std::string(pair->Field) + "@" + pair->Verb;
int status = 0;
@@ -452,10 +469,10 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
<< childLog;
#if MOBILEGL_BUILD_DISAGGREGATED
// WHICH ROLE, for the split pair (P7 wave 3, V1). The union above answers "did the
// poison fire", which is all the monolith pair can be asked; this pair exists to prove
// that the SERVER'S OWN verb stamp honours MOBILEGL_PIPE_POISON_OMIT
// (MG_Backend/MGPipe/PipeInputs.cpp's ServerPoisonOmission) - the thing that was NOT
// true before wave 3 and made the control silently green. A client-side firing would
// poison fire", which is all a one-role build can be asked; this pair exists to prove
// that the SERVER'S OWN verb stamp honours the omission
// (MG_Backend/MGPipe/PipeInputs.cpp's MGPipeServerSetPoisonOmission) - the thing that was
// NOT true before wave 3 and made the control silently green. A client-side firing would
// satisfy the union and prove the opposite of what the entry claims, so the half is
// named here.
if (pair->FiresOnTheServer) {
@@ -481,11 +498,11 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
}
// The ambient half of the pair, in the ambient Verify. lanes: the same sequence with the
// knob UNSET must run to completion and log no Fatal at all. The PoisonOmitted. entries are
// the armed half (knob set, the verb must abort), and CI requires those entries by name.
// marker UNSET must run to completion and log no Fatal at all. The PoisonOmitted. entries are
// the armed half (marker set, the verb must abort), and CI requires those entries by name.
//
// It does not skip when MOBILEGL_PIPE_POISON_OMIT is set: run by hand with the knob
// exported, it goes red, and the note in its failure says that is the poison firing.
// It does not skip when MGITEST_POISON_OMIT is set: run by hand with the marker exported,
// it goes red, and the note in its failure says that is the poison firing.
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
if (!Ready()) return;
@@ -493,7 +510,7 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
"build there is nothing for this control to be a control OF";
}
const bool omissionArmed = StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT");
const bool omissionArmed = StringKnobIsSet(kOmissionMarker);
int status = 0;
std::string reason;
@@ -501,10 +518,10 @@ void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
const std::string childLog = ReadChildLog();
const std::string note =
omissionArmed
? std::string(" NOTE: MOBILEGL_PIPE_POISON_OMIT is set in this process, so this failure is "
"expected: the poison IS armed, and this entry going red is the proof.")
: std::string();
omissionArmed ? std::string(" NOTE: ") + kOmissionMarker +
" is set in this process, so this failure is expected: the poison IS armed, and "
"this entry going red is the proof."
: std::string();
ASSERT_TRUE(WIFEXITED(status))
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
<< DescribeStatus(status)
+2 -2
View File
@@ -10,8 +10,8 @@
// MGP_FILL_VERB_LIST: every GLFunctionsTable function pointer in declaration order, with its
// class (gen_pipe.py refuses any other set). MGP_FILL_FIELD_LIST: the fields a class may read;
// sticky fields (MGP_COVERAGE_STICKY_LIST) are implicit. Fix Fatal{UnmigratedPipeInput} by
// adding a row, never by making it sticky. A row is removed only on a MOBILEGL_PIPE_POISON_OMIT
// run of the full CTS caselist on both devices; a corpus that never reaches a path proves nothing.
// adding a row, never by making it sticky. A row is removed only after a poison build without it
// ran the full CTS caselist on both devices; a corpus that never reaches a path proves nothing.
// gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro.
// clang-format off
+1 -1
View File
@@ -15,7 +15,7 @@
// materialising a queued clear, overriding a sampler's filter) would leave the copy stale, so
// the mutator calls MGP_NOTE_MUTATION(Field) where it moves the value. The notice refreshes
// that one field when it belongs to the verb in flight. It never touches the poison stamp, so a
// stamp withheld by MOBILEGL_PIPE_POISON_OMIT stays withheld.
// stamp the fill withheld (negative control B) stays withheld.
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext).
+17 -61
View File
@@ -75,10 +75,8 @@ namespace {
void SetUp() override {
m_previous = MG_State::pGLContext.Take();
MG_State::pGLContext = MakeUnique<GLContext>();
MGPipeSetPoisonOmission(nullptr, nullptr);
}
void TearDown() override {
MGPipeSetPoisonOmission(nullptr, nullptr);
MG_State::pGLContext = Move(m_previous);
}
SharedPtr<GLContext> m_previous;
@@ -88,11 +86,20 @@ namespace {
Bool Fresh(MGPipeInputField field) {
return MGPipeInputFieldIsFresh(gPipeInputs->FilledState(), field);
}
// Negative control B's omission, made here rather than by the library: fill `verb`, then put
// the stamp of `field` back to what it held before the fill. The value is copied and only the
// stamp is withheld, which is what a FillPoints.def row nobody wrote leaves behind.
void FillWithholdingTheStamp(MGPipeVerb verb, MGPipeInputField field) {
auto& filled = const_cast<MGPipeFilledState&>(gPipeInputs->FilledState());
const Uint64 before = filled.FilledGen[static_cast<SizeT>(field)];
MGPipeValidateForVerb(verb);
filled.FilledGen[static_cast<SizeT>(field)] = before;
}
#endif
[[maybe_unused]] constexpr const char* kOmittedFatal =
"Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}";
[[maybe_unused]] constexpr const char* kOmissionKnob = "GenerateMipmap:GetActiveTextureUnit";
#if MGTEST_HAVE_FORK
struct ChildResult {
@@ -139,7 +146,7 @@ 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.
// mask) nor loses one of its own. The omission is the test's own (FillWithholdingTheStamp).
TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
@@ -148,8 +155,7 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
FillWithholdingTheStamp(MGPipeVerb::GenerateMipmap, MGPipeInputField::GetActiveTextureUnit);
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
// The value was still copied: only the stamp is withheld.
@@ -173,15 +179,15 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
// A sticky field stays fresh across every verb.
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
// And the omission is scoped to its verb: a different verb of the same class keeps it.
// 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));
#endif
}
// Negative control B, layer 2 (G5): the read itself. The child fills GenerateMipmap with the
// omission and reads gPipeInputs->GetActiveTextureUnit(); the parent expects SIGABRT and the
// exact Fatal line, and that nothing else was fatal.
// stamp of GetActiveTextureUnit withheld and reads gPipeInputs->GetActiveTextureUnit(); the
// parent expects SIGABRT and the exact Fatal line, and that nothing else was fatal.
TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
@@ -190,10 +196,9 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
(void)gPipeInputs->GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
FillWithholdingTheStamp(MGPipeVerb::GenerateMipmap, MGPipeInputField::GetActiveTextureUnit);
(void)gPipeInputs->GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs->GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3); // reached only if the poison failed
@@ -253,53 +258,6 @@ TEST_F(PipeInputsTest, ReadingBeforeAnyFillAbortsNamingNoVerb) {
#endif
}
// The MOBILEGL_PIPE_POISON_OMIT parser, through the Features field the loader fills: the
// child sets the knob after its parent filled without it, and the first fill after that
// parses it, logs the arming line and withholds the stamp exactly as the programmatic form.
TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) {
#if !MOBILEGL_PIPE_POISON
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#elif !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
MGPipeValidateForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = kOmissionKnob;
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
(void)gPipeInputs->GetTextureUnitObject(0); // the sibling field: filled, must not abort
(void)gPipeInputs->GetActiveTextureUnit(); // the omitted field: Fatal
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap"), std::string::npos)
<< r.Log;
EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log;
EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log;
#endif
}
// An unknown verb in the knob is Fatal{PipeVerifyBadKnob} naming the knob and the value,
// before any stamp is withheld.
TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) {
#if !MGTEST_HAVE_FORK
GTEST_SKIP() << "no fork() on this platform";
#else
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
const ChildResult r = RunInChild([] {
MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit";
MGPipeValidateForVerb(MGPipeVerb::GenerateMipmap);
::_exit(3);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit\": "
"no such verb in kMGPipeVerbNames}"),
std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("poison omission armed"), std::string::npos) << r.Log;
#endif
}
// Negative control A at the block level (G4): a clean snapshot compares equal to the pushed
// block over the verb's mask; corrupting one field of the snapshot makes the compare name
// exactly that field, at the serial of the fill it belongs to.
@@ -566,8 +524,7 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
#else
auto& ctx = *MG_State::pGLContext;
MGPipeSetPoisonOmission("DrawArrays", "GetSamplingResolutionGeneration");
MGPipeValidateForVerb(MGPipeVerb::DrawArrays);
FillWithholdingTheStamp(MGPipeVerb::DrawArrays, MGPipeInputField::GetSamplingResolutionGeneration);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
EXPECT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration))
@@ -575,7 +532,6 @@ TEST_F(PipeInputsTest, TheMutationNoticeRefreshesTheValueButNotTheStamp) {
// FenceSync is a kQuery verb: its mask holds no texture field at all, so the notice must
// leave the generation unfilled and a read of it Fatal{UnmigratedPipeInput}.
MGPipeSetPoisonOmission(nullptr, nullptr);
MGPipeValidateForVerb(MGPipeVerb::FenceSync);
ASSERT_FALSE(Fresh(MGPipeInputField::GetSamplingResolutionGeneration));
ctx.BumpSamplingResolutionGeneration();
@@ -47,7 +47,7 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
| 变量 | 默认 | 说明 |
|---|---|---|
| `MOBILEGL_PIPE_PUSH` | `0x3fff` | 子系统位图(`MG_Pipe/MGPipe.h`,位永不复用);P13 起位 0–13 固定开,清任何一位都在启动时具名拒绝;只剩位 63("关 CSO 内容寻址"的行为对照)可设 |
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` / `_POISON_OMIT` | 0 / 1 / 空 / 空 | 影子比对与两个阴性对照。比对器只编进 verify 构建,其他构建里 `MOBILEGL_PIPE_VERIFY=1` 只告警;`_VERIFY_CORRUPT` 不进 `Config.h`,只有 verify 构建在比对器武装时读它(`PipeFill.cpp` 的 `ArmVerify`,同样经环境与系统配置文件) |
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` | 0 / 1 / 空 | 影子比对与它的阴性对照。比对器只编进 verify 构建,其他构建里 `MOBILEGL_PIPE_VERIFY=1` 只告警;`_VERIFY_CORRUPT` 不进 `Config.h`,只有 verify 构建在比对器武装时读它(`PipeFill.cpp` 的 `ArmVerify`,同样经环境与系统配置文件)。poison 的阴性对照没有运行时开关:集成测试的 harness 标记 `MGITEST_POISON_OMIT` 由重新 exec 的子进程经 verify 构建才有的 `MGPipeSetPoisonOmission` 武装(`Harness/PoisonOmissionArm.h`) |
| `MOBILEGL_PIPE_STATS` / `_STATS_PERIOD` / `_STATS_FILE` | 0 / 120 / 空 | 边界计数器(附 B);dump 按角色写 `<base>.client.json` / `<base>.server.json` |
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64 | 索引镜像预算(P8);`_LEGACY_MEMOS`、`_TEXEL_RETAIN_MB`、`_HANDLE_ABA_CONTROL` 随各自的臂在 P13 删除 |
+1
View File
@@ -62,6 +62,7 @@ them wrong.
| 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 |
## Kept after investigation
-4
View File
@@ -10503,14 +10503,12 @@
"PipeCatalogue.WireOpcodesAreThePositionsInTheCatalogue",
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.AFrontendMutationInsideAVerbRefreshesThePushedField",
"PipeInputsTest.BadPoisonOmitKnobIsFatalNamingTheKnob",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.EveryVerbFillsItsClassAndNothingElse",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale",
"PipeInputsTest.PoisonOmitKnobArmsTheOmission",
"PipeInputsTest.ReadingAFilledFieldCompletes",
"PipeInputsTest.ReadingAnOmittedFieldAbortsNamingTheVerb",
"PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb",
@@ -16801,14 +16799,12 @@
"PipeCatalogue.WireOpcodesAreThePositionsInTheCatalogue",
"PipeInputsTest.AFrontendMutationInsideAVerbDoesNotDivergeAtRead",
"PipeInputsTest.AFrontendMutationInsideAVerbRefreshesThePushedField",
"PipeInputsTest.BadPoisonOmitKnobIsFatalNamingTheKnob",
"PipeInputsTest.BadVerifyCorruptKnobIsFatalNamingTheKnob",
"PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial",
"PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator",
"PipeInputsTest.EveryVerbFillsItsClassAndNothingElse",
"PipeInputsTest.MutatedFieldIsNamedAtRead",
"PipeInputsTest.OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale",
"PipeInputsTest.PoisonOmitKnobArmsTheOmission",
"PipeInputsTest.ReadingAFilledFieldCompletes",
"PipeInputsTest.ReadingAnOmittedFieldAbortsNamingTheVerb",
"PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb",
+4 -4
View File
@@ -237,8 +237,8 @@ endif()
# * it carries none of the three MGPipe Fatals: Fatal{PipeVerifyDiffer (a push/pull divergence,
# the thing the mode exists to find), Fatal{UnmigratedPipeInput (a backend read of a field the
# verb's fill table does not list - fixed by adding the row to MG_Pipe/FillPoints.def, never by
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT
# or MOBILEGL_PIPE_POISON_OMIT). The third is in the regex on purpose even though D2 makes it
# marking the field sticky), and Fatal{PipeVerifyBadKnob (a misspelt MOBILEGL_PIPE_VERIFY_CORRUPT).
# The third is in the regex on purpose even though D2 makes it
# abort the process: with MOBILEGL_PIPE_VERIFY_FATAL=0 the abort is exactly what does not
# happen, and a typo'd knob would otherwise leave the negative-control lane looking healthy.
# The Fatal check is not redundant with the replay's exit status: MOBILEGL_PIPE_VERIFY_FATAL=0 is
@@ -286,8 +286,8 @@ if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQU
"divergence and is recorded, not silenced; Fatal{UnmigratedPipeInput, \"<Field>@<Verb>\"} "
"is a missing row in MG_Pipe/FillPoints.def's class table - add it, regenerate, rerun "
"(never mark the field sticky); Fatal{PipeVerifyBadKnob, ...} is a misspelt "
"MOBILEGL_PIPE_VERIFY_CORRUPT / MOBILEGL_PIPE_POISON_OMIT - fix the spelling, the "
"vocabularies are kMGPipeInputFieldNames[] and kMGPipeVerbNames[].")
"MOBILEGL_PIPE_VERIFY_CORRUPT - fix the spelling, the vocabulary is "
"kMGPipeInputFieldNames[].")
endif()
message(STATUS "MGPipe verify: ${pipe_verify_case} armed, zero divergences, zero unmigrated reads")
endif()