[Merge] (MGPipe, P5): integrate package p1

This commit is contained in:
2026-09-11 15:57:13 -04:00
14 changed files with 2418 additions and 4 deletions
+21
View File
@@ -1664,6 +1664,27 @@ jobs:
python3 scripts/gen_pipe_dirty_surface.py --check
python3 scripts/gen_pipe_dirty_surface.py --self-test
# A GATE as of P5 (R-7.1, CONTRACT-P5.md table 2). The eighth generator, and the only one
# whose output the step above cannot cover: `gen_pipe.py` does not write
# PipeFieldOwnership.inc, so regenerating the seven and diffing MG_Pipe/generated leaves a
# drifted ownership table green. The build-level static_assert only catches an
# UNCLASSIFIED row; a hand edit that CHANGES a class compiles clean, and --check is the
# only thing in the tree that catches it.
#
# NO BRANCH GUARD, deliberately, unlike the two G5 steps below: this asks "does the
# committed table still follow from the .def files", which is a question every branch can
# answer and every branch wants answered. The table outlives feat/disaggregated.
#
# --self-test is not optional, for the reason the two gates above give and for one this
# generator learned the hard way: its first version counted eleven trips when one control
# was a silent duplicate of another, because the harness asked "did something exit" rather
# than "did THIS exit". Every control now asserts its own message, and a harness control
# asserts that the harness still rejects someone else's exit.
- name: MGPipe PipeInputs field-ownership table is complete (R-7.1, table 2)
run: |
python3 scripts/gen_pipe_field_ownership.py --check
python3 scripts/gen_pipe_field_ownership.py --self-test
# A GATE as of P3a (G5). "pool 与延迟释放原样搬" (ROADMAP.md:19) is meant literally: the
# buffer pool, the deferred-release drain and the three persistently mapped rings move
# VERBATIM, and ARCHITECTURE.md:515 says why - their retire happens only inside Present, so
+144
View File
@@ -16,6 +16,11 @@
#include <cstdint>
#include <cstring>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Util/Metrics/PipeStats.h>
#endif
namespace MobileGL::MG_Pipe {
const char* MGPipeVerbName(MGPipeVerb verb) {
const auto index = static_cast<SizeT>(verb);
@@ -42,6 +47,145 @@ namespace MobileGL::MG_Pipe {
return std::nullopt;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ================================================================================
// P5: the server's verb stamp, the residual-pull counter, and the four-way read verdict
// ================================================================================
namespace {
Uint64 g_residualPulls = 0;
// The strict arm of R-7.3. Same first line as the ordinary poison Fatal, so every
// existing filter on Fatal{UnmigratedPipeInput still matches, plus the class and the
// phase that retires it - a strict abort that did not say which phase owes the answer
// would leave the reader exactly where the gate found them.
[[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb) {
const SizeT index = static_cast<SizeT>(field);
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, "
"MOBILEGL_IPC_STRICT_ERRORS=1, retires in %s]",
kMGPipeInputFieldNames[index], MGPipeVerbName(verb), kMGPipeFieldRetiringPhase[index]);
std::abort();
}
// One place decides what a BARRIER-PULLED read does, so the field accessors and the
// seven sticky forwards cannot drift apart on it.
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
++g_residualPulls;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
}
if (MG_Config::Ipc.StrictErrors) StrictBarrierPullFatal(field, verb);
}
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
// respects it for the same reason the client's residual fill does: a field outside the
// verb's class is one the fill never copied, so answering it out of gPipeInputs would
// hand the server the PREVIOUS verb's value - the exact staleness the generation poison
// exists to catch, re-introduced by the very mechanism meant to instrument it.
Bool FieldIsInVerbClass(MGPipeInputField field, MGPipeVerb verb) {
const SizeT verbIndex = static_cast<SizeT>(verb);
if (verbIndex >= kMGPipeVerbCount) return false;
const MGPipeVerbClass verbClass = kMGPipeVerbClass[verbIndex];
return MGPipeFieldMaskHas(kMGPipeClassFieldMask[static_cast<SizeT>(verbClass)], field);
}
} // namespace
// The third door into the storage (see PipeInputs.h). It exists because neither of the
// other two can be the one that stamps: MGPipeApplyAccess deliberately does not, and
// MGPipeFillAccess lives in MG_Impl, the role a server does not have.
struct MGPipeStampAccess {
static MGPipeFilledState& Filled(PipeInputs& inputs) { return inputs.m_filled; }
static void SetVerb(PipeInputs& inputs, MGPipeVerb verb) { inputs.m_currentVerb = verb; }
static void SetServerStamped(PipeInputs& inputs, Bool stamped) {
inputs.m_serverStampedVerb = stamped;
}
};
void MGPipeServerStampVerbBoundary(MGPipeVerb verb) {
PipeInputs& inputs = gPipeInputs;
MGPipeFilledState& filled = MGPipeStampAccess::Filled(inputs);
MGPipeStampAccess::SetVerb(inputs, verb);
// Starts at 1 for MGPipeValidateForVerb's reason: FilledGen == 0 is "never filled" on
// BOTH branches of MGPipeInputFieldIsFresh, so the zeroing below is a real withdrawal
// rather than a stamp that happens to be old.
++filled.CurrentVerbSerial;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
const MGPipeFieldOwnership ownership = kMGPipeFieldOwnership[i];
// STAMPED: in this verb's class AND answerable out of the records the applier has
// already applied. WITHDRAWN (0): everything else - which is BARRIER-PULLED, FATAL,
// and anything the verb's own may-read table says this verb does not read.
//
// The withdrawal is the load-bearing half of the rule: the client's residual fill
// stamped all 63 fields at its own verb boundary, so without it every field would
// read fresh on the server, `rsp` would be identically 0 and the exit gate would be
// decoration. It also cancels the sticky exemption for free - generated/
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so 0 wins over
// kMGPipeInputFieldSticky without a line of the generated file changing.
const Bool answerable = (ownership == MGPipeFieldOwnership::kRecordSupplied ||
ownership == MGPipeFieldOwnership::kApplierDerived) &&
FieldIsInVerbClass(field, verb);
filled.FilledGen[i] = answerable ? filled.CurrentVerbSerial : 0;
}
MGPipeStampAccess::SetServerStamped(inputs, true);
}
void MGPipeServerClearVerbBoundary() { MGPipeStampAccess::SetServerStamped(gPipeInputs, false); }
Uint64 MGPipeResidualPullCount() { return g_residualPulls; }
void MGPipeResetResidualPullCountForTesting() { g_residualPulls = 0; }
void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped) {
// OUTSIDE A SERVER-STAMPED VERB THIS IS THE MONOLITH ANSWER, UNCHANGED. A split BUILD
// running MOBILEGL_TRANSPORT=monolith - every unit and integration-gpu lane of
// build-split - has a client that stamped all 63 fields, so a stale read there is the
// same defect it is in a verify build. Softening it on the build rather than on the
// stamp would take 1842 unit cases' ability to go red away with it.
//
// AND A READ OUTSIDE THE VERB'S OWN CLASS IS STILL FATAL even for a BARRIER-PULLED
// field: the value it would be answered with was never copied for this verb, so
// counting it would trade a loud staleness for a quiet one.
if (!serverStamped ||
kMGPipeFieldOwnership[static_cast<SizeT>(field)] != MGPipeFieldOwnership::kBarrierPulled ||
!FieldIsInVerbClass(field, verb)) {
MGPipeInputPoisonFatalForVerb(field, verb);
}
CountBarrierPull(field, verb);
}
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped) {
if (!serverStamped) return false;
const MGPipeFieldOwnership narrowed = MGPipeFieldOwnershipOf(field, arg0);
if (narrowed == MGPipeFieldOwnershipOf(field)) return false; // the argument narrows nothing
if (narrowed == MGPipeFieldOwnership::kFatal) {
// The field's own stamp says fresh - the applier really did write the half that has
// a carrier - so only the argument can say that THIS read is unserved. THE MESSAGE
// NAMES THE ARGUMENT, because without it this line is byte-identical to what a
// genuinely stale read of the OTHER half would print, and the whole case for
// narrowing by argument rather than by a second field id is that the reader is told
// which half they asked for.
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [argument 0 = %u is %s while the "
"field is %s]",
kMGPipeInputFieldNames[static_cast<SizeT>(field)], MGPipeVerbName(verb), arg0,
MGPipeFieldOwnershipName(narrowed),
MGPipeFieldOwnershipName(MGPipeFieldOwnershipOf(field)));
std::abort();
}
if (narrowed == MGPipeFieldOwnership::kBarrierPulled) {
CountBarrierPull(field, verb);
return true; // decided here; the field-level check must not count it again
}
return true;
}
void MGPipeStickyForwardPull(MGPipeInputField field) {
// The seven carry no MGP_INPUT_CHECK at all (the declared exception argued at
// PipeInputs.h's F-class block), so freshness can never reach them and neither can the
// stamp's withdrawal. This is the only thing that puts them in `rsp`.
if (!gPipeInputs.ServerStampedVerb()) return;
CountBarrierPull(field, gPipeInputs.CurrentVerb());
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_PIPE_VERIFY
namespace {
using CurrentVertexAttributeValue = PipeInputs::CurrentVertexAttributeValue;
+178 -1
View File
@@ -26,6 +26,14 @@
#endif
namespace MobileGL::MG_Pipe {
// TABLE 2 (CONTRACT-P5.md section 3, R-7): the four ownership classes, one per field, plus
// the seven sticky forwards' own rows. Included HERE rather than from MG_Pipe/MGPipe.h with
// gen_pipe.py's seven outputs, deliberately: MGPipe.h is in the PULL build's include closure
// and G1 admits no symbol motion there, while this header is reached only through
// PipeInputsSwitch.h's MOBILEGL_PIPE_PUSH arm. It is also exactly the header the poison check
// below and the server's verb stamp both already see.
#include <MG_Pipe/generated/PipeFieldOwnership.inc>
// PipeInputs.cpp. The poison Fatal with the verb's name ("<none>" before the first
// verb): MGLOG_F + std::abort(), live at every log level on purpose - this is not
// MOBILEGL_ASSERT, which is inert in INFO builds.
@@ -37,18 +45,72 @@ namespace MobileGL::MG_Pipe {
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
Optional<MGPipeVerb> MGPipeFindVerb(const char* name);
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- P5: the split arm of the read check (R-7.2, R-7.3) ------------------------------
//
// A stale read stops being one answer and becomes FOUR, keyed on the field's table-2 class
// - which is what turns the generated table from a document into a runtime mechanism:
//
// RECORD-SUPPLIED / APPLIER-DERIVED the server could answer it and did not: a real
// defect. Fatal, exactly as today.
// BARRIER-PULLED the server is reading the value the client's
// residual fill left in gPipeInputs while the verb
// barrier holds both threads apart (R-1). LEGAL, and
// COUNTED: PipeStats::CallClass::ResidualPulls. Under
// MOBILEGL_IPC_STRICT_ERRORS=1 it is Fatal instead.
// FATAL no carrier and the reduced path never reads it.
//
// AND IT IS ARMED ONLY INSIDE A SERVER-STAMPED VERB (PipeInputs::ServerStampedVerb).
// A split BUILD running monolith transport - which is every unit and integration-gpu lane
// of build-split - has a client that fills and stamps all 63 fields at every verb, so a
// stale read there is the same defect it is in a verify build and gets the same Fatal.
// Without that condition the leniency would apply to lanes whose stamps are the client's,
// and 1842 unit cases would quietly stop being able to go red.
void MGPipeInputUnfreshRead(MGPipeInputField field, MGPipeVerb verb, Bool serverStamped);
// The same decision for an accessor that takes an argument the table narrows on
// (kMGPipeFieldArgumentOwnership). Called BEFORE the freshness test, because the narrowed
// class is a statement about the argument rather than about the stamp: the pack half of
// GetPixelStoreParameters is stamped and fresh while the unpack half has no carrier and no
// backend reader at all.
//
// RETURNS TRUE WHEN THE ARGUMENT ROW DECIDED, and the caller then skips the field-level
// check. Today the only row narrows to FATAL, which aborts, so the return value changes
// nothing; the day a row narrows to BARRIER-PULLED it is what stops the field-level check
// from either counting the same read twice or - worse, because the field's own class would
// not be BARRIER-PULLED - aborting a read the argument row had just declared legal.
Bool MGPipeInputArgumentRead(MGPipeInputField field, Uint32 arg0, MGPipeVerb verb, Bool serverStamped);
#endif
// The read-side poison check, on every non-forwarded accessor. Under MOBILEGL_PIPE_POISON
// a read of a field whose stamp is older than the current verb serial is
// Fatal{UnmigratedPipeInput, "Field@Verb"}; otherwise the accessor is a plain load.
#if MOBILEGL_PIPE_POISON
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputUnfreshRead((Field), m_currentVerb, m_serverStampedVerb); \
} \
} while (0)
#define MGP_INPUT_CHECK_ARG(Field, Arg0) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputArgumentRead((Field), static_cast<Uint32>(Arg0), m_currentVerb, \
m_serverStampedVerb)) { \
MGP_INPUT_CHECK(Field); \
} \
} while (0)
#else
#define MGP_INPUT_CHECK(Field) \
do { \
if (!::MobileGL::MG_Pipe::MGPipeInputFieldIsFresh(m_filled, (Field))) { \
::MobileGL::MG_Pipe::MGPipeInputPoisonFatalForVerb((Field), m_currentVerb); \
} \
} while (0)
#define MGP_INPUT_CHECK_ARG(Field, Arg0) MGP_INPUT_CHECK(Field)
#endif
#else
#define MGP_INPUT_CHECK(Field) ((void)0)
#define MGP_INPUT_CHECK_ARG(Field, Arg0) ((void)0)
#endif
// The compare-at-read hook of the MOBILEGL_PIPE_VERIFY comparator (P1 brief D8), defined
// in MG_Impl/Pipe/PipeFill.cpp: re-reads the field from the live context and compares it
@@ -194,6 +256,26 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_PIPE_POISON
const MGPipeFilledState& FilledState() const { return m_filled; }
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// TRUE between the server's verb-boundary stamp and whoever clears it. It is the
// arming condition of the whole split read path: only inside a server-stamped verb is
// a BARRIER-PULLED read counted rather than Fatal, and only there is a sticky forward
// a residual pull rather than an ordinary monolith call. A split build running
// monolith transport never sets it, which is why build-split's unit and integration
// cases behave exactly as a verify build's do.
//
// CLEARING IT IS THE APPLIER'S JOB AND NOT THE CLIENT'S, even though the client also
// does it. MGPipeValidateForVerb and MGPipeLeaveVerb both call
// MGPipeServerClearVerbBoundary, which is sufficient for inproc, where both roles share
// one process and one gPipeInputs - and misleading for P6, where MG_Impl is not in the
// server at all. There this flag would latch TRUE for the life of the server after the
// first stamp, every later read anywhere would be judged against the last verb's mask,
// and MGPipeStickyForwardPull would stop being a no-op outside a verb - so
// InvalidateCompileEnv reached from a later context's backend initialisation, the exact
// case the sticky exemption was written for, would be counted and, under strict, would
// abort. So: PipeApplier clears on leaving the applier. Not optional.
Bool ServerStampedVerb() const { return m_serverStampedVerb; }
#endif
// ---- V: values ----
Int GetActiveTextureUnit() const {
@@ -345,8 +427,17 @@ namespace MobileGL::MG_Pipe {
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetRenderStateParametersVersion, 0, 0);
return m_renderStateParametersVersion;
}
// THE ONE FIELD TABLE 2 NARROWS BY ARGUMENT. m_pixelStore[2] is one array indexed by
// this accessor's own argument, exactly as m_bufferBindingSlot[15] is indexed by a
// BufferTarget, and Coverage.def:62-69 already rules that such a field stays ONE row.
// Only [0] (pack) has a carrier - set_pixel_pack_state, which the applier writes
// (PipeApply.cpp:1373) - so the field is APPLIER-DERIVED and the UNPACK half is FATAL:
// every MGB_CTX->GetPixelStoreParameters site in the tree passes false
// (DirectGLES.cpp:7924, :9399, :10893, :11272, Utils.cpp:2302,
// VulkanRenderer.cpp:10980), and PipeFill.cpp's EmitPixelPackState says the same from
// the other side: "nothing on the far side of the boundary reads unpack state".
PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const {
MGP_INPUT_CHECK(MGPipeInputField::GetPixelStoreParameters);
MGP_INPUT_CHECK_ARG(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u);
MGP_INPUT_VERIFY_READ(MGPipeInputField::GetPixelStoreParameters, isUnpack ? 1u : 0u, 0);
return m_pixelStore[isUnpack ? 1 : 0];
}
@@ -616,6 +707,12 @@ namespace MobileGL::MG_Pipe {
// does NOT stamp the poison generations - a stamp says "the filler published this
// for THIS verb", which is the walk's statement, not the applier's.
friend struct MGPipeApplyAccess;
// THE THIRD DOOR, and the one the split phase needed that neither of the two above
// could be: the SERVER's verb-boundary stamp (PipeInputs.cpp). MGPipeApplyAccess
// deliberately does not stamp - see its comment above - and MGPipeFillAccess lives in
// MG_Impl, which is the role the server does not have. So the stamp gets a door of its
// own rather than a relaxation of either existing one.
friend struct MGPipeStampAccess;
// ---- identity ----
const void* m_contextIdentity = nullptr;
@@ -624,6 +721,9 @@ namespace MobileGL::MG_Pipe {
#if MOBILEGL_PIPE_POISON
MGPipeFilledState m_filled{};
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
Bool m_serverStampedVerb = false;
#endif
// ---- V ----
Int m_activeTextureUnit = 0;
@@ -713,6 +813,83 @@ namespace MobileGL::MG_Pipe {
// The docs budget ~20 KB; the block is a few KB.
static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget");
#if MOBILEGL_BUILD_DISAGGREGATED
// ============================================================================
// P5: the server-side verb stamp, and the counter that sizes what it leaves behind
// ============================================================================
//
// THE PREREQUISITE NOBODY ELSE OWNS (CONTRACT-P5.md section 3). Nothing stamps the poison
// generations on the applier side today and that is deliberate (see MGPipeApplyAccess'
// comment above: a stamp is the filler's statement, not the applier's). Under split the
// filler is in the other role, so without this every FilledGen[] would stay 0,
// MGPipeInputFieldIsFresh would answer false for EVERYTHING, and a purely server-side read
// would abort on the first field inside SyncRenderState - before any interesting case.
//
// THE RULE, in three lines, and the third one is the load-bearing one:
//
// 1. bump CurrentVerbSerial and set the verb, so a Fatal names it instead of "<none>";
// 2. stamp every RECORD-SUPPLIED and APPLIER-DERIVED field with the new serial - those
// are exactly the fields the records this verb carried can answer;
// 3. ZERO every BARRIER-PULLED and FATAL field's stamp.
//
// (3) is what makes the instrumentation real. The client's residual fill stamps ALL 63
// fields at its own verb boundary (PipeFill.cpp step 4), so without the zeroing every
// field would read fresh on the server, `rsp` would be identically 0, and the gate would
// be decoration - the precise "an inproc implementation proves nothing" failure R-2
// exists to prevent. Zeroing also cancels the sticky exemption for free: generated/
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so gen == 0 wins.
//
// The value a BARRIER-PULLED read then gets is still the client's residual fill's, and it
// is still CORRECT - because the verb barrier (R-1) leaves exactly one of the two threads
// runnable. That is the debt, not a bug; `rsp` is its size.
//
// v1 calls this from Server/PipeApplier::StampVerbBoundary. MGPipeVerbForWireOp maps the
// record's op onto a verb and answers kVerbCount for an op that is not verb-shaped, which
// is the case the applier must NOT stamp on: a set_dynamic_state between two draws is not
// a new verb, and stamping there would retire the previous verb's answers early.
//
// TWO THINGS THE CALLER INHERITS AND SHOULD NOT REDISCOVER:
//
// (a) The records BETWEEN two boundaries apply under the earlier boundary's stamp - its
// serial, its class mask and its verb NAME. That is correct today because every
// MGPipeApply* entry point touches gPipeInputs through MGPipeApplyAccess, which
// carries no MGP_INPUT_CHECK; the day one of them calls back into the backend, its
// reads will be judged against a verb they do not belong to.
// (b) The verb a draw record stamps is MGPipeVerb::DrawArrays for ALL TWENTY draw verbs.
// The class mask is right (FillPoints.def puts all twenty in kDraw) and the NAME in a
// Fatal is not: a glDrawElements that aborts will say "@DrawArrays". draw_vbo carries
// no verb id, so fixing it means either a field on the record or a second argument
// here; it is cosmetic for the verdict and misleading for the reader, and it belongs
// with whatever phase widens draw_vbo to the multi-draw family (P8).
void MGPipeServerStampVerbBoundary(MGPipeVerb verb);
// MANDATORY for the applier when it leaves the verb. The client's own MGPipeValidateForVerb
// and MGPipeLeaveVerb call it too, which is enough for inproc and NOT enough for a spawned
// server, where MG_Impl is not in the process - see ServerStampedVerb() above for what
// latching TRUE would do to the sticky forwards.
void MGPipeServerClearVerbBoundary();
// `rsp`. Also published per frame through PipeStats::CallClass::ResidualPulls; this is the
// raw count, which exists because PipeStats can be switched off and the exit gate may not
// be. Its value at the end of P5 IS the size of the P6/P7/P8 debt.
//
// PLAIN, NOT ATOMIC, AND THAT IS THE SAME RULING TABLE 3 MAKES FOR gPipeInputS ITSELF
// (CONTRACT-P5.md section 4): the verb barrier leaves at most one of {GL thread, apply
// thread} runnable, so there is one writer at any instant. This counter, m_serverStampedVerb
// and FilledGen[] all rest on that and on nothing else - so MOBILEGL_IPC_VERB_BARRIER=0,
// R-1's negative control, is a data race on all three as well as the correctness failure it
// is there to show. It is expected to be red; it is not expected to be meaningful.
Uint64 MGPipeResidualPullCount();
void MGPipeResetResidualPullCountForTesting();
// The sticky forwards' hook, called from each of the seven bodies in
// MG_Impl/Pipe/PipeFill.cpp. They carry no MGP_INPUT_CHECK at all - the declared exception
// argued at the F-class block above - so freshness can never reach them and the exit gate
// would be structurally blind on the seven fields that hand the server a raw frontend
// object or write into the frontend. This is what puts them in `rsp` and, under
// MOBILEGL_IPC_STRICT_ERRORS=1, makes them Fatal like any other BARRIER-PULLED row.
void MGPipeStickyForwardPull(MGPipeInputField field);
#endif
#if MOBILEGL_PIPE_VERIFY
// PipeInputs.cpp. Per-field equality for the entry compare (P1 brief D8): V by value
// through G4's MGPipeFieldEqual (bitwise floats, field-wise structs), O by identity, F
+35
View File
@@ -1551,36 +1551,59 @@ namespace MobileGL::MG_Pipe {
Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }
// ---- the seven F-class forwarders ----
//
// P5 (R-7.3, CONTRACT-P5.md table 2's "the seven sticky forwards"): each of them now opens
// with MGP_STICKY_FORWARD_PULL. They are THE SEVEN THAT HAND THE SERVER A RAW FRONTEND
// OBJECT OR WRITE INTO THE FRONTEND, and they are also the only fields the poison cannot
// see - they carry no MGP_INPUT_CHECK at all, by the declared exception argued at
// PipeInputs.h's F-class block, so freshness never reaches them and the exit gate was
// structurally blind on exactly the seven most dangerous rows. The hook is a no-op outside
// a server-stamped verb, so InvalidateCompileEnv keeps being reachable from backend
// initialisation - the case the exemption was written for - and every monolith lane, split
// build included, behaves as it does today.
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGP_STICKY_FORWARD_PULL(Field) MGPipeStickyForwardPull(MGPipeInputField::Field)
#else
#define MGP_STICKY_FORWARD_PULL(Field) ((void)0)
#endif
SizeT PipeInputs::GetBufferBindingPointCount(BufferTarget target) const {
MGP_STICKY_FORWARD_PULL(GetBufferBindingPointCount);
const auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetBufferBindingPointCount(target) : 0;
}
const SharedPtr<PipeInputs::ProgramObject>& PipeInputs::GetProgramObject(Uint index) {
MGP_STICKY_FORWARD_PULL(GetProgramObject);
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetProgramObject(index) : NullShared<ProgramObject>();
}
const SharedPtr<PipeInputs::ITextureObject>& PipeInputs::GetTextureObject(Uint index) {
MGP_STICKY_FORWARD_PULL(GetTextureObject);
auto* ctx = LiveContext();
return ctx != nullptr ? ctx->GetTextureObject(index) : NullShared<ITextureObject>();
}
Bool PipeInputs::HasOpenTransformFeedbackSpan(Uint64 lifetimeId) const {
MGP_STICKY_FORWARD_PULL(HasOpenTransformFeedbackSpan);
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->HasOpenTransformFeedbackSpan(lifetimeId);
}
void PipeInputs::InvalidateCompileEnv() {
MGP_STICKY_FORWARD_PULL(InvalidateCompileEnv);
if (auto* ctx = LiveContext()) ctx->InvalidateCompileEnv();
}
Bool PipeInputs::ValidateProgramName(Uint index) const {
MGP_STICKY_FORWARD_PULL(ValidateProgramName);
const auto* ctx = LiveContext();
return ctx != nullptr && ctx->ValidateProgramName(index);
}
void PipeInputs::RecordError(ErrorCode code, UniquePtr<ErrorInfo> info) {
MGP_STICKY_FORWARD_PULL(RecordError);
auto* ctx = LiveContext();
if (ctx == nullptr) {
MGLOG_E_ONCE("PipeInputs::RecordError: no live context, dropping error %d", static_cast<int>(code));
@@ -1588,6 +1611,7 @@ namespace MobileGL::MG_Pipe {
}
ctx->RecordError(code, Move(info));
}
#undef MGP_STICKY_FORWARD_PULL
// P3a D-H2.1. The draw's RAW vertex-fetch base instance, set immediately before the fill
// at the three *BaseInstance draw entry points. It replaces the ambient process global
@@ -1610,6 +1634,9 @@ namespace MobileGL::MG_Pipe {
// Same bump the next fill would make, without a verb to fill from: no field is
// stamped, so every stamp this verb made falls behind the serial.
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeServerClearVerbBoundary();
#endif
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
// The pending base instance belongs to the verb that was about to run, so leaving
@@ -2382,6 +2409,14 @@ namespace MobileGL::MG_Pipe {
// it on both branches, so a read before this first bump is
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
++filled.CurrentVerbSerial;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// The client is filling, so whatever the server stamped at its last verb boundary is
// withdrawn: the stamps below are the CLIENT's again and a stale read is a defect, not
// a residual pull. Disarming here rather than at the end of the applier's work is what
// makes the arming flag say "the current stamps are the server's" no matter which of
// the two roles ran last.
MGPipeServerClearVerbBoundary();
#endif
MGPipeFillAccess::SetVerb(inputs, verb);
auto* ctx = LiveContext();
+284
View File
@@ -0,0 +1,284 @@
// MobileGL - MobileGL/MG_Pipe/FieldOwnership.def
// 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
// The hand-maintained half of TABLE 2 (CONTRACT-P5.md section 3, R-7): where every
// PipeInputs field's value comes from once the backend is a server.
//
// scripts/gen_pipe_field_ownership.py joins this file against the DERIVED half and writes
// generated/PipeFieldOwnership.inc. CI runs `--check` and `--self-test` beside gen_pipe.py's.
//
// THE FOUR CLASSES (CONTRACT-P5.md section 3):
//
// RECORD_SUPPLIED a pushed record supplies the WHOLE field, so the server never needs
// the client for it. THIS CLASS IS DERIVED, NOT LISTED: it is
// kMGPipeFieldEmittedBy != kNone (Coverage.def's MGP_COVERAGE_EMITTED_LIST)
// minus the fields EmittedCallSuppliesTheWholeField refuses
// (MG_Impl/Pipe/PipeFill.cpp). 32 fields today. A row below that names a
// field the derivation already placed here is a CONTRADICTION and stops
// the generator - which is the only way this file can stay true as the
// emitted list grows.
//
// APPLIER_DERIVED the applier writes it out of records it already applies, but no row of
// the emitted list claims it, so the derivation above cannot see it.
//
// BARRIER_PULLED P5's DEBT. The server answers by reading a value the client's residual
// fill (PipeFill.cpp step 4) left in the single shared gPipeInputs while
// the verb barrier holds both threads apart (R-1). Correct only because
// of that barrier, which is why the barrier is load-bearing rather than
// cautious. Every row NAMES THE PHASE THAT RETIRES IT; a row with no
// phase stops the generator. Each such read increments
// PipeStats::CallClass::ResidualPulls (`rsp`) and, under
// MOBILEGL_IPC_STRICT_ERRORS=1, is Fatal.
//
// FATAL no carrier, and the reduced path never reads it, so a read is a real
// defect: Fatal{UnmigratedPipeInput, "<Field>@<verb>"}.
//
// A field in NONE of the four is a generator error and therefore a build failure (R-7.1).
// A field in TWO is the same. That is the whole mechanism: a hand-maintained table would be
// wrong within a week, and this one cannot be silently incomplete.
//
// clang-format off
// X(Field, Class, RetiringPhase, Why)
//
// RetiringPhase is `-` for every class but BARRIER_PULLED, where it is the ROADMAP phase
// whose row stops the pull. It is a string, not an enum, because two of them name two
// phases for the two backends and flattening that would lose the half that matters.
#define MGP_FIELD_OWNERSHIP_LIST(X) \
/* ---- BARRIER_PULLED: the 20 non-sticky rows the reduced path actually reads ---- */ \
/* scout-unmigrated-census section 3 intersects each verb class's may-read mask with "no */ \
/* record supplies it" and unions kClear (7 of 18), kDraw (19 of 47) and kReadback (12 of */ \
/* 17). That union is 21 fields; GetPixelStoreParameters is the 21st and it is */ \
/* APPLIER_DERIVED below, for the reason written there. OpenRA adds no field to this set - */ \
/* it widens the SITE set, not the field set. */ \
X(GetActiveTextureUnit, BARRIER_PULLED, "P3b/P4b", \
"the server answers from its own state; Coverage.def:215-219 says no call carries it") \
/* DirectGLES.cpp:4486, PrepareForDraw, UNCONDITIONAL ON EVERY DRAW. No #if, no arm guard, */ \
/* no record fallback, and PipeFill.cpp:1902-1905 says outright that what retires the pull */ \
/* is P8, not a better applier: the storage is a SharedPtr<VertexArrayObject> and */ \
/* bind_vertex_elements carries an eight-byte {slot, gen}. */ \
X(GetBoundVertexArray, BARRIER_PULLED, "P8", \
"frontend heap reference; the record carries a handle, the mirror is a pointer") \
/* 18 Espryt sites + 11 Magma. Coverage.def:37-70 splits the 15 BufferTargets across three */ \
/* calls and leaves SEVEN with no carrier at all (CopyRead, CopyWrite, PixelPack, */ \
/* PixelUnpack, Texture, DispatchIndirect, Query), which is why the FIELD is pulled even */ \
/* though eight targets are covered. */ \
X(GetBufferBindingSlot, BARRIER_PULLED, "P8 (indirect), P9 (readback), P13 (transfer)", \
"7 of 15 BufferTargets have no call; the field is one array over all 15") \
X(GetBufferBindingPoint, BARRIER_PULLED, "P3b/P4b, P7", \
"frontend BindingSlotRange1D pointer") \
X(GetTouchedBufferBindingPointCount, BARRIER_PULLED, "P3b/P4b", \
"no call carries the touched-count high-water mark") \
/* The applier CANNOT reproduce GLContext's cross-view conversion: the frontend writes */ \
/* (Int32)value into intValue while MGPipeApplySetVertexAttribDefaults memcpys one Data[4] */ \
/* into all three views (PipeFill.cpp:1884-1896). So the record exists, the applier writes */ \
/* the field, and the value is still wrong - which is BARRIER_PULLED, not APPLIER_DERIVED. */ \
X(GetCurrentVertexAttribute, BARRIER_PULLED, "P3b/P4b", \
"the applier writes it but cannot reproduce the cross-view conversion") \
/* 8 Espryt sites through GetFramebufferBindingSlotChecked + 13 Magma. SyncCurrentFBO */ \
/* (:2995) is SELF-DECLARED monolith glue (DirectGLES.cpp:2961-2965) while BindCurrentFBO */ \
/* (:4303-4353) is already split-clean - the bind is migrated, the sync is not. */ \
X(GetFramebufferBindingSlot, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"frontend BindingSlot<FramebufferObject> pointer") \
X(GetImageTextureBinding, BARRIER_PULLED, "P3b/P4b, P7", \
"frontend ImageTextureBinding base pointer") \
/* set_sampler_views' Count IS this value plus one, but the set is suppressed on an */ \
/* unchanged content hash while the high-water mark still moves (PipeFill.cpp:1917-1923). */ \
X(GetMaxTouchedTextureUnit, BARRIER_PULLED, "P3b/P4b", \
"hash-suppressed set, high-water mark still moves") \
/* The three texture SHUTTERS. Coverage.def:220-224 is explicit that no call carries them */ \
/* and none should: what replaces them server-side is the applier's own Serial, a different */ \
/* value with a different owner. So these are not values to migrate. */ \
X(GetSamplingResolutionGeneration, BARRIER_PULLED, "P3b/P4b", \
"a shutter, not a value: the server answers from its own Serial") \
X(GetTextureBindGeneration, BARRIER_PULLED, "P3b/P4b", \
"a shutter, not a value: the server answers from its own Serial") \
X(GetTextureContextId, BARRIER_PULLED, "P3b/P4b", \
"a shutter, not a value: the server answers from its own Serial") \
X(GetTextureUnitObject, BARRIER_PULLED, "P3b/P4b, P7", \
"frontend TextureUnit base pointer; 13 Espryt + 8 Magma sites") \
/* DirectGLES.cpp:4497, PrepareForDraw, the second unconditional pointer read of every draw. */ \
X(GetProgramForDraw, BARRIER_PULLED, "P8 (Espryt), P7 (Magma)", \
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
/* The XFB six. XFB itself is off the reduced path, but kDraw's may-read mask carries all */ \
/* six and the draw walk reads them regardless - which is exactly the case a field census */ \
/* taken from "what the scenario does" rather than from the mask would miss. */ \
X(IsTransformFeedbackActive, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"read on every kDraw walk although XFB is off the reduced path") \
X(IsTransformFeedbackPaused, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"read on every kDraw walk although XFB is off the reduced path") \
X(GetTransformFeedbackProgram, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"frontend SharedPtr<ProgramObject>") \
X(GetTransformFeedbackGeneration, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"no call carries it") \
X(GetBoundTransformFeedbackLifetimeId, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"the D21 counter-slot rekey's key; no call carries it") \
X(GetTransformFeedbackCapturedVertices, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
"no call carries it") \
\
/* ---- APPLIER_DERIVED ---- */ \
/* THE ONE ROW, and it is the one the contract asked P5 to split into two field ids. It is */ \
/* split HERE INSTEAD, by ARGUMENT, in the argument-exception list below - see that list's */ \
/* header for the evidence and for what would overturn the decision. The field itself is */ \
/* APPLIER_DERIVED because set_pixel_pack_state arrives and MGPipeApplyAccess::PackState */ \
/* writes m_pixelStore[0] out of it (PipeApply.cpp:1373), while no row of the emitted list */ \
/* claims the field (Coverage.def:173 keeps it out deliberately, because the record supplies */ \
/* half of it). */ \
X(GetPixelStoreParameters, APPLIER_DERIVED, "-", \
"set_pixel_pack_state; the applier writes m_pixelStore[0] (PipeApply.cpp:1373)") \
\
/* ---- FATAL: three non-sticky fields, each off the reduced path for a checkable reason --- */ \
X(GetBoundTransformFeedbackName, FATAL, "-", \
"DEAD: read by no backend since the D21 rekey (PipeInputs.h:232-234)") \
X(GetTransformFeedbackPausedPrimitiveCounter, FATAL, "-", \
"reachable only from class kQuery, which the reduced path never enters") \
X(GetProgramForDispatch, FATAL, "-", \
"reachable only from kDispatch; there is no compute on the reduced path") \
\
/* ---- the seven sticky forwards, as FIELD rows ---- */ \
/* They have no storage, so a read of the FIELD is a call of the FORWARD; the field row and */ \
/* the forward row below carry the same class by construction, and the generator refuses a */ \
/* pair that disagrees. Their poison exemption (generated/PipeFilled.inc:422's sticky arm) */ \
/* is cancelled under split by the server stamp, which zeroes their FilledGen - and 0 loses */ \
/* to nothing, because :419's "never filled" test runs first. */ \
X(GetBufferBindingPointCount, BARRIER_PULLED, "P7/P13", \
"an argument-keyed lookup into the frontend's binding-point table") \
X(GetProgramObject, BARRIER_PULLED, "P9", \
"hands the backend a frontend SharedPtr<ProgramObject> keyed by GL name") \
X(GetTextureObject, BARRIER_PULLED, "P7", \
"hands the backend a frontend SharedPtr<ITextureObject> keyed by GL name") \
X(HasOpenTransformFeedbackSpan, BARRIER_PULLED, "P7/P9", \
"a lookup into frontend XFB span state keyed by lifetime id") \
X(ValidateProgramName, BARRIER_PULLED, "P9", \
"a frontend name-table probe; Coverage.def calls it kClientResolved") \
X(InvalidateCompileEnv, BARRIER_PULLED, "P5", \
"a WRITE INTO THE FRONTEND; R-12 replaces it with the re-arriving caps snapshot") \
X(RecordError, BARRIER_PULLED, "P9", \
"a WRITE INTO THE FRONTEND; R-12's OnGlError, whose ordering is P9's")
// X(Field, Class, RetiringPhase, Mechanism) - the SEVEN STICKY FORWARDS as their own rows.
//
// CONTRACT-P5.md section 3: "The domain is 63 fields plus the 7 sticky forwards, which are
// among those 63 but are exempted from the poison and so need their own row. 70 rows."
// The reason they need a second row is that they are the seven that hand the server a raw
// FRONTEND OBJECT or write INTO the frontend, so "the exit gate is structurally blind on the
// seven most dangerous fields" - and the row that fixes that is about the FORWARD (a live
// call with no stored value), not about the field's storage, which does not exist.
//
// Mechanism is what replaces the forward, which is NOT the same question as which phase
// retires the pull.
#define MGP_FIELD_OWNERSHIP_FORWARD_LIST(X) \
X(GetBufferBindingPointCount, BARRIER_PULLED, "P7/P13", \
"a server-side binding-point table") \
X(GetProgramObject, BARRIER_PULLED, "P9", \
"a client-resolved program handle table (ARCHITECTURE 3.2 explicitly-not-ported)") \
X(GetTextureObject, BARRIER_PULLED, "P7", \
"a server-side texture handle table") \
X(HasOpenTransformFeedbackSpan, BARRIER_PULLED, "P7/P9", \
"server-side XFB span state") \
X(ValidateProgramName, BARRIER_PULLED, "P9", \
"a client-resolved program name probe") \
X(InvalidateCompileEnv, BARRIER_PULLED, "P5", \
"OnCapsInvalidated - the re-arriving caps snapshot IS the invalidation (R-12)") \
X(RecordError, BARRIER_PULLED, "P9", \
"OnGlError, the ordered reverse-channel error post (R-12); its ordering is P9's")
// X(Field, Arg0, Class, Why) - ARGUMENT-KEYED EXCEPTIONS.
//
// A row here narrows ONE argument value of ONE field to a different class. The field keeps
// its single row above; this is the same shape Coverage.def:62-69 already rules for
// GetBufferBindingSlot - "THE ROW STAYS ONE ROW, and that is structural rather than a
// shortcut: this list IS the MGPipeInputField enum and the PipeInputs field set, and the
// field is ONE array that a second row of the same name could only duplicate".
//
// GetPixelStoreParameters is m_pixelStore[2] indexed by its own `isUnpack` argument, exactly
// as GetBufferBindingSlot is m_bufferBindingSlot[15] indexed by its BufferTarget. Only [0]
// (pack) has a carrier, and the applier writes it.
//
// AND THE UNPACK HALF HAS NO BACKEND READER AT ALL. Every MGB_CTX->GetPixelStoreParameters
// call site in the tree passes `false`: DirectGLES.cpp:7924, :9399, :10893, :11272,
// Utils.cpp:2302 and VulkanRenderer.cpp:10980 - six, not the five the scout named, and the
// scout did not open them. PipeFill.cpp's own EmitPixelPackState says the same thing from
// the other side: "PACK only, deliberately: nothing on the far side of the boundary reads
// unpack state". So the honest class for the unpack half is FATAL, not BARRIER_PULLED: a
// future backend read of it would otherwise be served a stale struct in silence, and this
// way it is a named abort on the first read.
#define MGP_FIELD_OWNERSHIP_ARG_LIST(X) \
X(GetPixelStoreParameters, 1, FATAL, \
"the unpack half has no carrier AND no backend reader; all six MGB_CTX sites pass false")
// X(WireOp, Verb) - WHERE THE SERVER STAMPS.
//
// The stamp rule needs one thing the wire does not carry: which MGPipeVerb a record belongs
// to. The two name spaces are not the same and do not line up by name - the DRAW verb is
// `DrawArrays` and its record is `draw_vbo`, the BLIT verb is `BlitFramebuffer` and its record
// is `blit` - so the map is written here and checked against both sources (PipeCalls.def for
// the op, FillPoints.def for the verb) rather than believed.
//
// EXACTLY THE OPS THAT ARE VERB BOUNDARIES. CONTRACT-P5.md section 7 puts five slots in class
// B (emitted in P5): Clear, DrawArrays, ReadPixels, BlitFramebuffer and Present. FOUR OF THEM
// ARE HERE AND PRESENT IS NOT, and that is a ruling rather than an omission: FillPoints.def:21
// says in so many words that "Present and SetSwapInterval go through BackendObject virtuals
// and read no frontend state, so they are not verbs here". There is no MGPipeVerb::Present to
// stamp for, MGPipeValidateForVerb is never called for it, and stamping at Present would
// retire the previous verb's answers with nothing to put in their place.
//
// An op that is not in this list is NOT a verb boundary and the applier must not stamp on it:
// a set_dynamic_state between two draws is part of the draw's verb, not a new one.
//
// THE GENERATOR REFUSES AN OMISSION, NOT ONLY A TYPO, and that is the half the first version of
// this file did not have. Every call that is VERB-SHAPED must have a row here or an exemption
// row below with a reason. "Verb-shaped" is the union of two mechanical tests, both derived:
// (a) PipeCalls.def gives the call kind kCtxVerb - the catalogue's own word for it;
// (b) the call's name is also an MGPipeVerb name in FillPoints.def.
// Twelve rows and three exemptions cover all fifteen. Only four of the twelve can arrive in P5
// (CONTRACT §7 class B minus Present); the other eight are Fatal{UnmigratedVerb} today and are
// mapped ANYWAY, because the failure mode of an absent row is silent: the applier would run the
// record under the PREVIOUS verb's serial, mask and name, and a field inside that mask would
// read FRESH while holding the previous verb's value.
//
// NOT MECHANICALLY DETECTABLE, and so not claimed: a call that is a verb boundary, is not
// kCtxVerb, and whose name differs from its verb's. `ResourceCopyRegion` is the one in the tree
// (its verbs are CopyImageSubData / CopyTexSubImage2D / CopyTexImage2D - three of them, which is
// why it cannot be a row without a rule for choosing). The phase that emits it adds its row and
// decides which verb it is.
#define MGP_VERB_OP_LIST(X) \
/* CONTRACT §7 class B - the four that can actually arrive in P5. */ \
X(Clear, Clear) \
X(DrawVbo, DrawArrays) \
X(ReadPixels, ReadPixels) \
X(Blit, BlitFramebuffer) \
/* Class C today (Fatal{UnmigratedVerb}); mapped so the phase that emits one cannot get NO */ \
/* stamp by omission. DrawVbo above has the same shape and a worse case: it stands for all */ \
/* twenty draw verbs, which share the kDraw mask but not the NAME a Fatal prints. */ \
X(LaunchGrid, DispatchCompute) \
X(MemoryBarrier, MemoryBarrier) \
X(BeginStreamOutput, BeginTransformFeedback) \
X(EndStreamOutput, EndTransformFeedback) \
X(PauseStreamOutput, PauseTransformFeedback) \
X(ResumeStreamOutput, ResumeTransformFeedback) \
X(GenerateMipmap, GenerateMipmap) \
X(GetTextureImage, GetTextureImage)
// X(Op, Why) - verb-shaped calls that are deliberately NOT stamp points.
//
// An exemption is a ROW, not an absence, so that the reason is in the file rather than in a
// reviewer's head and so that the generator's completeness check has something to accept.
#define MGP_VERB_OP_EXEMPT_LIST(X) \
X(Present, \
"FillPoints.def:21 - Present and SetSwapInterval go through BackendObject virtuals and " \
"read no frontend state, so they are not verbs here. There is no MGPipeVerb::Present, " \
"MGPipeValidateForVerb is never called for it, and stamping would retire the previous " \
"verb's answers with nothing to put in their place. It is class B all the same.") \
X(SetSwapInterval, \
"the same sentence of FillPoints.def:21, and it is class C besides") \
X(Flush, \
"not a verb at all: the verb census (BRIEF §12 C-3) found Flush is not a GLFunctionsTable " \
"slot and glFlush/glFinish are empty function bodies (Definitions.cpp:111-112), so there " \
"is no MGPipeVerb::Flush for a row to name")
// clang-format on
+28 -1
View File
@@ -16,6 +16,12 @@
#include <MG_Backend/MGPipe/PipeInputs.h>
#if MOBILEGL_BUILD_DISAGGREGATED
// MGPipeUnmigratedEmulation's split arm reads MG_Config::Transport, which is what tells an
// emulation site whether it is running inside a server or in the monolith it was written for.
#include <Config.h>
#endif
#if MOBILEGL_PIPE_VERIFY
// THE ONE PLACE THE PROGRAM ARCHIVE'S CODEC IS CALLED, and it is compiled into the VERIFY
// build only. In monolith the archive does not travel - MGPProgramDesc's seven blob refs are
@@ -2842,5 +2848,26 @@ namespace MobileGL::MG_Pipe {
// It takes a literal and does nothing with it. Not a log line, not a counter: it sits on
// paths a frame can reach many times, and ROADMAP.md forbids committing hot-path
// instrumentation.
void MGPipeUnmigratedEmulation(const char* name) { (void)name; }
// P5 GIVES IT TEETH, and this is the whole of it: ONE function edit arms FIVE call sites
// (Managers.cpp:5334, DirectGLES.cpp:8051, :8702, :8997, :10623), exactly as the header
// and P4a's comment above promised. Every one of them reaches back into the CLIENT's
// address space - a texture shadow, a CPU mipmap fallback, a host-side copy - and a server
// has no client address space to reach into, so degrading silently is the one outcome that
// must not happen.
//
// THE ARM IS THE TRANSPORT, NOT THE BUILD. build-split runs MOBILEGL_TRANSPORT=monolith in
// every unit and integration-gpu lane, and several of the five are on ordinary monolith
// paths that those lanes exercise (glGenerateMipmap reaches two of them); arming on the
// build would turn 1117 integration cases red for running code that is correct in the role
// they run it in. None of the five is on P5's reduced path, so under a real transport this
// costs nothing and catches a lot.
void MGPipeUnmigratedEmulation(const char* name) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{UnmigratedEmulation, \"%s\"}", name != nullptr ? name : "<null>");
std::abort();
}
#endif
(void)name;
}
} // namespace MobileGL::MG_Pipe
@@ -0,0 +1,297 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc
// 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
// TABLE 2: PipeInputs field ownership (CONTRACT-P5.md section 3, R-7).
//
// GENERATED by scripts/gen_pipe_field_ownership.py from Coverage.def, FieldOwnership.def and
// MG_Impl/Pipe/PipeFill.cpp - DO NOT EDIT. Regenerate with
// `python3 scripts/gen_pipe_field_ownership.py`; CI runs it and diffs the result.
//
// Included from MG_Backend/MGPipe/PipeInputs.h inside namespace MobileGL::MG_Pipe, which is
// the one header that both the poison check and the server's verb stamp already see. It is
// NOT included from MG_Pipe/MGPipe.h with the other seven generated files, on purpose: that
// header is in the PULL build's include closure and G1 admits no symbol motion there.
// The four classes. kUnclassified exists so the static_assert below has something to refuse;
// the generator never emits it, which is what makes "a field in no class fails the build"
// true at two independent points rather than one.
enum class MGPipeFieldOwnership : Uint8 {
kUnclassified = 0,
kRecordSupplied, // a pushed record supplies the WHOLE field
kApplierDerived, // the applier writes it out of records it already applies
kBarrierPulled, // P5's debt: read out of the client's residual fill under the verb barrier
kFatal, // no carrier and the reduced path never reads it
};
inline constexpr const char* kMGPipeFieldOwnershipNames[] = {
"UNCLASSIFIED", "RECORD-SUPPLIED", "APPLIER-DERIVED", "BARRIER-PULLED", "FATAL",
};
inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCount] = {
MGPipeFieldOwnership::kBarrierPulled, // GetActiveTextureUnit
MGPipeFieldOwnership::kRecordSupplied, // GetBlendColor
MGPipeFieldOwnership::kRecordSupplied, // GetBlendEquationIndexed
MGPipeFieldOwnership::kRecordSupplied, // GetBlendFuncIndexed
MGPipeFieldOwnership::kFatal, // GetBoundTransformFeedbackName
MGPipeFieldOwnership::kBarrierPulled, // GetBoundVertexArray
MGPipeFieldOwnership::kBarrierPulled, // GetBufferBindingSlot
MGPipeFieldOwnership::kBarrierPulled, // GetBufferBindingPoint
MGPipeFieldOwnership::kBarrierPulled, // GetBufferBindingPointCount
MGPipeFieldOwnership::kBarrierPulled, // GetTouchedBufferBindingPointCount
MGPipeFieldOwnership::kRecordSupplied, // GetClampReadColor
MGPipeFieldOwnership::kRecordSupplied, // GetClearColor
MGPipeFieldOwnership::kRecordSupplied, // GetClearDepth
MGPipeFieldOwnership::kRecordSupplied, // GetClearStencil
MGPipeFieldOwnership::kRecordSupplied, // GetColorMaskIndexed
MGPipeFieldOwnership::kRecordSupplied, // GetCullFaceMode
MGPipeFieldOwnership::kBarrierPulled, // GetCurrentVertexAttribute
MGPipeFieldOwnership::kRecordSupplied, // GetDepthFunc
MGPipeFieldOwnership::kRecordSupplied, // GetDepthMask
MGPipeFieldOwnership::kRecordSupplied, // GetDepthRangeIndexed
MGPipeFieldOwnership::kBarrierPulled, // GetFramebufferBindingSlot
MGPipeFieldOwnership::kBarrierPulled, // GetImageTextureBinding
MGPipeFieldOwnership::kRecordSupplied, // GetLineWidth
MGPipeFieldOwnership::kRecordSupplied, // GetLogicOp
MGPipeFieldOwnership::kBarrierPulled, // GetMaxTouchedTextureUnit
MGPipeFieldOwnership::kRecordSupplied, // GetMinSampleShadingValue
MGPipeFieldOwnership::kRecordSupplied, // GetPatchDefaultInnerLevel
MGPipeFieldOwnership::kRecordSupplied, // GetPatchDefaultOuterLevel
MGPipeFieldOwnership::kRecordSupplied, // GetPatchVertices
MGPipeFieldOwnership::kRecordSupplied, // GetPipelineStateVersion
MGPipeFieldOwnership::kApplierDerived, // GetPixelStoreParameters
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonModeFront
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetFactor
MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetUnits
MGPipeFieldOwnership::kRecordSupplied, // GetPrimitiveRestartIndex
MGPipeFieldOwnership::kFatal, // GetProgramForDispatch
MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDraw
MGPipeFieldOwnership::kBarrierPulled, // GetProgramObject
MGPipeFieldOwnership::kRecordSupplied, // GetProvokingVertexMode
MGPipeFieldOwnership::kRecordSupplied, // GetRenderStateParameters
MGPipeFieldOwnership::kRecordSupplied, // GetRenderStateParametersVersion
MGPipeFieldOwnership::kBarrierPulled, // GetSamplingResolutionGeneration
MGPipeFieldOwnership::kRecordSupplied, // GetScissorBox
MGPipeFieldOwnership::kRecordSupplied, // GetStencilState
MGPipeFieldOwnership::kBarrierPulled, // GetTextureBindGeneration
MGPipeFieldOwnership::kBarrierPulled, // GetTextureContextId
MGPipeFieldOwnership::kBarrierPulled, // GetTextureObject
MGPipeFieldOwnership::kBarrierPulled, // GetTextureUnitObject
MGPipeFieldOwnership::kBarrierPulled, // GetTransformFeedbackCapturedVertices
MGPipeFieldOwnership::kBarrierPulled, // GetTransformFeedbackGeneration
MGPipeFieldOwnership::kFatal, // GetTransformFeedbackPausedPrimitiveCounter
MGPipeFieldOwnership::kBarrierPulled, // GetTransformFeedbackProgram
MGPipeFieldOwnership::kRecordSupplied, // GetViewport
MGPipeFieldOwnership::kRecordSupplied, // GetViewportIndexed
MGPipeFieldOwnership::kRecordSupplied, // IsCapabilityEnabled
MGPipeFieldOwnership::kRecordSupplied, // IsCapabilityEnabledIndexed
MGPipeFieldOwnership::kBarrierPulled, // IsTransformFeedbackActive
MGPipeFieldOwnership::kBarrierPulled, // IsTransformFeedbackPaused
MGPipeFieldOwnership::kBarrierPulled, // InvalidateCompileEnv
MGPipeFieldOwnership::kBarrierPulled, // ValidateProgramName
MGPipeFieldOwnership::kBarrierPulled, // RecordError
MGPipeFieldOwnership::kBarrierPulled, // GetBoundTransformFeedbackLifetimeId
MGPipeFieldOwnership::kBarrierPulled, // HasOpenTransformFeedbackSpan
};
// The ROADMAP phase whose row retires the pull. "-" for every class but BARRIER-PULLED.
inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] = {
"P3b/P4b", // GetActiveTextureUnit
"-", // GetBlendColor
"-", // GetBlendEquationIndexed
"-", // GetBlendFuncIndexed
"-", // GetBoundTransformFeedbackName
"P8", // GetBoundVertexArray
"P8 (indirect), P9 (readback), P13 (transfer)", // GetBufferBindingSlot
"P3b/P4b, P7", // GetBufferBindingPoint
"P7/P13", // GetBufferBindingPointCount
"P3b/P4b", // GetTouchedBufferBindingPointCount
"-", // GetClampReadColor
"-", // GetClearColor
"-", // GetClearDepth
"-", // GetClearStencil
"-", // GetColorMaskIndexed
"-", // GetCullFaceMode
"P3b/P4b", // GetCurrentVertexAttribute
"-", // GetDepthFunc
"-", // GetDepthMask
"-", // GetDepthRangeIndexed
"P3b/P4b (Espryt), P7 (Magma)", // GetFramebufferBindingSlot
"P3b/P4b, P7", // GetImageTextureBinding
"-", // GetLineWidth
"-", // GetLogicOp
"P3b/P4b", // GetMaxTouchedTextureUnit
"-", // GetMinSampleShadingValue
"-", // GetPatchDefaultInnerLevel
"-", // GetPatchDefaultOuterLevel
"-", // GetPatchVertices
"-", // GetPipelineStateVersion
"-", // GetPixelStoreParameters
"-", // GetPolygonModeFront
"-", // GetPolygonOffsetFactor
"-", // GetPolygonOffsetUnits
"-", // GetPrimitiveRestartIndex
"-", // GetProgramForDispatch
"P8 (Espryt), P7 (Magma)", // GetProgramForDraw
"P9", // GetProgramObject
"-", // GetProvokingVertexMode
"-", // GetRenderStateParameters
"-", // GetRenderStateParametersVersion
"P3b/P4b", // GetSamplingResolutionGeneration
"-", // GetScissorBox
"-", // GetStencilState
"P3b/P4b", // GetTextureBindGeneration
"P3b/P4b", // GetTextureContextId
"P7", // GetTextureObject
"P3b/P4b, P7", // GetTextureUnitObject
"P3b/P4b (Espryt), P7 (Magma)", // GetTransformFeedbackCapturedVertices
"P3b/P4b (Espryt), P7 (Magma)", // GetTransformFeedbackGeneration
"-", // GetTransformFeedbackPausedPrimitiveCounter
"P3b/P4b (Espryt), P7 (Magma)", // GetTransformFeedbackProgram
"-", // GetViewport
"-", // GetViewportIndexed
"-", // IsCapabilityEnabled
"-", // IsCapabilityEnabledIndexed
"P3b/P4b (Espryt), P7 (Magma)", // IsTransformFeedbackActive
"P3b/P4b (Espryt), P7 (Magma)", // IsTransformFeedbackPaused
"P5", // InvalidateCompileEnv
"P9", // ValidateProgramName
"P9", // RecordError
"P3b/P4b (Espryt), P7 (Magma)", // GetBoundTransformFeedbackLifetimeId
"P7/P9", // HasOpenTransformFeedbackSpan
};
// The seven sticky forwards, which are among the 63 above and need a row of their own:
// they are the ones that hand the server a raw frontend object or write into the
// frontend, so the exit gate is structurally blind on them without one.
inline constexpr SizeT kMGPipeFieldOwnershipForwardCount = 7;
static_assert(kMGPipeFieldOwnershipForwardCount == kMGPipeInputStickyFieldCount,
"the forward rows and Coverage.def's sticky set are the same seven");
inline constexpr MGPipeInputField kMGPipeFieldOwnershipForwardField[kMGPipeFieldOwnershipForwardCount] = {
MGPipeInputField::GetBufferBindingPointCount,
MGPipeInputField::GetProgramObject,
MGPipeInputField::GetTextureObject,
MGPipeInputField::HasOpenTransformFeedbackSpan,
MGPipeInputField::ValidateProgramName,
MGPipeInputField::InvalidateCompileEnv,
MGPipeInputField::RecordError,
};
inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnershipForward[kMGPipeFieldOwnershipForwardCount] = {
MGPipeFieldOwnership::kBarrierPulled, // GetBufferBindingPointCount
MGPipeFieldOwnership::kBarrierPulled, // GetProgramObject
MGPipeFieldOwnership::kBarrierPulled, // GetTextureObject
MGPipeFieldOwnership::kBarrierPulled, // HasOpenTransformFeedbackSpan
MGPipeFieldOwnership::kBarrierPulled, // ValidateProgramName
MGPipeFieldOwnership::kBarrierPulled, // InvalidateCompileEnv
MGPipeFieldOwnership::kBarrierPulled, // RecordError
};
inline constexpr const char* kMGPipeFieldOwnershipForwardMechanism[kMGPipeFieldOwnershipForwardCount] = {
"a server-side binding-point table", // GetBufferBindingPointCount, retires in P7/P13
"a client-resolved program handle table (ARCHITECTURE 3.2 explicitly-not-ported)", // GetProgramObject, retires in P9
"a server-side texture handle table", // GetTextureObject, retires in P7
"server-side XFB span state", // HasOpenTransformFeedbackSpan, retires in P7/P9
"a client-resolved program name probe", // ValidateProgramName, retires in P9
"OnCapsInvalidated - the re-arriving caps snapshot IS the invalidation (R-12)", // InvalidateCompileEnv, retires in P5
"OnGlError, the ordered reverse-channel error post (R-12); its ordering is P9's", // RecordError, retires in P9
};
// CONTRACT-P5.md section 3: "70 rows, each in exactly one class".
inline constexpr SizeT kMGPipeFieldOwnershipRowCount =
kMGPipeInputFieldCount + kMGPipeFieldOwnershipForwardCount;
static_assert(kMGPipeFieldOwnershipRowCount == 70, "table 2's row count moved");
// An ARGUMENT-KEYED narrowing of one field. The field keeps its single row above; this
// says that one argument value of it belongs to a different class. Coverage.def:62-69 already
// rules the shape for GetBufferBindingSlot - "THE ROW STAYS ONE ROW ... the field is ONE array
// that a second row of the same name could only duplicate" - and m_pixelStore[2] is the same
// shape indexed by its own isUnpack argument.
struct MGPipeFieldArgumentOwnership {
MGPipeInputField Field;
Uint32 Arg0;
MGPipeFieldOwnership Class;
};
inline constexpr SizeT kMGPipeFieldArgumentOwnershipCount = 1;
inline constexpr MGPipeFieldArgumentOwnership
kMGPipeFieldArgumentOwnership[kMGPipeFieldArgumentOwnershipCount] = {
{MGPipeInputField::GetPixelStoreParameters, 1u, MGPipeFieldOwnership::kFatal}, // the unpack half has no carrier AND no backend reader; all six MGB_CTX sites pass false
};
constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field) {
return kMGPipeFieldOwnership[static_cast<SizeT>(field)];
}
// The same answer, narrowed by the accessor's first argument. Every accessor that takes one
// may call this; only the fields with a row above answer differently from the field's class.
constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field, Uint32 arg0) {
for (SizeT i = 0; i < kMGPipeFieldArgumentOwnershipCount; ++i) {
if (kMGPipeFieldArgumentOwnership[i].Field == field &&
kMGPipeFieldArgumentOwnership[i].Arg0 == arg0) {
return kMGPipeFieldArgumentOwnership[i].Class;
}
}
return MGPipeFieldOwnershipOf(field);
}
constexpr const char* MGPipeFieldOwnershipName(MGPipeFieldOwnership ownership) {
return kMGPipeFieldOwnershipNames[static_cast<SizeT>(ownership)];
}
// THE BUILD FAILURE R-7.1 ASKS FOR. The generator refuses to emit an unclassified row, so
// this can only fire on a hand-edited header - which is exactly the edit the DO NOT EDIT
// banner cannot prevent on its own.
constexpr Bool MGPipeEveryFieldIsClassified() {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
if (kMGPipeFieldOwnershipForward[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
return true;
}
static_assert(MGPipeEveryFieldIsClassified(),
"a PipeInputs field is in none of the four ownership classes (CONTRACT-P5 table 2, R-7.1)");
// WHERE THE SERVER STAMPS. The wire's op and the fill's verb are different name spaces
// and do not line up by name (draw_vbo is DrawArrays, blit is BlitFramebuffer), so this is the
// join. An op with no row is NOT a verb boundary and the applier must not stamp on it.
//
// EVERY VERB-SHAPED CALL IS ANSWERED HERE OR EXEMPTED BY NAME, and the generator refuses an
// omission: a verb-shaped record with no row would apply under the PREVIOUS verb's serial,
// mask and name, so a field inside that mask would read FRESH while holding the previous
// verb's value - the one silent failure this table has. The exemptions:
// Flush not a verb at all: the verb census (BRIEF §12 C-3) found Flush is not a GLFunctionsTable
// Present FillPoints.def:21 - Present and SetSwapInterval go through BackendObject virtuals and
// SetSwapInterval the same sentence of FillPoints.def:21, and it is class C besides
constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
switch (op) {
case MGPWireOp::Clear: return MGPipeVerb::Clear;
case MGPWireOp::DrawVbo: return MGPipeVerb::DrawArrays;
case MGPWireOp::ReadPixels: return MGPipeVerb::ReadPixels;
case MGPWireOp::Blit: return MGPipeVerb::BlitFramebuffer;
case MGPWireOp::LaunchGrid: return MGPipeVerb::DispatchCompute;
case MGPWireOp::MemoryBarrier: return MGPipeVerb::MemoryBarrier;
case MGPWireOp::BeginStreamOutput: return MGPipeVerb::BeginTransformFeedback;
case MGPWireOp::EndStreamOutput: return MGPipeVerb::EndTransformFeedback;
case MGPWireOp::PauseStreamOutput: return MGPipeVerb::PauseTransformFeedback;
case MGPWireOp::ResumeStreamOutput: return MGPipeVerb::ResumeTransformFeedback;
case MGPWireOp::GenerateMipmap: return MGPipeVerb::GenerateMipmap;
case MGPWireOp::GetTextureImage: return MGPipeVerb::GetTextureImage;
default:
return MGPipeVerb::kVerbCount;
}
}
inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 12;
inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3;
// The class sizes, as constants a test can pin without recounting the table.
inline constexpr SizeT kMGPipeRecordSuppliedFieldCount = 32;
inline constexpr SizeT kMGPipeApplierDerivedFieldCount = 1;
inline constexpr SizeT kMGPipeBarrierPulledFieldCount = 27;
inline constexpr SizeT kMGPipeFatalFieldCount = 3;
static_assert(kMGPipeRecordSuppliedFieldCount + kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount == kMGPipeInputFieldCount, "the four class sizes do not partition the field set");
+33
View File
@@ -175,8 +175,41 @@ foreach(pipeTest ResourceEmitTest VertexInputEmitTest
endif()
endforeach()
# P5 package p1: table 2 (PipeInputs field ownership) at runtime. Registered in EVERY
# configuration rather than behind `if (MOBILEGL_BUILD_DISAGGREGATED)` like the Wire suite,
# because half of it is arithmetic over the generated table that a push or verify build can
# check just as well - and a suite that only exists in one configuration is a suite three
# configurations cannot notice breaking. Its split-only cases compile away there.
#
# Links gtest rather than gtest_main and carries its own main(), like PipeInputsTest and
# RenderStateSpansTest: its abort cases read the Fatal line back out of a log file the process
# names before anything logs.
add_executable(
FieldOwnershipTest
FieldOwnershipTest.cpp
)
target_include_directories(FieldOwnershipTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
${MGL_ROOT}/MobileGL/MG_Pipe
${MGL_ROOT}/3rdparty/xxHash
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
${MGL_ROOT}/3rdparty/SPIRV-Reflect
)
target_link_libraries(FieldOwnershipTest PRIVATE
GTest::gtest
${LINK_LIBRARIES}
)
if (MSVC)
target_compile_options(FieldOwnershipTest PRIVATE /Zc:preprocessor)
endif()
include(GoogleTest)
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(FieldOwnershipTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
foreach(pipeTest ResourceEmitTest VertexInputEmitTest
FramebufferEmitTest TextureEmitTest SamplerEmitTest ImageEmitTest
ProgramEmitTest CompositeResolverTest)
@@ -0,0 +1,592 @@
// MobileGL - MobileGL/MG_Test/Pipe/FieldOwnershipTest.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
// TABLE 2 at runtime (CONTRACT-P5.md section 3, BRIEF-P5 R-7, exit gate E4). Package p1.
//
// The generated table is checked two ways here, and they answer different questions:
//
// the ARITHMETIC cases run in any push build and say the table PARTITIONS the field set -
// 70 rows, four classes, every BARRIER-PULLED row naming the phase that retires it;
//
// the BEHAVIOUR cases run only in a split build and say the table is LOAD-BEARING - that a
// server verb stamp makes the record-supplied fields readable and withdraws the rest, that a
// BARRIER-PULLED read is counted rather than fatal, that MOBILEGL_IPC_STRICT_ERRORS=1 turns
// it into a named abort, and that the seven sticky forwards' poison exemption is cancelled.
//
// E4's negative control is ARecordSuppliedFieldIsReadableAfterAServerStamp: move one field
// from RECORD-SUPPLIED to FATAL in MG_Pipe/FieldOwnership.def and that case goes red by name.
// The generator's own controls are `gen_pipe_field_ownership.py --self-test`.
//
// Links gtest rather than gtest_main and carries its own main(), like PipeInputsTest: the
// abort cases read the Fatal line back out of a log file this process names before anything
// logs.
#include <gtest/gtest.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include "Includes.h"
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <Config.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Metrics/PipeStats.h>
#endif
#if !defined(_WIN32)
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
#define MGTEST_HAVE_FORK 1
#else
#include <process.h>
#define MGTEST_HAVE_FORK 0
#endif
using namespace MobileGL;
using namespace MobileGL::MG_Pipe;
namespace {
std::string g_logPath;
std::string ReadLog() {
std::ifstream in(g_logPath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
long ProcessId() {
#if defined(_WIN32)
return static_cast<long>(::_getpid());
#else
return static_cast<long>(::getpid());
#endif
}
#if MOBILEGL_PIPE_PUSH
using GLContext = MG_State::GLState::GLContext;
// A live frontend context, restored on the way out so the cases stay independent - the
// sticky forwards reach for one and the stamp cases must not depend on whether they found
// it (PipeInputsTest's idiom).
class FieldOwnershipTest : public ::testing::Test {
protected:
void SetUp() override {
m_previous = Move(MG_State::pGLContext);
MG_State::pGLContext = MakeUnique<GLContext>();
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Ipc.StrictErrors = false;
MGPipeServerClearVerbBoundary();
MGPipeResetResidualPullCountForTesting();
#endif
}
void TearDown() override {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeServerClearVerbBoundary();
MG_Config::Ipc.StrictErrors = false;
#endif
MG_State::pGLContext = Move(m_previous);
}
UniquePtr<GLContext> m_previous;
};
SizeT Index(MGPipeInputField field) { return static_cast<SizeT>(field); }
#if MGTEST_HAVE_FORK
struct ChildResult {
int Status = -1;
std::string Log;
};
// Runs `body` in a forked child and returns its wait status and log delta. The child must
// not use gtest assertions; it _exit(0)s when `body` returns, so a body expected to die is
// asserted dead by the parent rather than assumed dead.
template <class Body>
ChildResult RunInChild(Body body) {
ChildResult result;
const std::string before = ReadLog();
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return result;
if (pid == 0) {
body();
::_exit(0);
}
int status = 0;
if (::waitpid(pid, &status, 0) != pid) return result;
result.Status = status;
result.Log = ReadLog().substr(before.size());
return result;
}
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
Bool ExitedWith(const ChildResult& r, int code) { return WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == code; }
std::string DescribeStatus(const ChildResult& r) {
if (r.Status < 0) return "fork/waitpid failed";
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
return "status " + std::to_string(r.Status);
}
#endif // MGTEST_HAVE_FORK
#endif // MOBILEGL_PIPE_PUSH
} // namespace
#if !MOBILEGL_PIPE_PUSH
TEST(FieldOwnershipTest, EveryFieldAndForwardIsInExactlyOneClass) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(FieldOwnershipTest, TheClassSizesPartitionTheFieldSet) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(FieldOwnershipTest, EveryBarrierPulledRowNamesTheRetiringPhase) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
TEST(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) {
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
}
#else // MOBILEGL_PIPE_PUSH
// ---------------------------------------------------------------------------------------
// The arithmetic: the table partitions the field set. Runs in push, verify and split.
// ---------------------------------------------------------------------------------------
TEST_F(FieldOwnershipTest, EveryFieldAndForwardIsInExactlyOneClass) {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
EXPECT_NE(kMGPipeFieldOwnership[i], MGPipeFieldOwnership::kUnclassified)
<< kMGPipeInputFieldNames[i] << " is in none of the four ownership classes";
}
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
EXPECT_NE(kMGPipeFieldOwnershipForward[i], MGPipeFieldOwnership::kUnclassified)
<< "sticky forward " << i << " is in none of the four ownership classes";
}
EXPECT_EQ(kMGPipeFieldOwnershipRowCount, SizeT{70});
}
TEST_F(FieldOwnershipTest, TheClassSizesPartitionTheFieldSet) {
SizeT counted[5] = {};
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
++counted[static_cast<SizeT>(kMGPipeFieldOwnership[i])];
}
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kRecordSupplied)],
kMGPipeRecordSuppliedFieldCount);
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kApplierDerived)],
kMGPipeApplierDerivedFieldCount);
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kBarrierPulled)],
kMGPipeBarrierPulledFieldCount);
EXPECT_EQ(counted[static_cast<SizeT>(MGPipeFieldOwnership::kFatal)], kMGPipeFatalFieldCount);
// The census's own arithmetic (scout-unmigrated-census section 2.1): 31 of the 63 fields
// are served by NO pushed record - 24 non-sticky plus the seven sticky - so 32 are.
EXPECT_EQ(kMGPipeRecordSuppliedFieldCount, SizeT{32});
EXPECT_EQ(kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount,
SizeT{31});
}
TEST_F(FieldOwnershipTest, EveryBarrierPulledRowNamesTheRetiringPhase) {
SizeT pulled = 0;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const Bool isPulled = kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kBarrierPulled;
const Bool named = std::string(kMGPipeFieldRetiringPhase[i]) != "-";
EXPECT_EQ(isPulled, named) << kMGPipeInputFieldNames[i]
<< ": only a BARRIER-PULLED row has a retiring phase, and it must have one";
pulled += isPulled ? 1 : 0;
}
EXPECT_EQ(pulled, kMGPipeBarrierPulledFieldCount);
}
// The 21 the reduced path actually reads (scout-unmigrated-census section 3: the union of
// kClear's 7, kDraw's 19 and kReadback's 12). Twenty of them are BARRIER-PULLED; the
// twenty-first is GetPixelStoreParameters, whose PACK half the applier writes and whose UNPACK
// half has no carrier and no backend reader at all.
TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) {
const MGPipeInputField pulled[] = {
MGPipeInputField::GetActiveTextureUnit,
MGPipeInputField::GetBoundVertexArray,
MGPipeInputField::GetBufferBindingSlot,
MGPipeInputField::GetBufferBindingPoint,
MGPipeInputField::GetTouchedBufferBindingPointCount,
MGPipeInputField::GetCurrentVertexAttribute,
MGPipeInputField::GetFramebufferBindingSlot,
MGPipeInputField::GetImageTextureBinding,
MGPipeInputField::GetMaxTouchedTextureUnit,
MGPipeInputField::GetProgramForDraw,
MGPipeInputField::GetSamplingResolutionGeneration,
MGPipeInputField::GetTextureBindGeneration,
MGPipeInputField::GetTextureContextId,
MGPipeInputField::GetTextureUnitObject,
MGPipeInputField::GetTransformFeedbackCapturedVertices,
MGPipeInputField::GetTransformFeedbackGeneration,
MGPipeInputField::GetTransformFeedbackProgram,
MGPipeInputField::GetBoundTransformFeedbackLifetimeId,
MGPipeInputField::IsTransformFeedbackActive,
MGPipeInputField::IsTransformFeedbackPaused,
};
for (const auto field : pulled) {
EXPECT_EQ(MGPipeFieldOwnershipOf(field), MGPipeFieldOwnership::kBarrierPulled)
<< kMGPipeInputFieldNames[Index(field)] << " left the reduced path's debt";
}
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters),
MGPipeFieldOwnership::kApplierDerived);
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 0u),
MGPipeFieldOwnership::kApplierDerived);
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u),
MGPipeFieldOwnership::kFatal);
// The three off the reduced path, each for its own checkable reason.
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName),
MGPipeFieldOwnership::kFatal);
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter),
MGPipeFieldOwnership::kFatal);
EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch),
MGPipeFieldOwnership::kFatal);
}
TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) {
ASSERT_EQ(kMGPipeFieldOwnershipForwardCount, kMGPipeInputStickyFieldCount);
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
const MGPipeInputField field = kMGPipeFieldOwnershipForwardField[i];
EXPECT_TRUE(kMGPipeInputFieldSticky[Index(field)])
<< kMGPipeInputFieldNames[Index(field)] << " has a forward row but is not sticky";
EXPECT_EQ(kMGPipeFieldOwnershipForward[i], MGPipeFieldOwnershipOf(field));
EXPECT_STRNE(kMGPipeFieldOwnershipForwardMechanism[i], "");
}
}
// The stamp map, in both directions: the four P5 class-B boundaries, the eight mapped ahead of
// the phase that will emit them, and the three verb-shaped calls that are exempt by name.
TEST_F(FieldOwnershipTest, VerbBoundaryOpsCoverEveryVerbShapedCall) {
// CONTRACT §7 class B minus Present - the only four that can arrive in P5.
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Clear), MGPipeVerb::Clear);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::DrawVbo), MGPipeVerb::DrawArrays);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ReadPixels), MGPipeVerb::ReadPixels);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Blit), MGPipeVerb::BlitFramebuffer);
// Class C today, mapped anyway: an OMITTED stamp row is silent, because the record would
// apply under the previous verb's serial, mask and name.
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::LaunchGrid), MGPipeVerb::DispatchCompute);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::MemoryBarrier), MGPipeVerb::MemoryBarrier);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::BeginStreamOutput), MGPipeVerb::BeginTransformFeedback);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::EndStreamOutput), MGPipeVerb::EndTransformFeedback);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::PauseStreamOutput), MGPipeVerb::PauseTransformFeedback);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::ResumeStreamOutput), MGPipeVerb::ResumeTransformFeedback);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GenerateMipmap), MGPipeVerb::GenerateMipmap);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetTextureImage), MGPipeVerb::GetTextureImage);
EXPECT_EQ(kMGPipeVerbBoundaryOpCount, SizeT{12});
EXPECT_EQ(kMGPipeVerbBoundaryExemptCount, SizeT{3});
// Present is class B (it is emitted in P5) and is STILL not a verb boundary:
// FillPoints.def:21 - "Present and SetSwapInterval go through BackendObject virtuals and
// read no frontend state, so they are not verbs here". Stamping there would retire the
// previous verb's answers with nothing to put in their place.
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Present), MGPipeVerb::kVerbCount);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetSwapInterval), MGPipeVerb::kVerbCount);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::Flush), MGPipeVerb::kVerbCount);
// ... and a record that is part of a verb rather than a boundary of one.
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::SetDynamicState), MGPipeVerb::kVerbCount);
EXPECT_EQ(MGPipeVerbForWireOp(MGPWireOp::GetCaps), MGPipeVerb::kVerbCount);
}
// ---------------------------------------------------------------------------------------
// The behaviour: the table is load-bearing. Split builds only - it is the server verb stamp
// that arms all of it, and nothing in a monolith lane stamps.
// ---------------------------------------------------------------------------------------
#if MOBILEGL_BUILD_DISAGGREGATED
TEST_F(FieldOwnershipTest, AServerStampMakesRecordSuppliedFieldsFreshAndWithdrawsTheRest) {
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
EXPECT_TRUE(gPipeInputs.ServerStampedVerb());
EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::Clear);
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(
kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::Clear)])];
SizeT stamped = 0;
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
const auto field = static_cast<MGPipeInputField>(i);
const Bool fresh = MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field);
// The stamp respects the verb's own may-read table for the client fill's reason: a
// field outside the class was never copied for this verb, so answering it would hand
// the server the previous verb's value.
const Bool answerable = MGPipeFieldMaskHas(mask, field) &&
(kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kRecordSupplied ||
kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kApplierDerived);
EXPECT_EQ(fresh, answerable)
<< kMGPipeInputFieldNames[i] << " is " << MGPipeFieldOwnershipName(kMGPipeFieldOwnership[i])
<< " and the stamp answered " << (fresh ? "fresh" : "stale");
stamped += fresh ? 1 : 0;
}
// Not vacuous: kClear really does have record-supplied fields to stamp.
EXPECT_GT(stamped, SizeT{0});
// AND FOUR NAMED FIELDS, NOT DERIVED FROM THE ARRAY UNDER TEST. The loop above recomputes
// `answerable` out of kMGPipeFieldOwnership, so it can only catch a stamp that disagrees
// with the table - never a table that is wrong. These four say what the stamp must do for
// four fields whose class is an argument of this package rather than a lookup.
const MGPipeFilledState& filled = gPipeInputs.FilledState();
EXPECT_TRUE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetClearColor)); // supplied
EXPECT_TRUE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetRenderStateParameters));
EXPECT_FALSE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::GetFramebufferBindingSlot)); // pulled
EXPECT_FALSE(MGPipeInputFieldIsFresh(filled, MGPipeInputField::RecordError)); // sticky
}
// THE STICKY EXEMPTION, CANCELLED. generated/PipeFilled.inc answers "fresh" for a sticky field
// whatever the serial says - but it tests "never filled" FIRST, so the stamp's withdrawal
// (FilledGen = 0) wins over kMGPipeInputFieldSticky without a line of the generated file
// changing. Before this, the seven most dangerous fields were exempt by construction.
TEST_F(FieldOwnershipTest, TheStickyExemptionIsCancelledByTheServerStamp) {
MGPipeValidateForVerb(MGPipeVerb::Clear); // the CLIENT fills and stamps all 63, sticky included
EXPECT_TRUE(MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), MGPipeInputField::RecordError));
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
const MGPipeInputField field = kMGPipeFieldOwnershipForwardField[i];
EXPECT_FALSE(MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field))
<< kMGPipeInputFieldNames[Index(field)] << " is still exempt under split";
}
}
// E4's NEGATIVE CONTROL. Move one field from RECORD-SUPPLIED to FATAL in FieldOwnership.def
// and this case goes red by name: the read below aborts instead of completing.
TEST_F(FieldOwnershipTest, ARecordSuppliedFieldIsReadableAfterAServerStamp) {
ASSERT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetClearColor),
MGPipeFieldOwnership::kRecordSupplied);
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
(void)gPipeInputs.GetClearColor();
(void)gPipeInputs.GetRenderStateParameters();
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
// The pixel store is in kReadback's class, not kClear's, so its readable half is exercised
// under the verb that actually reads it.
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetPixelStoreParameters(false); // the half that has a carrier
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
}
TEST_F(FieldOwnershipTest, ABarrierPulledReadAfterAServerStampIsCountedNotFatal) {
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{0});
(void)gPipeInputs.GetActiveTextureUnit();
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{1});
(void)gPipeInputs.GetTextureContextId();
(void)gPipeInputs.GetMaxTouchedTextureUnit();
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{3});
}
// The seven carry no MGP_INPUT_CHECK, so freshness can never reach them; this is the only
// thing that puts them in `rsp`.
TEST_F(FieldOwnershipTest, AStickyForwardIsCountedAsAResidualPull) {
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{0});
(void)gPipeInputs.ValidateProgramName(1u);
(void)gPipeInputs.GetProgramObject(1u);
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{2});
}
// ... and outside a server-stamped verb they are ordinary monolith calls, which is what keeps
// InvalidateCompileEnv reachable from backend initialisation - the case the exemption exists
// for - and what keeps build-split's monolith lanes behaving as a verify build's do.
TEST_F(FieldOwnershipTest, NothingIsCountedOutsideAServerStampedVerb) {
MGPipeValidateForVerb(MGPipeVerb::ReadPixels);
(void)gPipeInputs.ValidateProgramName(1u);
gPipeInputs.InvalidateCompileEnv();
(void)gPipeInputs.GetActiveTextureUnit();
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{0});
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
}
// THE APPLIER'S OWN CLEAR, reached directly rather than through the client's fill. In a spawned
// server MG_Impl is not in the process, so MGPipeValidateForVerb/MGPipeLeaveVerb never run and
// MGPipeServerClearVerbBoundary is the ONLY thing that can disarm the flag; without it the
// server latches TRUE after its first stamp and the sticky exemption - the one
// InvalidateCompileEnv is reached from backend initialisation under - is gone for good.
TEST_F(FieldOwnershipTest, TheAppliersOwnClearDisarmsTheStampWithoutTheClientsFill) {
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
ASSERT_TRUE(gPipeInputs.ServerStampedVerb());
(void)gPipeInputs.ValidateProgramName(1u);
ASSERT_EQ(MGPipeResidualPullCount(), Uint64{1});
MGPipeServerClearVerbBoundary(); // what PipeApplier must call on leaving the applier
EXPECT_FALSE(gPipeInputs.ServerStampedVerb());
(void)gPipeInputs.ValidateProgramName(1u);
gPipeInputs.InvalidateCompileEnv();
EXPECT_EQ(MGPipeResidualPullCount(), Uint64{1}) << "a forward outside a stamped verb was counted";
}
// The verb's own may-read table still holds on the server: kClear does not read
// GetActiveTextureUnit, so reading it there is a stale answer rather than a residual pull,
// and it stays Fatal. Counting it would trade a loud staleness for a quiet one.
TEST_F(FieldOwnershipTest, ABarrierPulledFieldOutsideTheVerbsClassIsStillFatal) {
#if MGTEST_HAVE_FORK
const ChildResult r = RunInChild([] {
MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
(void)gPipeInputs.GetActiveTextureUnit(); // BARRIER-PULLED, but not in kClear's class
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@Clear\"}"), std::string::npos)
<< r.Log;
EXPECT_EQ(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
#else
GTEST_SKIP() << "needs fork";
#endif
}
TEST_F(FieldOwnershipTest, ResidualPullsReachThePublishedPerFrameCounter) {
namespace PS = MG_Util::PipeStats;
PS::SetEnabledForTesting(true);
PS::ResetForTesting();
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetActiveTextureUnit();
EXPECT_EQ(PS::FrameCalls(PS::CallClass::ResidualPulls), Uint64{1});
EXPECT_NE(PS::FormatWindowLine().find("rsp="), std::string::npos) << PS::FormatWindowLine();
PS::ResetForTesting();
PS::SetEnabledForTesting(false);
}
#if MGTEST_HAVE_FORK
// R-7.3's proof that the instrumentation can go red. An instrumentation that cannot is
// decoration, and the set it counts is not empty.
TEST_F(FieldOwnershipTest, StrictErrorsTurnsABarrierPulledReadIntoANamedAbort) {
const ChildResult r = RunInChild([] {
MG_Config::Ipc.StrictErrors = true;
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetActiveTextureUnit();
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@ReadPixels\"}"),
std::string::npos)
<< r.Log;
EXPECT_NE(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("MOBILEGL_IPC_STRICT_ERRORS=1"), std::string::npos) << r.Log;
// The strict line names the phase that owes the answer; a strict abort that did not would
// leave the reader exactly where the gate found them.
EXPECT_NE(r.Log.find("retires in P3b/P4b"), std::string::npos) << r.Log;
}
TEST_F(FieldOwnershipTest, TheSameReadWithoutStrictErrorsSurvivesAndIsCounted) {
const ChildResult r = RunInChild([] {
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetActiveTextureUnit();
if (MGPipeResidualPullCount() != 1) ::_exit(7);
});
ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log;
}
TEST_F(FieldOwnershipTest, StrictErrorsAlsoPromotesTheStickyForwards) {
const ChildResult r = RunInChild([] {
MG_Config::Ipc.StrictErrors = true;
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
(void)gPipeInputs.ValidateProgramName(1u);
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"ValidateProgramName@DrawArrays\"}"),
std::string::npos)
<< r.Log;
}
// A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never
// reads it, so it is a real defect rather than a debt.
TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) {
const ChildResult r = RunInChild([] {
MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays);
(void)gPipeInputs.GetProgramForDispatch();
});
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DrawArrays\"}"),
std::string::npos)
<< r.Log;
}
// The argument-keyed row: the pack half is answerable and the unpack half is not, and the
// difference is the accessor's own argument rather than a second field id. Splitting the field
// into two ids would have made the unpack half BARRIER-PULLED - silently served - which is
// weaker than this.
TEST_F(FieldOwnershipTest, TheUnpackHalfOfThePixelStoreAbortsWhileThePackHalfDoesNot) {
const ChildResult fatal = RunInChild([] {
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetPixelStoreParameters(true);
});
ASSERT_TRUE(DiedOfAbort(fatal)) << DescribeStatus(fatal) << "\n" << fatal.Log;
EXPECT_NE(fatal.Log.find("Fatal{UnmigratedPipeInput, \"GetPixelStoreParameters@ReadPixels\"}"),
std::string::npos)
<< fatal.Log;
// The line must say WHICH HALF. Without this the message is byte-identical to what a
// genuinely stale read of the pack half would print, and the whole case for narrowing by
// argument instead of by a second field id is that the reader is told which half they
// asked for.
EXPECT_NE(fatal.Log.find("argument 0 = 1 is FATAL while the field is APPLIER-DERIVED"),
std::string::npos)
<< fatal.Log;
const ChildResult ok = RunInChild([] {
MGPipeServerStampVerbBoundary(MGPipeVerb::ReadPixels);
(void)gPipeInputs.GetPixelStoreParameters(false);
if (MGPipeResidualPullCount() != 0) ::_exit(7);
});
ASSERT_TRUE(ExitedWith(ok, 0)) << DescribeStatus(ok) << "\n" << ok.Log;
EXPECT_EQ(ok.Log.find("Fatal{"), std::string::npos) << ok.Log;
}
// One function edit, five call sites (Managers.cpp:5334, DirectGLES.cpp:8051, :8702, :8997,
// :10623). The arm is the TRANSPORT, not the build: build-split's own lanes run monolith and
// several of the five are on ordinary monolith paths they exercise.
TEST_F(FieldOwnershipTest, AnUnmigratedEmulationIsFatalUnderARealTransportAndInertUnderMonolith) {
const ChildResult monolith = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::Monolith;
MGPipeUnmigratedEmulation("get-tex-image-shadow");
});
ASSERT_TRUE(ExitedWith(monolith, 0)) << DescribeStatus(monolith) << "\n" << monolith.Log;
EXPECT_EQ(monolith.Log.find("Fatal{"), std::string::npos) << monolith.Log;
const ChildResult split = RunInChild([] {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
MGPipeUnmigratedEmulation("get-tex-image-shadow");
});
ASSERT_TRUE(DiedOfAbort(split)) << DescribeStatus(split) << "\n" << split.Log;
EXPECT_NE(split.Log.find("Fatal{UnmigratedEmulation, \"get-tex-image-shadow\"}"), std::string::npos)
<< split.Log;
}
#endif // MGTEST_HAVE_FORK
#endif // MOBILEGL_BUILD_DISAGGREGATED
#endif // MOBILEGL_PIPE_PUSH
int main(int argc, char** argv) {
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
// write, and caches the FILE*. PipeInputsTest's idiom, and for its reason - the abort cases
// read their Fatal line back out of this file.
namespace fs = std::filesystem;
const fs::path path =
fs::temp_directory_path() / ("mobilegl-fieldownership-test-" + std::to_string(ProcessId()) + ".log");
std::error_code ec;
fs::remove(path, ec);
g_logPath = path.string();
#if defined(_WIN32)
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
#else
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
#endif
::testing::InitGoogleTest(&argc, argv);
const int rc = RUN_ALL_TESTS();
fs::remove(path, ec);
return rc;
}
+8
View File
@@ -180,6 +180,7 @@ namespace MobileGL::MG_Util::PipeStats {
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
"framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions",
"shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls",
"residual-pulls",
#endif
};
const char* const kGateNames[kGateCount] = {
@@ -457,6 +458,13 @@ namespace MobileGL::MG_Util::PipeStats {
// Espryt had already allocated and then had to re-mint image-bindable, replaying its
// levels from the client's shadow, because ImageBindableHint reached it too late.
line += " trp=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureRemintPulls)]);
// rsp is P5's residual-pull count: reads, on the server side, of a PipeInputs field no
// pushed record supplies, answered out of the client's residual fill under the verb
// barrier. It is the SIZE OF THE P6/P7/P8 DEBT and it is published on this line rather
// than only at teardown because the number an operator needs is per frame: a debt that
// tracks the draw count is a pull inside a loop, and one that tracks the frame count is
// a pull per verb. Zero in every monolith lane by construction.
line += " rsp=" + std::to_string(calls[static_cast<Uint32>(CallClass::ResidualPulls)]);
#endif
line += "] gates[";
for (Uint32 i = 0; i < kGateCount; ++i) {
+15
View File
@@ -164,6 +164,21 @@ namespace MobileGL::MG_Util::PipeStats {
// sync is allocated image-bindable up front and never counts. `trp=` on the summary
// line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default.
TextureRemintPulls,
// P5's, and push-only for the same reason as the nine above.
//
// `rsp` - THE SIZE OF THE P6/P7/P8 DEBT. One per read, on the server side, of a
// BARRIER-PULLED PipeInputs field (CONTRACT-P5.md table 2): a field no pushed record
// supplies, which the server answers by reading the value the CLIENT's residual fill
// left in the single shared gPipeInputs while the verb barrier holds both threads
// apart. That is correct only because of the barrier, which is what makes the barrier
// load-bearing rather than cautious - so the count is the debt, and a phase that
// retires a family of fields is expected to move it down.
//
// Counted at the ONE place that decides what a stale read means
// (MG_Backend/MGPipe/PipeInputs.cpp), so the 56 checked accessors and the seven sticky
// forwards cannot drift apart on it. It is zero in every monolith lane by
// construction: nothing arms it but a server verb-boundary stamp.
ResidualPulls,
#endif
Count
};
+10 -1
View File
@@ -80,10 +80,19 @@ CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska:
| G2 | `PipeThunks.inc` | monolith 直调 thunk `MGP_<Name>()``MG_Impl` 的约 93 个 `gBackendFunctionsTable.GL.*` 站点逐名改到它上面 |
| G3 | `PipeWire.inc` | wire 记录 + 每种一条尺寸 `static_assert` + applier 分发前的运行期边界检查 → `Fatal{ProtocolCorruption}` |
| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表来自 `PipeFields.def`;浮点按位比较,NaN patch level 不会误报) |
| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id61 个)与逐 verb 世代 poison |
| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id**63 个**`static_assert(kMGPipeInputFieldCount == 63)`)与逐 verb 世代 poison |
| G6 | `PipeCoverage.inc` | 477 行后端读点清单 → MGPipe 调用的映射(`Coverage.def` 手工维护一半):299 → 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED** |
| G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2 |
**G8(P5 新增,另一个生成器)** `scripts/gen_pipe_field_ownership.py``generated/PipeFieldOwnership.inc`
`CONTRACT-P5.md` §3 的表 263 个字段 + 7 个 sticky forward = **70 行**,每行恰好属于
`RECORD-SUPPLIED / APPLIER-DERIVED / BARRIER-PULLED / FATAL` 之一,**不在任何一类里 = 构建失败**(R-7.1)。
`RECORD-SUPPLIED` 是**推导**出来的(`Coverage.def` 的 emitted 列表减去 `PipeFill.cpp`
`EmittedCallSuppliesTheWholeField` 拒绝项),手工那一半在 `MG_Pipe/FieldOwnership.def`
它从 `MG_Backend/MGPipe/PipeInputs.h` include**不从 `MG_Pipe/MGPipe.h`**——后者在 pull 构建的
include 闭包里,而 G1 不允许那里有任何符号位移。`--check``--self-test`11 条阴性对照)与
`gen_pipe.py` 的同样在 CI 里跑。
### 3.2 分组与计数
| Class | 条 | 内容 |
+1 -1
View File
@@ -79,7 +79,7 @@ python3 tools/trace_replay/run_android_retrace_local.py \
- **llvmpipe / lavapipe 动态 accessor**`GuiBatchScenario`14 帧 / 26 drawmemo 冷):Espryt 20.65 / Magma 15.54 次/draw——落在预测区间内,且因场景太短偏高;真机稳态数字见 §3。
- **dirty-surface 面**`python3 scripts/gen_pipe_dirty_surface.py --summary`,本树):`MG_Impl/GLImpl` 41 个文件,926 次 mutator 调用,73 个不同 mutator`RecordError` 一项就占 836 次);92 次(36 个即时发布点、7 个 mutator,绝大多数是 `RecordError`)位于同函数内也到达后端的入口,其余 834 次由紧随的 verb 发布。映射表是 73 条目的问题。
- **读点覆盖**`python3 scripts/gen_pipe.py`):71 条调用(11 screen / 60 context)、63 个 verify payload、61`PipeInputs` 字段;477 行后端读点清单 → 299 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED**。
- **读点覆盖**`python3 scripts/gen_pipe.py`):71 条调用(11 screen / 60 context)、63 个 verify payload、**63**`PipeInputs` 字段(旧数 61 是 P1 之前的,`gen_pipe.py` 自己打印 63;477 行后端读点清单 → 299 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED**。
- **OOM 探测惯用法**41 个 trace fixture 中 0 例——全部语料只有 9 次 `glRenderbufferStorage` 调用散在 5 个 fixture,无一在其后 3 个调用内跟 `glGetError`;语料里真实的成功性检查是 `glCheckFramebufferStatus`。→ `glRenderbufferStorage*` 不 ack。
- **`FramebufferSrgb` / `DepthClamp`**`FramebufferSrgb` 的六个后端读点全部消费一个编译期常量 `false``DepthClamp` 零读点;两者的 `glEnable` 落到 `RenderState.cpp``default:` 分支既不存储也不报 `GL_INVALID_ENUM`41 个 fixture 无一开启任一项(补真存储不会改动任何既有 fixture 的输出)。
- **`GetIntegeri_v` 族**Espryt 实现里是 `GetIntegeri_v` 的 9 个分支 + `GetInteger64i_v` 的 2 个(不是"15 个 case");`GL_COMPUTE_WORK_GROUP_SIZE``GL_Program.cpp``ProgramObject::GetComputeLocalSize` 纯前端回答。
+772
View File
@@ -0,0 +1,772 @@
#!/usr/bin/env python3
# MobileGL - scripts/gen_pipe_field_ownership.py
# 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
"""TABLE 2 - where every PipeInputs field's value comes from once the backend is a server.
CONTRACT-P5.md section 3 / BRIEF-P5 R-7. Reads
MobileGL/MG_Pipe/Coverage.def the 63 accessors, the 7 sticky, the 40 emitted
MobileGL/MG_Impl/Pipe/PipeFill.cpp EmittedCallSuppliesTheWholeField's refusals
MobileGL/MG_Pipe/FieldOwnership.def the hand-maintained half
and writes MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc. The output is COMMITTED; CI
regenerates it and fails on a diff, exactly as gen_pipe.py's seven outputs do.
ONE CLASS PER ROW, AND THE BUILD FAILS OTHERWISE. 63 field rows + the 7 sticky forwards =
70, each in exactly one of RECORD_SUPPLIED / APPLIER_DERIVED / BARRIER_PULLED / FATAL. A
field in none of them stops this script, so the committed header can never contain an
unclassified row and --check is what catches a stale one. RECORD_SUPPLIED is DERIVED rather
than listed - it is Coverage.def's emitted list minus PipeFill.cpp's refusals - so the
mapping file cannot drift away from the emitters it describes without a red gate.
python3 scripts/gen_pipe_field_ownership.py # write it, print the summary
python3 scripts/gen_pipe_field_ownership.py --check # THE GATE: rc 1 on any hole
python3 scripts/gen_pipe_field_ownership.py --self-test # the gate's own negative controls
"""
import argparse
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
COVERAGE_DEF = os.path.join(PIPE_DIR, "Coverage.def")
OWNERSHIP_DEF = os.path.join(PIPE_DIR, "FieldOwnership.def")
PIPE_FILL = os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "Pipe", "PipeFill.cpp")
PIPE_CALLS = os.path.join(PIPE_DIR, "PipeCalls.def")
FILL_POINTS = os.path.join(PIPE_DIR, "FillPoints.def")
OUT_NAME = "PipeFieldOwnership.inc"
CLASSES = ("RECORD_SUPPLIED", "APPLIER_DERIVED", "BARRIER_PULLED", "FATAL")
ENUMERATOR = {
"RECORD_SUPPLIED": "kRecordSupplied",
"APPLIER_DERIVED": "kApplierDerived",
"BARRIER_PULLED": "kBarrierPulled",
"FATAL": "kFatal",
}
BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
// 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
// TABLE 2: PipeInputs field ownership (CONTRACT-P5.md section 3, R-7).
//
// GENERATED by scripts/gen_pipe_field_ownership.py from Coverage.def, FieldOwnership.def and
// MG_Impl/Pipe/PipeFill.cpp - DO NOT EDIT. Regenerate with
// `python3 scripts/gen_pipe_field_ownership.py`; CI runs it and diffs the result.
//
// Included from MG_Backend/MGPipe/PipeInputs.h inside namespace MobileGL::MG_Pipe, which is
// the one header that both the poison check and the server's verb stamp already see. It is
// NOT included from MG_Pipe/MGPipe.h with the other seven generated files, on purpose: that
// header is in the PULL build's include closure and G1 admits no symbol motion there.
"""
def read(path):
with open(path, "r", encoding="utf-8") as handle:
return handle.read()
def mask_comments(text):
"""Blank comment bodies, keeping every offset and newline. String literals are LEFT
ALONE - unlike gen_pipe_dirty_surface.py's masker - because this file's rows carry their
retiring phase and their reason as quoted arguments."""
out = list(text)
i = 0
n = len(text)
while i < n:
if text[i] == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] != "\n":
out[i] = " "
i += 1
elif text[i] == "/" and i + 1 < n and text[i + 1] == "*":
out[i] = out[i + 1] = " "
i += 2
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
if text[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = out[i + 1] = " "
i += 2
else:
i += 1
return "".join(out)
def macro_block(text, name):
"""The body of a `#define <name>(X) ...` continued-line macro, comments blanked."""
masked = mask_comments(text)
start = masked.find("#define %s(X)" % name)
if start < 0:
sys.exit("gen_pipe_field_ownership: %s is not in the file" % name)
end = start
while True:
line_end = masked.find("\n", end)
if line_end < 0:
body = masked[start:]
break
if not masked[end:line_end].rstrip().endswith("\\"):
body = masked[start:line_end]
break
end = line_end + 1
# The rows wrap, so the continuation backslashes have to go before a row regex can see a
# row as one thing. Offsets do not matter here; only the token sequence does.
return body.replace("\\\n", "\n")
def parse_coverage(text=None):
"""The ordered 63 accessors, the 7 sticky and the 40 emitted, out of Coverage.def."""
text = read(COVERAGE_DEF) if text is None else text
accessors = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
macro_block(text, "MGP_COVERAGE_ACCESSOR_LIST"))
sticky = re.findall(r"X\(\s*(\w+)\s*,", macro_block(text, "MGP_COVERAGE_STICKY_LIST"))
emitted = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
macro_block(text, "MGP_COVERAGE_EMITTED_LIST"))
if not accessors:
sys.exit("gen_pipe_field_ownership: Coverage.def's accessor list did not parse")
seen = set()
for name in accessors:
if name in seen:
sys.exit("gen_pipe_field_ownership: %s appears twice in the accessor list" % name)
seen.add(name)
for name in sticky + emitted:
if name not in seen:
sys.exit("gen_pipe_field_ownership: %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST"
% name)
return accessors, sticky, emitted
def parse_supplies_whole_field(text=None):
"""The fields EmittedCallSuppliesTheWholeField REFUSES, read out of PipeFill.cpp rather
than restated here - the derivation that keeps RECORD_SUPPLIED honest is only as good as
its source, so the source is the function itself."""
text = read(PIPE_FILL) if text is None else text
masked = mask_comments(text)
start = masked.find("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)")
if start < 0:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField is not in %s"
% os.path.basename(PIPE_FILL))
false_at = masked.find("return false;", start)
if false_at < 0:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField has no `return false` arm")
refused = re.findall(r"case\s+MGPipeInputField::(\w+)\s*:", masked[start:false_at])
if not refused:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField refuses nothing - "
"either the function moved or the parse broke; a silently empty refusal set "
"would make every emitted field RECORD_SUPPLIED")
return refused
ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*,\s*\"([^\"]*)\"\s*\)")
ARG_ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\d+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*\)")
PAIR_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*\)")
# An exemption's reason may be one string or several adjacent ones, the way a long C literal is
# written; the row regex takes the first and the check only cares that there is one.
EXEMPT_RE = re.compile(r"X\(\s*(\w+)\s*,\s*\"([^\"]*)\"")
def parse_ownership(text=None):
text = read(OWNERSHIP_DEF) if text is None else text
fields = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_LIST"))
forwards = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_FORWARD_LIST"))
args = ARG_ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_ARG_LIST"))
verb_ops = PAIR_RE.findall(macro_block(text, "MGP_VERB_OP_LIST"))
exempt = EXEMPT_RE.findall(macro_block(text, "MGP_VERB_OP_EXEMPT_LIST"))
if not fields:
sys.exit("gen_pipe_field_ownership: FieldOwnership.def's field list did not parse")
return fields, forwards, args, verb_ops, exempt
def parse_ops_and_verbs(calls_text=None, fill_points_text=None):
"""The two name spaces the stamp map joins, read from the files that define them: the
catalogue's calls (PipeCalls.def, which IS the MGPWireOp enum, with each call's KIND) and
the verb set (FillPoints.def, which IS MG_Backend::GLFunctionsTable's member list)."""
calls_text = read(PIPE_CALLS) if calls_text is None else calls_text
fill_points_text = read(FILL_POINTS) if fill_points_text is None else fill_points_text
calls = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*,\s*(k\w+)\s*,",
macro_block(calls_text, "MGP_CALL_LIST"))
verbs = re.findall(r"X\(\s*(\w+)\s*,\s*(k\w+)\s*\)",
macro_block(fill_points_text, "MGP_FILL_VERB_LIST"))
if not calls:
sys.exit("gen_pipe_field_ownership: PipeCalls.def's call list did not parse")
if not verbs:
sys.exit("gen_pipe_field_ownership: FillPoints.def's verb list did not parse")
return calls, [v for v, _ in verbs]
def verb_shaped_calls(calls, verbs):
"""The calls that MUST have a stamp row or an exemption, by two derived tests: the
catalogue's own kind (kCtxVerb) and a name that is also a verb's. Neither can see a call
that is a verb boundary, is not kCtxVerb and is renamed - FieldOwnership.def names the one
such call in the tree and says the phase that emits it writes its row."""
verbSet = set(verbs)
return [name for name, kind in calls if kind == "kCtxVerb" or name in verbSet]
def check_verb_ops(verb_ops, exempt, calls, verbs):
"""The stamp map, in BOTH directions.
A row whose op or verb does not exist would become a switch arm that fails to compile. An
OMITTED row is worse and is what this check exists for: the applier would run the record
under the PREVIOUS verb's serial, mask and name, so a field inside that mask reads FRESH
while holding the previous verb's value - the one failure in this package that is silent."""
opNames = [name for name, _ in calls]
if not verb_ops:
sys.exit("gen_pipe_field_ownership: MGP_VERB_OP_LIST is empty - the server would have "
"no verb boundary to stamp at and every field would read @<none>")
seen = set()
for op, verb in verb_ops:
if op not in opNames:
sys.exit("gen_pipe_field_ownership: the stamp map names op %s, which is not a call "
"in PipeCalls.def" % op)
if verb not in verbs:
sys.exit("gen_pipe_field_ownership: the stamp map names verb %s, which is not a "
"verb in FillPoints.def" % verb)
if op in seen:
sys.exit("gen_pipe_field_ownership: op %s has two stamp rows" % op)
seen.add(op)
required = verb_shaped_calls(calls, verbs)
exemptMap = {}
for op, why in exempt:
if op not in opNames:
sys.exit("gen_pipe_field_ownership: the stamp map exempts op %s, which is not a call "
"in PipeCalls.def" % op)
if op not in required:
sys.exit("gen_pipe_field_ownership: op %s is exempted from the stamp map but is not "
"verb-shaped, so it was never required - an exemption that exempts nothing "
"reads as a decision that was made" % op)
if op in seen:
sys.exit("gen_pipe_field_ownership: op %s has both a stamp row and an exemption" % op)
if not why.strip():
sys.exit("gen_pipe_field_ownership: op %s is exempted with no reason" % op)
exemptMap[op] = why
missing = [op for op in required if op not in seen and op not in exemptMap]
if missing:
sys.exit("gen_pipe_field_ownership: %d verb-shaped call(s) have no stamp row and no "
"exemption: %s - an omitted stamp point is SILENT (the record would apply under "
"the previous verb's serial, mask and name), so it is a build failure here"
% (len(missing), ", ".join(missing)))
return verb_ops, exemptMap, required
def build(accessors, sticky, emitted, refused, rows, forwards, args):
"""The join, and every gate it is allowed to fail on. Returns
(ownership, phase, why, forward rows, argument rows, counts)."""
supplied = [f for f in emitted if f not in set(refused)]
ownership = {}
phase = {}
why = {}
for field in supplied:
ownership[field] = "RECORD_SUPPLIED"
phase[field] = "-"
why[field] = "derived: Coverage.def's emitted list, and PipeFill.cpp does not refuse it"
for field, cls, retires, reason in rows:
if field not in accessors:
sys.exit("gen_pipe_field_ownership: FieldOwnership.def names %s, which is not a "
"PipeInputs field" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: %s is in class %s, which is not one of %s"
% (field, cls, "/".join(CLASSES)))
if cls == "RECORD_SUPPLIED":
sys.exit("gen_pipe_field_ownership: %s claims RECORD_SUPPLIED, which is DERIVED and "
"may not be asserted by hand" % field)
if field in ownership:
sys.exit("gen_pipe_field_ownership: %s is in TWO classes - the derivation says "
"RECORD_SUPPLIED and FieldOwnership.def says %s" % (field, cls))
if cls == "BARRIER_PULLED" and retires.strip() in ("", "-"):
sys.exit("gen_pipe_field_ownership: %s is BARRIER_PULLED and names no retiring "
"phase - P5's debt is only sized if every row says who pays it" % field)
if cls != "BARRIER_PULLED" and retires.strip() != "-":
sys.exit("gen_pipe_field_ownership: %s is %s and names a retiring phase; only "
"BARRIER_PULLED rows have one" % (field, cls))
ownership[field] = cls
phase[field] = retires
why[field] = reason
missing = [f for f in accessors if f not in ownership]
if missing:
sys.exit("gen_pipe_field_ownership: %d field(s) in NO class, which is a build failure "
"(R-7.1): %s" % (len(missing), ", ".join(missing)))
# The seven sticky forwards' own rows. They are the seven that hand the server a frontend
# object or write into the frontend, so the gate is structurally blind on them without a
# row of their own - and the row has to agree with the field row, or the two halves of the
# same accessor would be documented as different things.
forward_map = {}
for field, cls, retires, mechanism in forwards:
if field not in sticky:
sys.exit("gen_pipe_field_ownership: %s has a FORWARD row but is not sticky in "
"Coverage.def" % field)
if field in forward_map:
sys.exit("gen_pipe_field_ownership: %s has two FORWARD rows" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: forward %s is in class %s, which is not one of "
"%s" % (field, cls, "/".join(CLASSES)))
if ownership[field] != cls:
sys.exit("gen_pipe_field_ownership: %s's field row says %s and its forward row says "
"%s; a read of a sticky field IS a call of its forward" % (field, ownership[field], cls))
forward_map[field] = (cls, retires, mechanism)
absent = [f for f in sticky if f not in forward_map]
if absent:
sys.exit("gen_pipe_field_ownership: sticky forward(s) with no row: %s" % ", ".join(absent))
arg_rows = []
for field, arg0, cls, reason in args:
if field not in accessors:
sys.exit("gen_pipe_field_ownership: argument exception names %s, which is not a "
"PipeInputs field" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) is in class %s"
% (field, arg0, cls))
if cls == ownership[field]:
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) repeats the field's "
"own class (%s) and narrows nothing" % (field, arg0, cls))
arg_rows.append((field, int(arg0), cls, reason))
counts = {cls: sum(1 for f in accessors if ownership[f] == cls) for cls in CLASSES}
return ownership, phase, why, forward_map, arg_rows, counts
def emit(accessors, sticky, ownership, phase, why, forward_map, arg_rows, counts, verb_ops,
exemptMap):
out = [BANNER.format(name=OUT_NAME)]
add = out.append
add("""
// The four classes. kUnclassified exists so the static_assert below has something to refuse;
// the generator never emits it, which is what makes "a field in no class fails the build"
// true at two independent points rather than one.
enum class MGPipeFieldOwnership : Uint8 {
kUnclassified = 0,
kRecordSupplied, // a pushed record supplies the WHOLE field
kApplierDerived, // the applier writes it out of records it already applies
kBarrierPulled, // P5's debt: read out of the client's residual fill under the verb barrier
kFatal, // no carrier and the reduced path never reads it
};
inline constexpr const char* kMGPipeFieldOwnershipNames[] = {
"UNCLASSIFIED", "RECORD-SUPPLIED", "APPLIER-DERIVED", "BARRIER-PULLED", "FATAL",
};
""")
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCount] = {")
for field in accessors:
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[ownership[field]], field))
add("};\n")
add("// The ROADMAP phase whose row retires the pull. \"-\" for every class but BARRIER-PULLED.")
add("inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] = {")
for field in accessors:
add(" \"%s\", // %s" % (phase[field], field))
add("};\n")
add("// The seven sticky forwards, which are among the 63 above and need a row of their own:")
add("// they are the ones that hand the server a raw frontend object or write into the")
add("// frontend, so the exit gate is structurally blind on them without one.")
add("inline constexpr SizeT kMGPipeFieldOwnershipForwardCount = %d;" % len(sticky))
add("static_assert(kMGPipeFieldOwnershipForwardCount == kMGPipeInputStickyFieldCount,")
add(" \"the forward rows and Coverage.def's sticky set are the same seven\");")
add("inline constexpr MGPipeInputField kMGPipeFieldOwnershipForwardField[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" MGPipeInputField::%s," % field)
add("};")
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnershipForward[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[forward_map[field][0]], field))
add("};")
add("inline constexpr const char* kMGPipeFieldOwnershipForwardMechanism[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" \"%s\", // %s, retires in %s" % (forward_map[field][2], field, forward_map[field][1]))
add("};\n")
add("// CONTRACT-P5.md section 3: \"70 rows, each in exactly one class\".")
add("inline constexpr SizeT kMGPipeFieldOwnershipRowCount =")
add(" kMGPipeInputFieldCount + kMGPipeFieldOwnershipForwardCount;")
add("static_assert(kMGPipeFieldOwnershipRowCount == 70, \"table 2's row count moved\");\n")
add("""// An ARGUMENT-KEYED narrowing of one field. The field keeps its single row above; this
// says that one argument value of it belongs to a different class. Coverage.def:62-69 already
// rules the shape for GetBufferBindingSlot - "THE ROW STAYS ONE ROW ... the field is ONE array
// that a second row of the same name could only duplicate" - and m_pixelStore[2] is the same
// shape indexed by its own isUnpack argument.
struct MGPipeFieldArgumentOwnership {
MGPipeInputField Field;
Uint32 Arg0;
MGPipeFieldOwnership Class;
};""")
add("inline constexpr SizeT kMGPipeFieldArgumentOwnershipCount = %d;" % len(arg_rows))
add("inline constexpr MGPipeFieldArgumentOwnership")
add(" kMGPipeFieldArgumentOwnership[kMGPipeFieldArgumentOwnershipCount] = {")
for field, arg0, cls, reason in arg_rows:
add(" {MGPipeInputField::%s, %du, MGPipeFieldOwnership::%s}, // %s"
% (field, arg0, ENUMERATOR[cls], reason))
add("};\n")
add("""constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field) {
return kMGPipeFieldOwnership[static_cast<SizeT>(field)];
}
// The same answer, narrowed by the accessor's first argument. Every accessor that takes one
// may call this; only the fields with a row above answer differently from the field's class.
constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field, Uint32 arg0) {
for (SizeT i = 0; i < kMGPipeFieldArgumentOwnershipCount; ++i) {
if (kMGPipeFieldArgumentOwnership[i].Field == field &&
kMGPipeFieldArgumentOwnership[i].Arg0 == arg0) {
return kMGPipeFieldArgumentOwnership[i].Class;
}
}
return MGPipeFieldOwnershipOf(field);
}
constexpr const char* MGPipeFieldOwnershipName(MGPipeFieldOwnership ownership) {
return kMGPipeFieldOwnershipNames[static_cast<SizeT>(ownership)];
}
// THE BUILD FAILURE R-7.1 ASKS FOR. The generator refuses to emit an unclassified row, so
// this can only fire on a hand-edited header - which is exactly the edit the DO NOT EDIT
// banner cannot prevent on its own.
constexpr Bool MGPipeEveryFieldIsClassified() {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
if (kMGPipeFieldOwnershipForward[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
return true;
}
static_assert(MGPipeEveryFieldIsClassified(),
"a PipeInputs field is in none of the four ownership classes (CONTRACT-P5 table 2, R-7.1)");
""")
add("""// WHERE THE SERVER STAMPS. The wire's op and the fill's verb are different name spaces
// and do not line up by name (draw_vbo is DrawArrays, blit is BlitFramebuffer), so this is the
// join. An op with no row is NOT a verb boundary and the applier must not stamp on it.
//
// EVERY VERB-SHAPED CALL IS ANSWERED HERE OR EXEMPTED BY NAME, and the generator refuses an
// omission: a verb-shaped record with no row would apply under the PREVIOUS verb's serial,
// mask and name, so a field inside that mask would read FRESH while holding the previous
// verb's value - the one silent failure this table has. The exemptions:""")
for op in sorted(exemptMap):
add("// %-16s %s" % (op, exemptMap[op]))
add("""constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
switch (op) {""")
for op, verb in verb_ops:
add(" case MGPWireOp::%s: return MGPipeVerb::%s;" % (op, verb))
add(""" default:
return MGPipeVerb::kVerbCount;
}
}
""")
add("inline constexpr SizeT kMGPipeVerbBoundaryOpCount = %d;" % len(verb_ops))
add("inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = %d;" % len(exemptMap))
add("")
add("// The class sizes, as constants a test can pin without recounting the table.")
for cls in CLASSES:
add("inline constexpr SizeT kMGPipe%sFieldCount = %d;"
% ("".join(p.capitalize() for p in cls.split("_")), counts[cls]))
add("static_assert(%s == kMGPipeInputFieldCount, \"the four class sizes do not partition the field set\");"
% " + ".join("kMGPipe%sFieldCount" % "".join(p.capitalize() for p in cls.split("_"))
for cls in CLASSES))
return "\n".join(out) + "\n"
def write(path, text, check_only, changed):
existing = read(path) if os.path.exists(path) else None
if existing == text:
return
changed.append(os.path.basename(path))
if not check_only:
with open(path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
def expect_trip(name, because, fn, quiet=False):
"""A control that must exit FOR ITS OWN REASON.
The first version of this function caught any SystemExit and asked nothing about which one,
and that is exactly how control #4 came to be a silent duplicate of control #1: its
replacement string was raw, so it mangled the row instead of blanking the phase, the row
stopped parsing, and the generator exited with "a field in NO class" while the report
counted a trip for "a BARRIER_PULLED row with no retiring phase". The guard that every debt
row names its retiring phase therefore had no control at all.
`because` is a substring the control's own message must contain. This is the discipline
gen_pipe_dirty_surface.py's self_test already uses (it asserts each control's problem
string) and it is the half that was dropped."""
try:
fn()
except SystemExit as exit:
message = str(exit.code) if exit.code is not None else ""
if because in message:
return 1
if not quiet:
print("gen_pipe_field_ownership: self-test: control %r tripped for SOMEONE ELSE'S "
"reason:\n expected to contain: %s\n actually said: %s"
% (name, because, message), file=sys.stderr)
return 0
if not quiet:
print("gen_pipe_field_ownership: self-test: control did NOT trip: %s" % name, file=sys.stderr)
return 0
def self_test():
"""The negative controls (gen_pipe.py --self-test's shape): each gate must go red for its
own reason, and zero trips is itself an error."""
coverage = read(COVERAGE_DEF)
ownership_text = read(OWNERSHIP_DEF)
fill = read(PIPE_FILL)
accessors, sticky, emitted = parse_coverage(coverage)
refused = parse_supplies_whole_field(fill)
calls, verbs = parse_ops_and_verbs()
def run(own_text=None, cov=None, fill_text=None, calls_text=None):
acc, stk, emt = parse_coverage(cov if cov is not None else coverage)
ref = parse_supplies_whole_field(fill_text if fill_text is not None else fill)
rows, fwd, args, verb_ops, exempt = parse_ownership(
own_text if own_text is not None else ownership_text)
c, v = parse_ops_and_verbs(calls_text) if calls_text is not None else (calls, verbs)
check_verb_ops(verb_ops, exempt, c, v)
return build(acc, stk, emt, ref, rows, fwd, args)
# The rows wrap over two lines with a trailing backslash, so every control below edits
# them through a regex whose gaps tolerate that rather than through a literal that would
# silently stop matching the day someone re-aligns the file.
GAP = r"[\s\\]*"
def edit(pattern, replacement, what, text=None):
text = ownership_text if text is None else text
edited, count = re.subn(pattern, replacement, text, count=1)
if count != 1:
sys.exit("gen_pipe_field_ownership: self-test: could not %s - the control's own "
"edit no longer matches the file it is supposed to break" % what)
return edited
# Every control below is (name, the substring its own message must contain, the edit). The
# substring is not decoration: see expect_trip.
# 1. THE HEADLINE CONTROL (exit gate E4): take one field out of the table. It is in no
# class, and that is a build failure rather than a silent default.
dropped = edit(r"X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED," + GAP + r"\"[^\"]*\","
+ GAP + r"\"[^\"]*\"\)", "", "remove GetActiveTextureUnit's row")
controls = [("a field in NO class (GetActiveTextureUnit's row removed)",
"field(s) in NO class",
lambda: run(own_text=dropped))]
# 2. The other direction: a field the derivation already placed in RECORD_SUPPLIED, also
# claimed by hand. E4's negative control is "move a field from supplied to FATAL".
doubled = ownership_text.replace(
"#define MGP_FIELD_OWNERSHIP_LIST(X)",
"#define MGP_FIELD_OWNERSHIP_LIST(X) X(GetClearColor, FATAL, \"-\", \"moved by hand\") \\\n", 1)
controls.append(("a RECORD_SUPPLIED field claimed by hand (supplied -> FATAL)",
"GetClearColor is in TWO classes",
lambda: run(own_text=doubled)))
# 3. A row naming something that is not a field at all.
typo = edit(r"X\(GetActiveTextureUnit,", "X(GetActiveTextureUnitt,", "misspell a field name")
controls.append(("a row naming a non-field",
"GetActiveTextureUnitt, which is not a PipeInputs field",
lambda: run(own_text=typo)))
# 4. A BARRIER_PULLED row with no retiring phase: the debt is only sized if every row
# says who pays it, which is the whole of R-7.2's "rsp IS the size of the debt".
# THE REPLACEMENT IS NOT A RAW STRING. It was, and the backslashes survived into the
# substitution, mangled the row past ROW_RE's reach and made this control a silent
# duplicate of #1 for a whole round.
unphased = edit(r"(X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED,)" + GAP + r"\"[^\"]*\",",
"\\1 \"-\",", "blank a BARRIER_PULLED row's retiring phase")
controls.append(("a BARRIER_PULLED row with no retiring phase",
"GetActiveTextureUnit is BARRIER_PULLED and names no retiring phase",
lambda: run(own_text=unphased)))
# 5. A class that is not one of the four.
bogus = edit(r"(X\(GetActiveTextureUnit,)" + GAP + r"BARRIER_PULLED,",
"\\1 SOMEHOW_FINE,", "introduce a fifth class")
controls.append(("a fifth class",
"GetActiveTextureUnit is in class SOMEHOW_FINE",
lambda: run(own_text=bogus)))
# 6. A sticky forward that lost its own row - the seven most dangerous fields are exactly
# the ones a table without forward rows is blind to.
no_forward = edit(r"X\(RecordError," + GAP + r"BARRIER_PULLED," + GAP + r"\"P9\"," + GAP
+ r"\"OnGlError[^\"]*\"\)", "", "remove RecordError's forward row")
controls.append(("a sticky forward with no row",
"sticky forward(s) with no row: RecordError",
lambda: run(own_text=no_forward)))
# 7. A forward row that disagrees with its field row.
disagree = edit(r"X\(GetTextureObject," + GAP + r"BARRIER_PULLED," + GAP + r"\"P7\"," + GAP
+ r"\"a server-side texture handle table\"\)",
"X(GetTextureObject, FATAL, \"-\", \"a server-side texture handle table\")",
"contradict GetTextureObject's field row")
controls.append(("a forward row that contradicts its field row",
"field row says BARRIER_PULLED and its forward row says FATAL",
lambda: run(own_text=disagree)))
# 8. An argument exception that narrows nothing.
same = edit(r"X\(GetPixelStoreParameters, 1, FATAL,",
"X(GetPixelStoreParameters, 1, APPLIER_DERIVED,",
"make the argument exception repeat the field's class")
controls.append(("an argument exception that repeats the field's class",
"narrows nothing",
lambda: run(own_text=same)))
# 9. THE DERIVATION'S OWN SOURCE. If EmittedCallSuppliesTheWholeField's refusals stop
# parsing, every emitted field silently becomes RECORD_SUPPLIED and eight rows of this
# table quietly contradict themselves - so an empty refusal set has to stop the script
# rather than produce a plausible table.
blinded = fill.replace("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)",
"Bool EmittedCallSuppliesTheWholeFieldXX(MGPipeInputField field)", 1)
controls.append(("the derivation's source function renamed away",
"EmittedCallSuppliesTheWholeField is not in PipeFill.cpp",
lambda: run(fill_text=blinded)))
# 10-11. THE STAMP MAP against both name spaces.
bad_op = edit(r"X\(Clear,\s*Clear\)", "X(Klear, Clear)", "misspell a stamp map op")
controls.append(("a stamp row naming an op that does not exist",
"names op Klear, which is not a call in PipeCalls.def",
lambda: run(own_text=bad_op)))
bad_verb = edit(r"X\(DrawVbo,\s*DrawArrays\)", "X(DrawVbo, DrawArrayz)",
"misspell a stamp map verb")
controls.append(("a stamp row naming a verb that does not exist",
"names verb DrawArrayz, which is not a verb in FillPoints.def",
lambda: run(own_text=bad_verb)))
# 12. THE OMISSION, which the first version of this gate could not see at all. A
# verb-shaped call with neither a row nor an exemption is the one silent failure in
# this package: the record applies under the PREVIOUS verb's serial, mask and name.
no_row = edit(r"X\(GenerateMipmap,\s*GenerateMipmap\)", "", "remove GenerateMipmap's stamp row")
controls.append(("a verb-shaped call with no stamp row and no exemption",
"have no stamp row and no exemption: GenerateMipmap",
lambda: run(own_text=no_row)))
# 13. An exemption is a decision, so it may not be written for a call that was never
# required - that would read as a ruling where there was none.
idle = edit(r"#define MGP_VERB_OP_EXEMPT_LIST\(X\)",
"#define MGP_VERB_OP_EXEMPT_LIST(X) X(SetDynamicState, \"not verb-shaped\") \\\n",
"exempt a call that was never required")
controls.append(("an exemption for a call that is not verb-shaped",
"SetDynamicState is exempted from the stamp map but is not verb-shaped",
lambda: run(own_text=idle)))
# 14. An exemption with no reason is an absence with extra steps.
mute = edit(r"X\(Flush," + GAP + r"\"", "X(Flush, \"\" \"", "blank an exemption's reason")
controls.append(("an exemption with no reason",
"Flush is exempted with no reason",
lambda: run(own_text=mute)))
# 15. THE REQUIRED SET'S OWN SOURCE. verb_shaped_calls reads PipeCalls.def's KIND column; if
# that column stops parsing as kCtxVerb the required set collapses to the name matches
# alone and control 12 would pass for the wrong reason. Renaming the kind is the cheapest
# way to prove the kind is actually being read.
kindless = read(PIPE_CALLS).replace("kCtxVerb", "kCtxVerbb")
controls.append(("the catalogue's kCtxVerb kind renamed away",
"is exempted from the stamp map but is not verb-shaped",
lambda: run(calls_text=kindless)))
# THE HARNESS'S OWN CONTROL, and it is the durable form of how M-1 was found. The defect was
# not the escaping in control #4; it was that expect_trip asked "did something exit" rather
# than "did THIS exit", so a control could silently become a duplicate of another. Prove the
# harness can tell them apart: control #1's edit, asserted against control #4's reason, must
# be REJECTED. Without this line the stricter harness could itself rot back.
if expect_trip("(harness control) control #1's edit under control #4's reason",
"names no retiring phase", lambda: run(own_text=dropped), quiet=True) != 0:
sys.exit("gen_pipe_field_ownership: self-test: expect_trip accepted an exit that belongs "
"to another control - the harness cannot tell one control from another, which is "
"exactly the defect that let control #4 be a silent duplicate of control #1")
trips = 0
for name, because, fn in controls:
trips += expect_trip(name, because, fn)
# The positive control: the real tables pass, and they partition the real field set.
_, _, _, _, _, counts = run()
total = sum(counts.values())
if total != len(accessors):
sys.exit("gen_pipe_field_ownership: self-test: the positive control does not partition "
"the field set (%d of %d)" % (total, len(accessors)))
if len(sticky) != 7:
sys.exit("gen_pipe_field_ownership: self-test: Coverage.def no longer has seven sticky fields")
if len(refused) != 9:
sys.exit("gen_pipe_field_ownership: self-test: EmittedCallSuppliesTheWholeField refuses %d "
"fields, not the nine the contract's derivation is written against" % len(refused))
if trips == 0:
sys.exit("gen_pipe_field_ownership: self-test: no negative control tripped - the gates are "
"not checking anything")
if trips != len(controls):
sys.exit("gen_pipe_field_ownership: self-test: %d of %d negative controls did not trip"
% (len(controls) - trips, len(controls)))
print("gen_pipe_field_ownership: self-test: %d negative-control trip(s), each asserted against "
"its OWN message; harness control OK; positive control OK "
"(%d fields partitioned, 7 sticky forwards, 9 refusals)" % (trips, total))
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="do not write; exit 1 if regenerating would change anything")
parser.add_argument("--self-test", action="store_true",
help="run the negative controls (each gate must trip) and exit")
args = parser.parse_args()
if args.self_test:
return self_test()
accessors, sticky, emitted = parse_coverage()
refused = parse_supplies_whole_field()
rows, forwards, arg_rows, verb_ops, exempt = parse_ownership()
calls, verbs = parse_ops_and_verbs()
_, exemptMap, required = check_verb_ops(verb_ops, exempt, calls, verbs)
ownership, phase, why, forward_map, arg_list, counts = build(
accessors, sticky, emitted, refused, rows, forwards, arg_rows)
if not os.path.isdir(GENERATED_DIR):
os.makedirs(GENERATED_DIR)
changed = []
write(os.path.join(GENERATED_DIR, OUT_NAME),
emit(accessors, sticky, ownership, phase, why, forward_map, arg_list, counts, verb_ops,
exemptMap),
args.check, changed)
print("gen_pipe_field_ownership: %d fields + %d sticky forwards = %d rows; "
"%d record-supplied (derived), %d applier-derived, %d barrier-pulled, %d fatal, "
"%d argument exception(s)"
% (len(accessors), len(sticky), len(accessors) + len(sticky),
counts["RECORD_SUPPLIED"], counts["APPLIER_DERIVED"], counts["BARRIER_PULLED"],
counts["FATAL"], len(arg_list)))
print("gen_pipe_field_ownership: stamp map: %d verb-shaped call(s) = %d row(s) + %d "
"exemption(s), 0 unanswered"
% (len(required), len(verb_ops), len(exemptMap)))
pulled = [(f, phase[f]) for f in accessors if ownership[f] == "BARRIER_PULLED"]
print("gen_pipe_field_ownership: the debt, by retiring phase:")
by_phase = {}
for field, retires in pulled:
by_phase.setdefault(retires, []).append(field)
for retires in sorted(by_phase):
print("gen_pipe_field_ownership: %-42s %d" % (retires, len(by_phase[retires])))
if changed:
if args.check:
print("gen_pipe_field_ownership: OUT OF DATE: %s" % ", ".join(changed), file=sys.stderr)
return 1
print("gen_pipe_field_ownership: wrote %s" % ", ".join(changed))
else:
print("gen_pipe_field_ownership: generated file is up to date")
return 0
if __name__ == "__main__":
sys.exit(main())