mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Feat] (MG_Pipe, scripts): generate the PipeInputs field-ownership table - 63 fields plus the seven sticky forwards in exactly one of four classes, RECORD-SUPPLIED derived from the emitted list rather than asserted, and a field in none of them stops the generator
This commit is contained in:
@@ -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,65 @@ 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.
|
||||
void 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 { \
|
||||
::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 +249,15 @@ 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 the client's next fill. 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 1842 unit cases and
|
||||
// its 1117 integration cases behave exactly as a verify build's do.
|
||||
Bool ServerStampedVerb() const { return m_serverStampedVerb; }
|
||||
#endif
|
||||
|
||||
// ---- V: values ----
|
||||
Int GetActiveTextureUnit() const {
|
||||
@@ -345,8 +409,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 +689,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 +703,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 +795,60 @@ 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.
|
||||
void MGPipeServerStampVerbBoundary(MGPipeVerb verb);
|
||||
// Called when the applier leaves the verb (and by the client's own fill). Clears the
|
||||
// arming flag, so a read outside a server verb is judged exactly as it is in monolith.
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
// 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. A later
|
||||
// phase whose record becomes a verb boundary adds its row here, and the generator refuses a
|
||||
// row whose op or verb does not exist.
|
||||
#define MGP_VERB_OP_LIST(X) \
|
||||
X(Clear, Clear) \
|
||||
X(DrawVbo, DrawArrays) \
|
||||
X(ReadPixels, ReadPixels) \
|
||||
X(Blit, BlitFramebuffer)
|
||||
// clang-format on
|
||||
@@ -0,0 +1,282 @@
|
||||
// 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.
|
||||
// Present is deliberately absent: FillPoints.def:21 - "Present and SetSwapInterval go through
|
||||
// BackendObject virtuals and read no frontend state, so they are not verbs here".
|
||||
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;
|
||||
default:
|
||||
return MGPipeVerb::kVerbCount;
|
||||
}
|
||||
}
|
||||
|
||||
inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 4;
|
||||
|
||||
// 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");
|
||||
@@ -0,0 +1,635 @@
|
||||
#!/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*\)")
|
||||
|
||||
|
||||
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"))
|
||||
if not fields:
|
||||
sys.exit("gen_pipe_field_ownership: FieldOwnership.def's field list did not parse")
|
||||
return fields, forwards, args, verb_ops
|
||||
|
||||
|
||||
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) 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
|
||||
ops = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*,\s*\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 ops:
|
||||
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 ops, [v for v, _ in verbs]
|
||||
|
||||
|
||||
def check_verb_ops(verb_ops, ops, verbs):
|
||||
"""The stamp map, against both name spaces. A row whose op or verb does not exist would
|
||||
otherwise become a switch arm that fails to compile minutes later - or, worse, a silently
|
||||
absent stamp point."""
|
||||
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 ops:
|
||||
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)
|
||||
return verb_ops
|
||||
|
||||
|
||||
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):
|
||||
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.
|
||||
// Present is deliberately absent: FillPoints.def:21 - "Present and SetSwapInterval go through
|
||||
// BackendObject virtuals and read no frontend state, so they are not verbs here".
|
||||
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("")
|
||||
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, fn):
|
||||
"""A control that must exit. Returns 1 when it did, and says so when it did not - a
|
||||
silent pass here is the gate not checking anything."""
|
||||
try:
|
||||
fn()
|
||||
except SystemExit:
|
||||
return 1
|
||||
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)
|
||||
|
||||
ops, verbs = parse_ops_and_verbs()
|
||||
|
||||
def run(own_text=None, cov=None, fill_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 = parse_ownership(own_text if own_text is not None else ownership_text)
|
||||
check_verb_ops(verb_ops, ops, verbs)
|
||||
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
|
||||
|
||||
# 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)",
|
||||
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)",
|
||||
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", 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".
|
||||
unphased = edit(r"(X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED,)" + GAP + r"\"[^\"]*\",",
|
||||
r"\1 \"-\",", "blank a BARRIER_PULLED row's retiring phase")
|
||||
controls.append(("a BARRIER_PULLED row with 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,",
|
||||
r"\1 SOMEHOW_FINE,", "introduce a fifth class")
|
||||
controls.append(("a fifth class", 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", 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", 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",
|
||||
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",
|
||||
lambda: run(fill_text=blinded)))
|
||||
|
||||
# 10. THE STAMP MAP against both name spaces. A row naming an op that is not a call, or a
|
||||
# verb that is not a verb, would otherwise be an arm that fails to compile - or an
|
||||
# absent stamp point, which is silent.
|
||||
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", 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", lambda: run(own_text=bad_verb)))
|
||||
|
||||
trips = 0
|
||||
for name, fn in controls:
|
||||
trips += expect_trip(name, 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), 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 = parse_ownership()
|
||||
ops, verbs = parse_ops_and_verbs()
|
||||
check_verb_ops(verb_ops, ops, 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),
|
||||
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), %d verb-boundary op(s)"
|
||||
% (len(accessors), len(sticky), len(accessors) + len(sticky),
|
||||
counts["RECORD_SUPPLIED"], counts["APPLIER_DERIVED"], counts["BARRIER_PULLED"],
|
||||
counts["FATAL"], len(arg_list), len(verb_ops)))
|
||||
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