mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Merge] (MGPipe, P5): integrate package p1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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` 字段 id(61 个)与逐 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 的表 2,63 个字段 + 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 | 条 | 内容 |
|
||||
|
||||
@@ -79,7 +79,7 @@ python3 tools/trace_replay/run_android_retrace_local.py \
|
||||
|
||||
- **llvmpipe / lavapipe 动态 accessor**(`GuiBatchScenario`,14 帧 / 26 draw,memo 冷):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` 纯前端回答。
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user