From bb6848c8d1d4dee94ab81ae9d40bb7a99de08a88 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 11 Sep 2026 14:12:32 -0400 Subject: [PATCH] [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 --- MobileGL/MG_Backend/MGPipe/PipeInputs.h | 138 +++- MobileGL/MG_Pipe/FieldOwnership.def | 240 +++++++ .../MG_Pipe/generated/PipeFieldOwnership.inc | 282 ++++++++ scripts/gen_pipe_field_ownership.py | 635 ++++++++++++++++++ 4 files changed, 1294 insertions(+), 1 deletion(-) create mode 100644 MobileGL/MG_Pipe/FieldOwnership.def create mode 100644 MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc create mode 100644 scripts/gen_pipe_field_ownership.py diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index d744b877..8a0d3eed 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -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 + // PipeInputs.cpp. The poison Fatal with the verb's name ("" 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 MGPipeFindInputField(const char* name); Optional 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(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 ""; + // 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 diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def new file mode 100644 index 00000000..cd90c885 --- /dev/null +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -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, "@"}. +// +// 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 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 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; 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") \ + 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 keyed by GL name") \ + X(GetTextureObject, BARRIER_PULLED, "P7", \ + "hands the backend a frontend SharedPtr 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 diff --git a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc new file mode 100644 index 00000000..3220bc85 --- /dev/null +++ b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc @@ -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(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(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"); diff --git a/scripts/gen_pipe_field_ownership.py b/scripts/gen_pipe_field_ownership.py new file mode 100644 index 00000000..73e2cb87 --- /dev/null +++ b/scripts/gen_pipe_field_ownership.py @@ -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 (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 @") + 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(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(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())