diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp index af001d12..ab14ef77 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.cpp @@ -55,24 +55,14 @@ namespace MobileGL::MG_Pipe { Uint64 g_residualPulls = 0; #if MOBILEGL_PIPE_VERIFY - // ---- P7 wave 3 (V1): negative control B's SERVER half ---------------------------- + // ---- MOBILEGL_PIPE_POISON_OMIT, server half ---- // - // MOBILEGL_PIPE_POISON_OMIT withholds the STAMP of one (verb, field) pair, and until a - // verify build first ran on the split arm it was only ever read by the client's filler - // (MG_Impl/Pipe/PipeFill.cpp's ParsePoisonOmissionKnob). On the split arm that is not - // where the stamp the backend reads comes from: the stamp below re-stamps every field - // the verb's class can answer out of applied records, so it restamped the very field - // the client had withheld and the control went GREEN - measured, both backends, the - // child's glGenerateMipmap returned and the process exited 0 with - // GenerateMipmap:GetActiveTextureUnit omitted. The knob means "this verb's stamp of this - // field was never written", and under a transport the stamp that answers the backend is - // this one, so this one honours it too. - // - // PARSED HERE, NOT SHARED WITH THE CLIENT'S PARSER, because this file is in the server - // library and MG_Impl is not. A malformed value is the client's Fatal{PipeVerifyBadKnob} - // (it parses first, at the first fill of the process); this side simply does not arm. - // VERIFY BUILDS ONLY: the knob belongs to the verify harness, and a push build's stamp - // stays the branch-free fill the P5d profile asked for. + // The knob withholds the stamp of one (verb, field) pair, a negative control for the + // poison check. Under a transport the backend reads the server's stamp, which re-stamps + // every field the verb's class can answer, so the server must withhold the pair too or + // the control cannot go red. Parsed here because this file is in the server library and + // MG_Impl is not; a malformed value is the client's Fatal{PipeVerifyBadKnob}, and this + // side simply does not arm. Verify builds only, so the shipped stamp stays branch-free. struct ServerPoisonOmission { Bool Parsed = false; String Value; @@ -128,25 +118,13 @@ namespace MobileGL::MG_Pipe { std::abort(); } - // P5e (gl), ID-117: THE ADMITTED MARKER, AND WHY IT IS ONLY THE TAG THAT CHANGED. - // - // `@` and the `[BARRIER-PULLED, …, retires in ]` tail are IDENTICAL - // to the Fatal above; the leading tag is `Admitted{` instead of `Fatal{`. Every filter - // written since P5c matches `Fatal{UnmigratedPipeInput`, so all of them keep meaning - // exactly "red" and none of them has to learn a new grammar to keep meaning it. A reader - // grepping for the pair still finds it, and the lane can ratchet on both sets with one - // regex per tag (ID-119's two-sided ratchet). - // - // THE DEDUPE IS NOT TIDINESS. An 852-draw Minecraft frame reaches an admitted readback - // row once per draw; without this, one frame writes 852 identical lines, the log file is - // the size of the run and the marker census cannot be read at all. So it is once per - // (field, verb) per process, which is the granularity the allowlist is written in. - // - // A PLAIN ARRAY, NOT AN ATOMIC, and that is the file's existing rule rather than a - // shortcut: CountBarrierPull is reachable only after the server stamped a verb boundary, - // which happens on the apply thread inside PipeApplier::ApplyOne, and g_residualPulls - // beside it is a plain Uint64 for the same reason. A racing writer here would at worst - // log a duplicate line, never lose one. + // The admitted marker: the same `@` and `[BARRIER-PULLED, ...]` tail as the + // strict Fatal above, with `Admitted{` in place of `Fatal{`, so filters on + // `Fatal{UnmigratedPipeInput` still mean only "red" and the lane can match each set with + // one regex. Logged once per (field, verb) per process, the granularity of the allowlist; + // otherwise an ~850-draw frame writes one line per draw. A plain array, not an atomic: + // CountBarrierPull runs only on the apply thread after a server verb stamp, like + // g_residualPulls, and a race would at worst log a duplicate line. void AdmittedBarrierPullOnce(MGPipeInputField field, MGPipeVerb verb, Bool escalated) { const SizeT fieldIndex = static_cast(field); const SizeT verbIndex = static_cast(verb); @@ -170,49 +148,13 @@ namespace MobileGL::MG_Pipe { kMGPipeFieldRetiringPhase[fieldIndex]); } - // One place decides what a BARRIER-PULLED read does, so the field accessors and the - // seven sticky forwards cannot drift apart on it. - // - // ---- P5e (ra), CONTRACT-P5E §3.3: THE DETECTOR IS UNCONDITIONAL UNDER AN UNBARRIERED - // RECORD, AND THAT IS WHAT MAKES THE STRICT LANE A GATE ------------------------------- - // - // The knob exists because under LOCKSTEP a pulled row is a real, ordered, fresh value: - // the client filled it and then parked, so "count it and carry on" is an honest - // measurement of remaining debt and MOBILEGL_IPC_STRICT_ERRORS is the operator asking - // for the debt to be loud instead. None of that survives run-ahead. With the client - // running ahead of this apply, the row was either never filled for this verb (§3.1 - // skips the fill) or is being overwritten by a verb two frames later - so the value is - // torn or stale BY CONSTRUCTION and there is nothing for a counter to count. A - // "count it" arm here would be a wrong picture with a number beside it. - // - // P5e (gl), ID-119: THE OLD PIN HERE - "rsp is 0 on unbarriered records" - WAS VACUOUS - // AND HAS BEEN WITHDRAWN. The unbarriered arm below is [[noreturn]] and runs BEFORE - // ++g_residualPulls, so an unbarriered pull can never reach the counter whatever this - // function is written to do; the sentence was true of every possible implementation and - // therefore checked nothing. What rsp actually counts is BARRIERED pulls, and under the - // strict knob it counts exactly the ADMITTED ones, because every other barriered pull - // aborts two lines down. That is the form CONTRACT-P5E §7 now states and the lane checks. - // - // ---- P5e (gl), ID-117: AN ADMITTED PULL IS LOUD, NOT FATAL --------------------------- - // - // Before this, strict aborted on EVERY barrier-pulled read, admitted or not - which made - // the CI's allowlist comparison unreachable code (the run's rc != 0 exits first) and made - // "hard green" impossible with the knob as written. So there is a third state, and the - // two existing ones are untouched: - // - // unbarriered -> Fatal, no knob. The value is torn by construction. - // barriered, unadmitted -> Fatal under strict. A debt no phase has taken. - // barriered, admitted -> ONE MGLOG_W per (field, verb), and the entry completes. - // - // "Admitted" has three disjuncts and the third is a runtime one (ID-128): the generated - // table answers the first two, and the record's own escalation flag answers the third. - // - // An admitted pull is a debt this phase deliberately leaves standing: the field's row is - // BARRIER_PULLED, the verb's op is statically barriered so the client really is parked - // behind the record, and the field is inside the verb's own may-read mask - so the value - // is real, ordered and fresh, exactly ruling 4's "barriered records keep P5C semantics". - // MGPipeBarrierPullAdmitted is generated from those three tables (ID-116); there is no - // list here to drift. + // Decides what a BARRIER-PULLED read does, for the field accessors and the sticky forwards: + // unbarriered -> Fatal, whatever the knob: the client is not parked behind the record, + // so the row is torn or stale and there is nothing to count. + // barriered -> counted (g_residualPulls); under MOBILEGL_IPC_STRICT_ERRORS it is Fatal + // unless admitted, and an admitted one logs one MGLOG_W per (field, verb). + // Admitted: the generated MGPipeBarrierPullAdmitted table says so, or the record was + // barriered by an escalation, which only its payload shows. void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) { // P5f (f1), P5F-WIRE-COMPLETENESS.md §4: UNDER THE DUAL-BLOCK REHEARSAL EVERY // BARRIER-PULLED READ IS A NAMED FATAL, unconditionally - ahead of the barriered diff --git a/MobileGL/MG_Backend/MGPipe/PipeInputs.h b/MobileGL/MG_Backend/MGPipe/PipeInputs.h index 87bbe176..fc716327 100644 --- a/MobileGL/MG_Backend/MGPipe/PipeInputs.h +++ b/MobileGL/MG_Backend/MGPipe/PipeInputs.h @@ -51,26 +51,12 @@ namespace MobileGL::MG_Pipe { Optional MGPipeFindInputField(const char* name); Optional MGPipeFindVerb(const char* name); - // ---- 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. + // The server-side read check (ARCHITECTURE.md §9.2.1). Inside a server-stamped verb + // (PipeInputs::ServerStampedVerb) a stale read is judged by the field's ownership class: + // RECORD_SUPPLIED or APPLIER_DERIVED is a role violation, FATAL aborts, and BARRIER_PULLED + // (no production row) inside the verb's class counts as a residual pull unless + // CountBarrierPull (PipeInputs.cpp) aborts it. Outside such a verb every stale read is the + // plain poison Fatal, because there the client stamped every field itself. 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 @@ -194,25 +180,14 @@ namespace MobileGL::MG_Pipe { static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount, "the forwarded (F-class) fields and the sticky fields of PipeFields.def are the same seven rows"); - // The block the backends read instead of GLContext (ARCHITECTURE.md 9.2 phase A, P1 brief - // D4). One struct, three storage classes, and every accessor keeps the NAME, PARAMETERS - // and RETURN TYPE of its GLContext counterpart (MG_State/GLState/Core.h) so the strangler - // sed is type-neutral: - // - // V (value) copied out of GLContext at fill time by calling the same accessor; - // no derivation logic is re-implemented here, which is what keeps the - // copy semantically identical by construction. - // O (object reference) a SharedPtr copy, or a raw pointer to the live GLContext-owned - // slot/array for the accessors that return a non-const reference into - // the context. Identity is what phase C turns into a handle. - // F (forwarded) argument-keyed lookups and reverse-channel calls, defined out of - // line in MG_Impl/Pipe/PipeFill.cpp (the client side, where the live - // context may be spelled). Sticky: stamped once by the first fill that - // sees a live context. - // - // Every non-forwarded accessor is MGP_INPUT_CHECK (poison) -> MGP_INPUT_VERIFY_READ - // (compare-at-read) -> the storage. Both macros expand to nothing when their switch is - // off, so a plain MOBILEGL_PIPE_PUSH build's accessor is a load. + // The block the backends read instead of GLContext (ARCHITECTURE.md §9.2). Each accessor + // keeps the name, parameters and return type of its GLContext counterpart (Core.h). + // V (value) copied from GLContext at fill time through the same accessor, so no + // derivation is re-implemented here. + // O (object) a SharedPtr copy, or a raw pointer into a GLContext-owned slot or array. + // F (forward) argument-keyed lookups and reverse-channel calls, defined in PipeFill.cpp + // (MG_Impl/Pipe); sticky, stamped by the first fill that sees a live context. + // Other accessors run MGP_INPUT_CHECK then MGP_INPUT_VERIFY_READ; both compile away when off. struct PipeInputs { using GLContext = MG_State::GLState::GLContext; using BufferObject = MG_State::GLState::BufferObject; @@ -252,23 +227,12 @@ namespace MobileGL::MG_Pipe { #if MOBILEGL_PIPE_POISON const MGPipeFilledState& FilledState() const { return m_filled; } #endif - // TRUE between the server's verb-boundary stamp and whoever clears it. It is the - // arming condition of the whole split read path: only inside a server-stamped verb is - // a BARRIER-PULLED read counted rather than Fatal, and only there is a sticky forward - // a residual pull rather than an ordinary monolith call. A split build running - // monolith transport never sets it, which is why build-split's unit and integration - // cases behave exactly as a verify build's do. - // - // CLEARING IT IS THE APPLIER'S JOB AND NOT THE CLIENT'S, even though the client also - // does it. MGPipeValidateForVerb and MGPipeLeaveVerb both call - // MGPipeServerClearVerbBoundary, which is sufficient for inproc, where both roles share - // one process and one gPipeInputs - and misleading for P6, where MG_Impl is not in the - // server at all. There this flag would latch TRUE for the life of the server after the - // first stamp, every later read anywhere would be judged against the last verb's mask, - // and MGPipeStickyForwardPull would stop being a no-op outside a verb - so - // InvalidateCompileEnv reached from a later context's backend initialisation, the exact - // case the sticky exemption was written for, would be counted and, under strict, would - // abort. So: PipeApplier clears on leaving the applier. Not optional. + // True between the server's verb-boundary stamp and the applier clearing it. Only inside + // such a verb is a BARRIER_PULLED read counted rather than Fatal, and only there is a + // sticky forward a residual pull. PipeApplier::LeaveApplier must clear it after every + // record: a server process has no client half to do it, so the flag would stay set and + // later reads (InvalidateCompileEnv during another context's backend initialisation) + // would be judged against the last verb's mask. Bool ServerStampedVerb() const { return m_serverStampedVerb; } // ---- V: values ---- @@ -799,63 +763,22 @@ namespace MobileGL::MG_Pipe { TextureUnit* m_textureUnitBase = nullptr; }; - // The single global the backends read through gPipeInputs (ARCHITECTURE.md 9.2). An inline - // variable: no .cpp is needed for the definition. - // - // P14 S2: IT IS THE PROCESS-WIDE BLOCK, and `gPipeInputs` below is the name every reader - // spells - now one reference per THREAD, resolving to a served session's own block when the - // calling thread belongs to one. See the block after it. - // - // LEAK-AT-EXIT STORAGE, and it is the same rule Init.cpp and GlobalObjects.cpp state for - // pGLContext and pActiveBackendObject: "a process that exits without eglTerminate simply - // leaks the global singletons to the OS instead of running destructors during static - // teardown". This block breaks that rule if it is a value, because its O-class members - // are SharedPtrs to FRONTEND objects: a VertexArrayObject that the application deleted - // while it was bound has its last reference here, and destroying this block from - // __run_exit_handlers therefore runs ~VertexArrayObject -> ~BufferObject at exit. Those - // destructors are not exit-safe and cannot be made so - they reach the client's slot - // allocator, the resource tracker, the vertex-input emitter, the applier AND, through - // MGPipeApplyResourceDestroy, the backend's own twin tables, deferred-release queue, - // buffer pool and driver entry points, every one of which is either already destroyed or - // about to be. So the reference is never dropped: nothing here can start such a chain. - // A live context releases these SharedPtrs the ordinary way, at the fill point. - // (P3a; the exit-time heap corruption this closes is p3a-results/exit-order-v1.md.) + // The process-wide block, used by every thread outside a served session (see gPipeInputs + // below). Deliberately leaked at exit, like pGLContext and pActiveBackendObject: its O-class + // members can hold the last reference to frontend objects (a deleted but still bound VAO), + // and destroying them from exit handlers would reach the slot allocator, emitters, applier + // and backend twin tables after they are gone. A live context releases these references at + // the fill point. inline PipeInputs& gPipeInputsDefault = *new PipeInputs(); // ============================================================================ - // P14 S2: WHOSE BLOCK IS `gPipeInputs`? + // Which block `gPipeInputs` names // ============================================================================ - // - // ONE PROCESS, SEVERAL SERVED SESSIONS. The applier writes this block and the backend reads it - // through gPipeInputs, both on the SAME apply thread - the block IS "the state the record in hand - // carries". Two sessions applying concurrently on two apply threads through one block would - // read each other's render state, which is the cross-talk a multi-session server must not - // have. Nothing about the block's contents changed; what changed is that the name resolves to - // the SESSION'S block on a thread that belongs to one. - // - // WHY A THREAD-LOCAL SLOT, AND NOT SOMETHING CLEVERER. gPipeInputs is spelled at ~380 - // sites in MG_Backend/MG_Impl (they read `MG_Pipe::gPipeInputs->` directly since P13 retired the - // pull/push switch macro); a name whose TARGET varies with the calling thread is the only - // shape that keeps all of them compiling and all of them correct. - // - // P15: A POINTER SLOT, NOT A REFERENCE. It used to be a `thread_local PipeInputs&` bound by a - // dynamic initializer, and a thread_local with a dynamic initializer is reached through its - // TLS init function, which tests two more thread_local guards on every read: three TLS - // address calls per access in a dlopen'ed library, at every API level. The slot below is - // constant-initialized and trivially destructible, so an access is one TLS address - // computation, a load and a null test; the binding itself is unchanged - once per thread, at - // its first use, through the same resolver. - // - // THE RESOLVER IS A HOOK because this header may not know about sessions (MG_Remote/Server - // owns them and includes THIS header, not the other way round). It is installed once by the - // server's session registry and never replaced; null - the client process, the pull build, a - // unit case - makes every thread resolve to gPipeInputsDefault, which is the old behaviour - // byte for byte. - // - // BINDING IS PER THREAD AND HAPPENS AT FIRST USE, so a thread must be inside its session's - // scope before it touches the block. Both of a session's threads are: the control thread opens - // its scope before it reads a byte of the Hello, and the apply thread opens it as its first - // act (ServerLoop::ApplyThreadMain). + // Each served session applies on its own thread, so gPipeInputs resolves per thread through + // g_pipeInputsThreadResolver, installed once by the server's session registry (null in a + // client: every thread gets gPipeInputsDefault). The slot is a constant-initialized pointer + // (no TLS init call per access) bound at first use, so a thread enters its session's scope + // first: the control thread before the Hello, the apply thread in ServerLoop::ApplyThreadMain. using PipeInputsThreadResolver = PipeInputs* (*)(); inline PipeInputsThreadResolver g_pipeInputsThreadResolver = nullptr; @@ -891,26 +814,13 @@ namespace MobileGL::MG_Pipe { #if MOBILEGL_BUILD_DISAGGREGATED // ============================================================================ - // P5f (f1), P5F-WIRE-COMPLETENESS.md §4: THE DUAL-BLOCK REHEARSAL + // The client block (MOBILEGL_IPC_ROLE_SPLIT_STATE) // ============================================================================ - // - // One gPipeInputs served both roles because the verb barrier (R-1) made "the client filled - // it" and "the server is reading it" mutually exclusive in TIME on the same object. The - // rehearsal removes the same-object half of that sentence: under - // MOBILEGL_IPC_ROLE_SPLIT_STATE=1 the client's residual fill writes the CLIENT block below - // and the backend/applier keep reading gPipeInputs, which is the SERVER block. Every path - // that worked only because the two were one object - a BARRIER-PULLED field, a sticky - // forward's read of the fill side's stamps - then has no value to read, and the read - // becomes a NAMED Fatal{UnmigratedPipeInput, "@"} (CountBarrierPull's - // dual-block arm, PipeInputs.cpp) instead of a silent cross-role answer. - // - // THE READ SIDE'S SPELLING DOES NOT MOVE. gPipeInputs remains the server-role block, so the - // 379 gPipeInputs sites and PipeApply.cpp's applier writes are untouched; the only new spelling - // is on the fill side (MG_Impl/Pipe/PipeFill.cpp), which asks MGPipeClientInputs(). Under - // monolith transport - every unit and integration-gpu lane of a split build - the selection - // folds back to the single shared block and behaviour is byte-for-byte the old one. - // - // Same leak-at-exit storage as gPipeInputs above, for the same reason. + // With the knob on and a real transport, the client's residual fill writes this block + // (MGPipeClientInputs) while the applier and backend read gPipeInputs, the server block; a + // read that relied on one shared object then aborts by name (CountBarrierPull) instead of + // returning a cross-role value. Otherwise both roles use the one shared block. Leaked at + // exit like gPipeInputsDefault. inline PipeInputs& gPipeInputsClientBlock = *new PipeInputs(); // PipeInputs.cpp. Whether the REHEARSAL is armed: the knob AND a real transport (and not @@ -1023,52 +933,13 @@ namespace MobileGL::MG_Pipe { static_assert(sizeof(PipeInputs) < 20 * 1024, "PipeInputs outgrew its budget"); // ============================================================================ - // P5: the server-side verb stamp, and the counter that sizes what it leaves behind + // The server-side verb stamp // ============================================================================ - // - // 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. - // - // TWO THINGS THE CALLER INHERITS AND SHOULD NOT REDISCOVER: - // - // (a) The records BETWEEN two boundaries apply under the earlier boundary's stamp - its - // serial, its class mask and its verb NAME. That is correct today because every - // MGPipeApply* entry point touches gPipeInputs through MGPipeApplyAccess, which - // carries no MGP_INPUT_CHECK; the day one of them calls back into the backend, its - // reads will be judged against a verb they do not belong to. - // (b) The verb a draw record stamps is MGPipeVerb::DrawArrays for ALL TWENTY draw verbs. - // The class mask is right (FillPoints.def puts all twenty in kDraw) and the NAME in a - // Fatal is not: a glDrawElements that aborts will say "@DrawArrays". draw_vbo carries - // no verb id, so fixing it means either a field on the record or a second argument - // here; it is cosmetic for the verdict and misleading for the reader, and it belongs - // with whatever phase widens draw_vbo to the multi-draw family (P8). + // Called by PipeApplier::StampVerbBoundary for each op MGPipeVerbForWireOp maps to a verb. + // Bumps CurrentVerbSerial, sets the verb, stamps the RECORD_SUPPLIED and APPLIER_DERIVED + // fields in the verb's class and zeroes every other stamp; without the zeroing every read + // would look fresh, since the client's fill stamped all fields. Records between boundaries + // apply under the earlier stamp; all draws stamp as DrawArrays (ARCHITECTURE.md §9.2.1). void MGPipeServerStampVerbBoundary(MGPipeVerb verb); // MANDATORY for the applier when it leaves the verb. The client's own MGPipeValidateForVerb // and MGPipeLeaveVerb call it too, which is enough for inproc and NOT enough for a spawned diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.h b/MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.h index 8bb8c530..b25f4657 100644 --- a/MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.h +++ b/MobileGL/MG_IntegrationTest/Harness/PipeSlotPeek.h @@ -56,7 +56,7 @@ namespace MGITest { SamplerCso, SamplerViewCso, // ShaderCso covers BOTH the ordinary program slots and the program-pipeline COMPOSITES - // minted out of the reserved high band (MGPipeHandles.h:86-107, D-H7). One kind, because + // minted out of the reserved high band (kMGPipeShaderCsoCompositeSlotBase). One kind, because // that is what the allocator has: the band is a second dense table inside the same kind // and LiveCount counts both. // diff --git a/MobileGL/MG_Pipe/Coverage.def b/MobileGL/MG_Pipe/Coverage.def index c5a60258..fe5fcfba 100644 --- a/MobileGL/MG_Pipe/Coverage.def +++ b/MobileGL/MG_Pipe/Coverage.def @@ -6,23 +6,13 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The hand-maintained half of G6 (plan B section 4.7, gate 10.3-5): which MGPipe call -// answers each backend read point in scripts/data/backend_read_inventory.md (477 rows, 57 -// files, generated from the backends by MobileGL-CS's extract_backend_read_inventory.py). -// -// gen_pipe.py joins the inventory's `member` column against MGP_COVERAGE_ACCESSOR_LIST and -// its `delta` column against MGP_COVERAGE_DELTA_LIST, then writes generated/PipeCoverage.inc -// with the per-accessor table and prints the coverage summary. Rows matching neither are -// UNMAPPED: allowed in P0 and merely counted, ZERO from P5 onward, when the gate becomes -// "regenerate and git diff --exit-code with 0 UNMAPPED". -// -// Three pseudo-calls stand for read points that do NOT become a forward call: -// kClientResolved - the frontend answers it itself; the server is never asked -// (section 4.4.6: "the server answers nothing the client can answer"). -// kReverseChannel - it becomes one of the ten MGPipeCallbacks (section 7.1). -// kStructuralHandle - the row is a SIGNATURE carrying SharedPtr, which -// becomes an MGPipeHandle parameter; there is no single call to name. -// +// Coverage table (ARCHITECTURE.md §3.1): which MGPipe call answers each backend read point in +// scripts/data/backend_read_inventory.md. gen_pipe.py joins the inventory's `member` column +// with MGP_COVERAGE_ACCESSOR_LIST and its `delta` column with MGP_COVERAGE_DELTA_LIST into +// generated/PipeCoverage.inc; a row matching neither is UNMAPPED, and the gate requires zero. +// Pseudo-calls: kClientResolved (the frontend answers; the server is never asked), +// kReverseChannel (an MGPipeCallbacks entry, §8.1), kStructuralHandle (a SharedPtr +// parameter that becomes an MGPipeHandle). // clang-format off // X(Accessor, PipeCall) @@ -34,39 +24,14 @@ /* dead: no backend reads it since D21; kept for inventory row 594 */ \ X(GetBoundTransformFeedbackName, SetStreamOutputTargets) \ X(GetBoundVertexArray, BindVertexElements) \ - /* Polymorphic over BufferTarget, and P3a SPLITS it - not by re-vendoring the */ \ - /* inventory (the extractor lives in MobileGL-CS and still does not carry the */ \ - /* target argument), but by supplying the target from the EMISSION SITE, which */ \ - /* knows it exactly. The split, target by target, in BufferTarget's OWN */ \ - /* spelling (BufferObject.h:15-33) so the list cross-checks mechanically: */ \ - /* Vertex, and the per-attribute buffer of a VAO -> set_vertex_buffers */ \ - /* Index -> set_index_buffer */ \ - /* DrawIndirect, Parameter -> set_indirect_buffers */ \ - /* Uniform, ShaderStorage, AtomicCounter, */ \ - /* TransformFeedback -> set_shader_buffers */ \ - /* CopyRead, CopyWrite, PixelPack, PixelUnpack, */ \ - /* Texture -> still pulled: the */ \ - /* transfer and pixel-store targets have no call of their own yet; */ \ - /* DispatchIndirect -> still pulled, and */ \ - /* it is the one a reader most plausibly assumes set_indirect_buffers */ \ - /* covers: BOTH backends read it (SyncBoundBuffer at every */ \ - /* glDispatchComputeIndirect) and NO call carries it - MGPIndirectBuffers */ \ - /* is the DrawIndirect + Parameter pair only - so the field stays the */ \ - /* fill loop's for this target; */ \ - /* Query -> still pulled: no */ \ - /* backend reads it at all and no call names it. */ \ - /* That is 15 of 15; a target missing from this list is a target a later */ \ - /* phase would retire the pull for on the strength of a split that never */ \ - /* covered it. */ \ - /* 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 (m_bufferBindingSlot[kBufferTargetCount]) that a second */ \ - /* row of the same name could only duplicate. So the split lives here and in */ \ - /* the emitters, and the row keeps naming set_indirect_buffers for the plan's */ \ - /* explicit replacement of the DrawIndirect/Parameter pair. It is deliberately */ \ - /* NOT in the EMITTED list below: seven targets above are still pulled, and a */ \ - /* row there says "the whole field is supplied", which for this field would be */ \ - /* the same half-truth GetPixelStoreParameters is kept out for. */ \ + /* Polymorphic over BufferTarget; the emitters supply the target, so the calls */ \ + /* split it: Vertex (and a VAO's attribute buffers) -> set_vertex_buffers, */ \ + /* Index -> set_index_buffer, DrawIndirect and Parameter -> set_indirect_buffers, */ \ + /* Uniform, ShaderStorage, AtomicCounter, TransformFeedback -> set_shader_buffers. */ \ + /* The other seven targets (copy, pixel, Texture, DispatchIndirect, Query) have no */ \ + /* set_* carrier; on the server, verbs carry the handles they need. The field is */ \ + /* one array, so it stays one row, and it has no emitted row: no call supplies all */ \ + /* of it (FieldOwnership.def). */ \ X(GetBufferBindingSlot, SetIndirectBuffers) \ X(GetBufferBindingPoint, SetShaderBuffers) \ X(GetBufferBindingPointCount, SetShaderBuffers) \ @@ -157,102 +122,14 @@ X(handle-ify (wire handle), kStructuralHandle) \ X(Buffer ops delta, ResourceRespecify) -// X(Accessor, PipeCall) - the EMITTED list (P2 brief D5): which P2 call now SUPPLIES this -// PipeInputs field, so the per-verb residual fill loop no longer has to pull it out of -// GLContext. gen_pipe.py turns it into kMGPipeFieldEmittedBy[] (generated/PipeFilled.inc); -// a field with no row here keeps going through the fill loop, which is what makes the -// MOBILEGL_PIPE_PUSH bitmask a true per-subsystem A/B rather than an all-or-nothing switch. -// -// Every name must be an accessor in MGP_COVERAGE_ACCESSOR_LIST and every call must be a -// real call in PipeCalls.def; gen_pipe.py refuses anything else. -// -// The one row whose call differs from the accessor list's is GetPrimitiveRestartIndex: -// coverage maps it onto draw_vbo because that is where a backend reads it, but the VALUE -// travels in dynamic chunk D6, so set_dynamic_state is what supplies it. -// -// GetPixelStoreParameters is DELIBERATELY ABSENT, and the reason is the shape of the field -// rather than of the call. The field is PipeInputs::m_pixelStore[2] - pack AND unpack - and -// set_pixel_pack_state carries the PACK half only, deliberately and permanently (D10, -// ARCHITECTURE.md 4.6 D5: nothing on the far side of the boundary reads unpack state). A row -// here says "this field is supplied, the fill loop may skip it", which would be a half-truth: -// the moment the render-state bitmask has its bit set, the unpack half would be written by -// nothing while its poison stamp said it was published, so neither the poison nor the verify -// comparator could see it. Until the field is split, the whole of it keeps going through the -// fill loop and the pack half is simply written twice. -// -// P3a ADDS ONE ROW, GetBoundVertexArray -> BindVertexElements, and it is the vertex-input -// family's only candidate: GetBufferBindingSlot is polymorphic over a target set P3a covers -// only part of (see its comment above) and GetCurrentVertexAttribute has been here since P2. -// The row is INERT until the vertex-input subsystem is wired - MG_Impl/Pipe/PipeFill.cpp's -// kMGPipeWiredSubsystems does not carry that bit at the contract commit, because the emitters -// beside it are still stubs - which is exactly the guard that lets a row land before the call -// that carries it exists. -// -// A NOTE FOR THE COMMIT THAT WIRES IT, because it is not visible from this file: the field is -// a shared pointer to the frontend VAO, and the vertex-input calls supply the CONFIGURATION -// (the applier's MGPipeVertexElementsRecord), not the object. So the row is shape-only in the -// same sense GetCurrentVertexAttribute's is, and it stays so until the backend's twin -// resolution reads the applier's BoundVertexElements instead of the object - at which point -// PipeFill.cpp's EmittedCallSuppliesTheWholeField arm is where that is decided, deliberately -// rather than silently by this row's presence. -// -// P4a ADDS SIX ROWS, and the same note applies to every one of them: each is SHAPE-ONLY, each -// lands in PipeFill.cpp's EmittedCallSuppliesTheWholeField FALSE arm, and the decision is -// taken THERE rather than inherited from a row's presence here. The rows and their calls: -// -// GetFramebufferBindingSlot -> SetFramebufferState GetProgramForDraw -> SetDrawProgram -// GetImageTextureBinding -> SetShaderImages GetProgramForDispatch -> SetDispatchProgram -// GetTextureUnitObject -> SetSamplerViews GetMaxTouchedTextureUnit -> SetSamplerViews -// -// Five of the six are the pointer-storage case GetBoundVertexArray already documents: the -// field is a BindingSlot, an ImageTextureBinding, a TextureUnit or a -// SharedPtr - frontend heap references - and the calls carry eight-byte -// {slot, gen} handles and resolved descriptors. The applier has no way to produce a pointer -// and P4a deliberately does not give it one; skipping the pull would leave every one of those -// mirrors null on every draw of every push build. What retires those pulls is not a better -// applier, it is the phase where the backend stops reading a frontend object at all. -// -// THE SIXTH left that arm at P5c rv: GetMaxTouchedTextureUnit is a plain Int whose problem was -// never shape, it was the CARRIER - set_sampler_views' Count is the set's window, and the set -// is suppressed on an unchanged content hash while the high-water mark still moves on a -// redundant re-bind. set_context_values (CONTRACT-P5C.md §5.3) carries the mark as a VALUE of -// its own, whole-record suppressed, so the mark rides an unsuppressed field and the row is -// RECORD_SUPPLIED outright. -// -// P5c rv ADDS EIGHT ROWS, and they are the whole of the value-class debt that was left -// (CONTRACT-P5C.md §5.3): seven residual values no set_* call carried - the two texture-unit -// counters, the 15-target touched-count array and the five transform-feedback values - plus -// GetMaxTouchedTextureUnit re-homed from SetSamplerViews. All eight name SetContextValues, one -// POD emitted at validate when any covered value moved, hash-suppressed whole. The OBJECT rows -// (GetTextureUnitObject and its four P4a siblings above) stay BARRIER_PULLED with their -// retiring phases; the three texture shutters (below) take no row here either - they carry no -// wire value at all, the accessors answer the applier's own Serial. -// -// GetTextureContextId / GetTextureBindGeneration / GetSamplingResolutionGeneration stay out -// DELIBERATELY, and the reason is ownership rather than coverage: they are frontend SHUTTERS, -// and what replaces them server-side is the applier's own Serial - a different value with a -// different owner. A row here would claim the sets supply the generations, and the fill loop -// would skip two counters no record carries. -// And GetTextureObject / GetProgramObject are STICKY (see MGP_COVERAGE_STICKY_LIST): they are -// keyed by GL name, they are object lookups rather than verb state, and a forwarded field has -// no storage for an emitted call to supply. -// -// P5e ADDS ONE ROW, GetBufferBindingPoint -> SetShaderBuffers (docs/archive/contracts/CONTRACT-P5E.md §5.6), -// and it is SHAPE-ONLY like P3a's and P4a's six: the field's storage is four raw bases into the -// frontend's Array, 4>, and set_shader_buffers carries RESOLVED -// {handle, offset, size} ranges per class. The applier has no way to produce a pointer and P5e -// deliberately does not give it one, so the row lands in PipeFill.cpp's -// EmittedCallSuppliesTheWholeField FALSE arm and the decision is taken THERE. What retires the -// pull is not a better applier, it is the four Espryt consumers reading the applier's -// BoundShaderBuffers instead of the frontend's binding points - at which point the -// FieldOwnership.def row flips to RECORD_SUPPLIED. -// -// THE ROW IS INERT AT THIS COMMIT for GetBoundVertexArray's reason one phase on: the emission -// and the fill skip both test kMGPipeWiredSubsystems, and the buffer-binding family's wired -// constant is 0 until the package that gives MG_Impl/Pipe/ShaderBufferEmit.h its body sets it. -// It lands here, with its two PipeFill.cpp switch arms, because this file and that one belong -// to the contract package for the whole phase - a family package that had to add an enumerator -// AND the arm that dispatches on it would be editing the one file the merge trap lives in. +// X(Accessor, PipeCall) - the emitted list: the call that supplies each PipeInputs field, so +// the residual fill loop may skip it. gen_pipe.py turns it into kMGPipeFieldEmittedBy[] +// (generated/PipeFilled.inc) and refuses an accessor or call not listed above or in +// PipeCalls.def. A row names the call that carries the value, which may differ from the read +// point above (GetPrimitiveRestartIndex rides dynamic chunk D6). A row alone does not skip the +// fill: PipeFill.cpp's EmittedCallSuppliesTheWholeField is false where the call carries handles +// but the field holds a frontend pointer. Deliberately absent: GetPixelStoreParameters (pack +// half only), the texture shutters (answered from applier serials) and the sticky lookups. #define MGP_COVERAGE_EMITTED_LIST(X) \ X(GetActiveTextureUnit, SetContextValues) \ X(GetBlendColor, SetDynamicState) \ diff --git a/MobileGL/MG_Pipe/DirtySurface.def b/MobileGL/MG_Pipe/DirtySurface.def index 3fe47bce..bf7a8197 100644 --- a/MobileGL/MG_Pipe/DirtySurface.def +++ b/MobileGL/MG_Pipe/DirtySurface.def @@ -6,169 +6,13 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The dirty-surface mapping (ARCHITECTURE.md 5.2 corollary 4, P2 brief D16). -// -// MGPipe replaces "the backend rediscovers what changed" with "the frontend says what -// changed", which only works if EVERY frontend mutation a backend can observe has an answer -// to "what publishes this". The failure mode is silent and one-directional: a mutation that -// forgets to publish renders stale, and no purity gate can see it. -// -// So the surface is enumerated MECHANICALLY. scripts/gen_pipe_dirty_surface.py scans -// MG_Impl/GLImpl AND MG_State/GLState for every pGLContext-> mutator call and every -// MGP_NOTE_MUTATION site, and, with --check, fails if a scanned mutator has no row here or a -// row here names a mutator the scan no longer finds. Both directions, so a deleted mutator -// cannot leave a stale row behind either. -// -// THE SECOND ROOT AND THE SECOND MECHANISM ARE P3a's, and they were not cosmetic: reading -// MG_State/GLState found BumpSamplingResolutionGeneration, a mutator with no row at all -// whose every caller lives in that directory, and reading MGP_NOTE_MUTATION brought -// NoteUnitTouched - which no `pGLContext->` pattern can see, because every texture and -// sampler bind routes through it rather than calling it through the context - inside the -// gate. A notice site carries a FIELD name, so the mutator a row names is the ENCLOSING -// function. -// -// ANSWERS. A row lists EVERY publisher that fires on EVERY path through that mutator, -// and only those; several are joined with '|'. A publisher that fires on some paths but -// not all must not appear, because a shutter built from this file would then UNDER-fire, -// and ARCHITECTURE.md 13.2 names under-firing as the dangerous direction. The one row that -// carries a bit which fires on only some paths says so in its answer - kPulledPartialShutter -// joined with that bit - because the alternative, dropping the bit, tells a reader of this -// file that a bit P2 already emits a call for has no shutter at all. -// -// "EVERY PATH" MEANS EVERY PATH THAT MUTATES. A setter that returns early because the value -// did not change publishes nothing and needs to publish nothing - there is no mutation to -// carry - so a redundant-write guard (SetColorMask's `if (changed) BumpVersions();`, the -// BitwiseEqual guards on the patch levels) does not make its publisher conditional in the -// sense this rule cares about. A publisher reached on only SOME of the paths that DO mutate -// - SetCapability's ClipDistance arms, SetStencilFunc's reference-only call - is the thing -// that must not be named. -// -// EVERY BIT ANSWER IN THIS FILE IS DERIVED AND CHECKED, in two families and one -// direction. The RenderState family (45 rows) is checked both ways against RenderState.cpp, -// below. Every other NEW_* answer is checked against the shutter Tracker.h builds for that -// bit: gen_pipe_dirty_surface.py resolves what the shutter READS and what each mutator -// transitively WRITES (through MGP_NOTE_AGGREGATE too, whose hop it reads out of -// MGPipeNoteAggregate's own switch, and through the function-like macros of MG_State, which -// it EXPANDS - sixteen of RenderState.cpp's writes exist only after the preprocessor has -// pasted them together) to the SAME two-level token, MEM: plus FIELD:., -// and fails a row that names a bit whose shutter its mutator moves on no path at all. That -// half is one-directional on purpose - "it does write something the shutter reads" cannot -// prove it does so on EVERY path - so it catches under-firing and not over-claiming. -// -// AN ABSENCE CLAIM IS ONLY WORTH THE READING BEHIND IT, and this gate learned that twice: -// its write analysis first recorded a write through a member's field as the field alone -// and never the member, so --check printed, as a fact about RenderState.cpp, that -// SetPixelStoreParam "writes nothing NEW_PIXEL_PACK's shutter reads" about a setter whose -// whole body is sixteen writes to exactly that member; then, once it read the member, it -// still could not see a write through a REFERENCE (SetBlendEquation's `for (auto& -// blendState : m_parameters.BlendStates)`) and said the same false thing about seven more -// setters - while its reader side resolved `render.PatchVertices` to the WHOLE of -// m_parameters, so every setter that touched any byte of it "supported" NEW_PATCH_STATE and -// a row saying glClearColor publishes the patch state was green. So now: a reference or -// pointer bound to a member-rooted lvalue is followed, and its writes are credited to the -// member and the field it was bound to; a write whose root the analysis cannot place (a -// reference parameter, a call result, a member without the m_ prefix, a token it could not -// expand) TAINTS the function, and every answer that depends on a tainted function is -// UNDECIDED - printed with its reason, never a verdict; a writer supports a bit only when -// the two sides share a member AND, both resolved to fields, their field sets intersect (a -// whole-member write or read is every field); a member in common with no field information -// on one side is COARSE, reported and never counted. --check counts only the supported -// answers as derived, prints the COARSE and UNDECIDED tallies, and FAILS on an UNDECIDED -// row unless MGP_DIRTY_SURFACE_UNDECIDED_LIST at the bottom of this file marks it - a -// mark that outlives its reason is a red gate too. What it still cannot claim: a shutter -// member written outside MG_State/GLState + MG_Impl/Pipe is undecided in the absence -// direction, a call is resolved by NAME to every body of that name, and a FIELD token is -// not scoped to a type - all three only widen what a mutator is credited with, and the -// second is also how a taint spreads. -// -// The prose answers (kImmediate, kExplicitDestroy, kUnpublishedDestroy, kNoBackendRead, -// kPulledEveryVerb, kPulledPartialShutter, kReverseChannel) are statements no derivation -// checks - except the bits a kPulledPartialShutter row names, which are checked like any -// other bit answer. --check prints how many rows carry a prose answer, so "all mapped" can -// never be read as "all verified". -// -// For the RenderState family that answer is not a matter of taste and it is CHECKED -// rather than asserted: scripts/gen_pipe_dirty_surface.py reads RenderState.cpp and -// derives, per setter, which of NEW_RENDER_STATE / NEW_PIPELINE_STATE moves on every -// path - BumpVersions() moves both, a bare ++m_version moves only NEW_RENDER_STATE, and -// a setter with both kinds of path therefore always-fires only NEW_RENDER_STATE - and -// --check fails when a row disagrees, in either direction. That check exists because -// this file got exactly two rows wrong: SetCapability, whose ClipDistance0..7 arms move -// only m_version, and SetStencilFunc, whose pipeline bump is conditional on Func moving. -// Both named NEW_PIPELINE_STATE, which does not fire for glEnable(GL_CLIP_DISTANCE0) or -// for a reference-only glStencilFunc. -// -// NEW_* a MGPipeDirty bit (MG_Impl/Pipe/Tracker.h). The tracker's shutter for -// that bit moves when this mutator runs, so the next verb publishes it. -// kImmediate the mutating function also reaches the backend in the same body, so the -// mutation is published inline and needs no shutter at all. -// kReverseChannel not state: a write INTO the frontend from the backend's side. -// kNoBackendRead no backend read point observes this state at all. -// kExplicitDestroy published by the delete_* / resource_destroy call the Track H slice -// emits when the object's last reference drops - an object's DEATH, -// which no generation shutters because there is no longer an object -// to carry one. Only for a kind that HAS an identity on the wire to -// destroy: the resources and CSOs of PipeCalls.def, which is what P2 -// brief D13 scopes Espryt 0b's explicit destroy to. -// kUnpublishedDestroy -// the same event for a kind NOTHING publishes: a program, a program -// pipeline and a shader have no per-object handle on the wire at all -// in P2 - resource_destroy and the delete_* family name resources and -// CSOs - so their DirectGLES twins are still reclaimed by the -// backend's own registry teardown and no frontend call says they -// died. Recorded as a hole rather than dressed up as a mechanism that -// exists; naming kExplicitDestroy here would be the same defect the -// RenderState derivation above exists to stop, one class down in -// stakes. (D13's prose says 'six kinds' while the Core.cpp ranges it -// cites also cover MarkProgram/MarkShaderForDeletion; the tree -// decides, and the tree has no wire object for those three.) -// kPulledEveryVerb no shutter exists at all - no MGPipeDirty bit moves on any path through -// this mutator - and none is needed yet: the PipeInputs field it writes is -// in its verb class's may-read mask, so the residual fill copies it at -// EVERY verb of that class. A shutter here is a P3/P4 optimisation, not a -// correctness gap. -// kPulledPartialShutter -// the same pull, but a bit DOES move - on some of the paths that mutate, -// not all of them - so this row must never be read as "no shutter exists". -// The bits that move are named after the '|', which is the one place this -// file joins a prose answer with a bit, and the reason is exactly that a -// P3a shutter builder has to be able to tell "no bit covers this" from "a -// bit covers half of it". The named bits are checked the same way every -// other bit answer is - a dead one is a red gate - but they are NOT a -// licence to narrow: what holds on every mutating path is the pull. -// Which rows need this answer is a human judgement and stays one: the -// derivation's "it does move that shutter" direction over-approximates -// (a call name resolves to every body of that name, a write inside an -// `if` counts), so it can refute a named bit but cannot find the rows -// that should have named one. -// -// KNOWN BLIND SPOTS OF THE SCANNER, recorded here rather than left implicit -// (gen_pipe_dirty_surface.py's own notes plus its scan roots): -// 1. it matches braced function bodies textually, so a mutator inside a LAMBDA is -// attributed to the enclosing function; -// 2. a mutation published through a HELPER the entry point calls reads as deferred here. -// 3. CLOSED AT P3a. The scan root was MG_Impl/GLImpl only, so the four MGP_NOTE_MUTATION -// sites in MG_State/GLState/TextureState/TextureState.h were outside it entirely. The -// root is now both directories and the notice is a recognised publish mechanism; what -// that found is the two rows marked "P3a" below. -// The gate is therefore a COMPLETENESS gate over what the scanner does see. The semantic -// proof stays the MOBILEGL_PIPE_VERIFY lane, which is blind to none of them. -// -// THE MUTATOR PREFIX SET WIDENS AT P4a, and what it does NOT gain is the more interesting -// half. `pGLContext->` + Add|Set|Mark|Bump|Allocate|Truncate|Record|Notify|Begin|End could -// not see `UseProgram`, `BindVertexArray`, `BindProgramPipelineObject` or -// `BindTransformFeedbackObject` - four mutators that each move a field P3a or P4a pushes - -// because none of them starts with one of those words. `Use` and `Bind` are added, and the -// complete set the widening surfaces was enumerated by grep at the phase's base ref so it -// cannot surprise anybody: exactly those four names, on seven call sites. -// -// `Create*` and `Pop*` are DELIBERATELY NOT ADDED. They create or destroy objects rather than -// move a pushed field, and each object class's creation and destruction is already answered -// twice over - by its own Mark*ForDeletion row below and by the constructor-time -// resource_create - so adding them would produce rows that restate an answer this file already -// gives, and every one of them would have to be maintained against a mechanism that is not -// theirs. A gate whose rows do not each carry their own question is a gate nobody reads. -// +// Dirty-surface map: what publishes each frontend mutation a backend can observe, checked by +// scripts/gen_pipe_dirty_surface.py --check (ARCHITECTURE.md §5.2.1). An answer joins with '|' +// the publishers that fire on every path that mutates. NEW_*: a Tracker.h bit the next verb +// publishes. kImmediate: the same body reaches the backend. kReverseChannel: a backend write into +// the frontend. kNoBackendRead: no backend reads it. kExplicitDestroy / kUnpublishedDestroy: a +// death with / without a wire handle. kPulledEveryVerb: no bit; the residual fill copies the field +// at every verb. kPulledPartialShutter|NEW_*: the same pull; the bits move on some paths only. // clang-format off // X(Mutator, Answer) @@ -300,22 +144,13 @@ /* the paths that mutate and not all, which is exactly what this answer is for; what */ \ /* holds on every mutating path is still the pull through GetBufferBindingPoint. */ \ X(SetNamedTransformFeedbackBinding, kPulledPartialShutter|NEW_SO_TARGETS) \ - /* P5e (sb), THE ONE NAME THE `Touch` PREFIX SURFACES, on two call sites - GL_Buffer.cpp's*/ \ - /* BindBufferBase_State and BindBufferRange_State. It moves the per-target high-water */ \ - /* mark that bounds every indexed-binding-point walk there is: the backend's, the */ \ - /* client's GPU-write sweep's, and set_shader_buffers' emitted window. */ \ - /* */ \ - /* kPulledEveryVerb, in GetMaxTouchedTextureUnit's exact words one family over: the */ \ - /* field it writes (GetTouchedBufferBindingPointCount) has no shutter of its own and */ \ - /* needs none - it is in kDraw's, kDispatch's and kXfbSpan's may-read masks, so the */ \ - /* residual fill copies it at every verb of those classes, and since P5c rv */ \ - /* set_context_values carries it as a value whose gate is the whole-record hash. */ \ - /* */ \ - /* IT NAMES NO BIT, AND THAT IS NOT THE SAME AS THE MUTATION GOING UNPUBLISHED. Both */ \ - /* call sites bump the bind-point generation on the line below this call, so bits 15/16/ */ \ - /* 17 fire for the bind - but they fire for the BINDING, through a different member, and */ \ - /* a row here that named them would claim this mutator moves a shutter it does not touch */ \ - /* (the derivation refutes exactly that, which is how the claim was tested). */ \ + /* Called by BindBufferBase_State / BindBufferRange_State (GL_Buffer.cpp), this mutator */ \ + /* moves the per-target high-water mark that bounds every indexed binding-point walk. */ \ + /* No shutter reads it: GetTouchedBufferBindingPointCount is in the kDraw, kDispatch */ \ + /* and kXfbSpan may-read masks, and set_context_values carries it. Both call sites bump */ \ + /* the target's bind-point generation, which fires that target's NEW_CONST_BUFFERS, */ \ + /* NEW_SHADER_BUFFERS or NEW_SO_TARGETS through a different member, so this row must */ \ + /* not name them. */ \ X(TouchBufferBindingPoint, kPulledEveryVerb) \ /* ---- P4a, THE FOUR THE WIDENED PREFIX SET SURFACES. Every one of them moves a field */ \ /* P3a or P4a pushes and none of them was visible to the scan before, because none */ \ @@ -350,38 +185,13 @@ /* the kDraw and kXfbSpan may-read masks, so the residual fill copies them at every */ \ /* verb of those classes. Narrowing it is P4b's, with set_stream_output_targets. */ \ X(BindTransformFeedbackObject, kPulledEveryVerb) \ - /* ---- an object's death: no generation, because there is no longer an object */ \ - /* to carry one. Espryt 0b's delete_* / resource_destroy publishes the kinds */ \ - /* that have a handle on the wire; programs, program pipelines and shaders have */ \ - /* none in P2, so nothing publishes theirs - kUnpublishedDestroy, a known hole. */ \ - /* TWO OF THESE ROWS STOPPED BEING ASPIRATIONAL AT P3a, and the call that makes */ \ - /* each true is named rather than implied: */ \ - /* MarkBufferObjectForDeletion -> resource_destroy (PipeCalls.def), emitted */ \ - /* from ~BufferObject the moment the last reference drops - which is the */ \ - /* glDelete* that only marks the name, followed by whatever unbind actually */ \ - /* releases it - and followed IN THAT ORDER by the client freeing the slot. */ \ - /* MarkVertexArrayForDeletion -> delete_vertex_elements, published through the */ \ - /* death notice ~VertexArrayObject already raises for the VertexElementsCso */ \ - /* kind; the CSO handle is minted per frontend VAO off its lifetime id. */ \ - /* THE PUBLISHER IS THE BACKEND'S (Managers.cpp's OnFrontendStateObject- */ \ - /* Destroyed consumer, package espryt), not the client's: the client mints */ \ - /* the CSO handle and emits create/bind, and the free rides with that */ \ - /* consumer. Until it lands the row states the design, not the tree. */ \ - /* P4a CLOSES ONE OF THE THREE HOLES ABOVE AND STATES WHY THE OTHER TWO ARE NOT HOLES. */ \ - /* MarkProgramForDeletion -> kExplicitDestroy. delete_shader_state exists now and */ \ - /* ~ProgramObject emits it through the client-side death helper, in the fixed */ \ - /* order: the wire delete first, the backend notice second, the slot free last. A */ \ - /* program pipeline COMPOSITE takes the same call on the same helper - the server */ \ - /* never learns it is a composite. */ \ - /* MarkProgramPipelineForDeletion stays kUnpublishedDestroy, and it is NOT waiting */ \ - /* for a later phase: a ProgramPipelineObject has no lifetime id and no wire object */ \ - /* at all (its only identity is m_everBound). It never gets a handle, so there is */ \ - /* nothing for a delete to name. What its cache's eviction DOES publish is the */ \ - /* composite's delete_shader_state, which is the row above. */ \ - /* MarkShaderForDeletion stays kUnpublishedDestroy for the same kind of reason: a */ \ - /* ShaderObject has no lifetime id and never crosses the boundary - the payload is */ \ - /* per-stage SPIR-V plus the reflection archive, not source, and glslang lives */ \ - /* entirely on the client. */ \ + /* ---- an object's death: no generation, since no object is left to carry one. */ \ + /* kExplicitDestroy: the destructor (last reference, not glDelete*) runs the kind's */ \ + /* MGPipeEmit*DestroyAndFree (PipeMutation.h): the wire delete if the kind has one */ \ + /* (framebuffers do not), then the backend notice, then the slot free. Buffers send */ \ + /* resource_destroy, VAOs delete_vertex_elements, programs and pipeline composites */ \ + /* delete_shader_state. kUnpublishedDestroy: program pipelines and shaders have no */ \ + /* lifetime id and never cross the boundary, so no record names their death. */ \ X(MarkBufferObjectForDeletion, kExplicitDestroy) \ X(MarkFramebufferObjectForDeletion, kExplicitDestroy) \ X(MarkProgramForDeletion, kExplicitDestroy) \ @@ -408,48 +218,13 @@ X(AddTransformFeedbackPausedPrimitives, kPulledEveryVerb) \ X(AddTransformFeedbackPrimitives, kPulledEveryVerb) -// X(Mutator, Bit) - the (row, bit) pairs above whose derivation is KNOWN to come out -// UNDECIDED, each with the reason --check prints for it. Every bit answer NOT listed here -// is marked derived: --check fails when the derivation cannot decide it, and fails again -// when a mark here names a pair the derivation now decides, so this list can neither hide a -// row nor outlive its reason. -// -// IT WAS EMPTY UNTIL P4a, and it stops being empty for a reason that is a property of the -// SCANNER rather than of the rows. Every entry below is a bit answer that is plainly true - -// glUseProgram is what moves the object bits 6, 12 and 14's shutters read through (the -// program in use; bits 12 and 14 since the fable seam round, F-1 / F-2), and glBindVertexArray -// is what moves the object bit 5's shutter reads through - and the write analysis cannot say -// so, because each of the two mutators reaches, BY NAME, a body that writes a member with no -// m_ prefix: -// -// UseProgram -> DestroyProgramSlot() writes `attachedShaders` -// BindVertexArray -> a call spelled `Bind(` resolves to every body of that name, one of -// which (ImageTextureBinding::Bind) writes `Access` -// -// A call resolved by name to every body of that name is one of the three over-approximations -// this analysis documents about itself, and an unplaceable write TAINTS the body it is in - -// which is the right default, because "it does not write anything the shutter reads" must -// never be claimed about code the script could not read. Widening the taint rule to ignore -// non-m_ writes would weaken the one mechanism that catches a genuine under-fire, so the rows -// are MARKED, with the tool's own reason, rather than the tool being made more permissive. -// Control 21 is what proves every marked row still needs its mark (it reads this list, so a -// row that gains a bit here is counted rather than assumed), and control 18 is what fails the -// moment any of them becomes decidable and the mark outlives its reason. -// -// The ten mutators that reach a tainted body (--check prints the count) all carry a prose -// answer, which no derivation checks; these two mutators are the first that carry a bit answer. -// -// P5e (sb) ADDS A THIRD, for the same property of the scanner and a third unplaceable write: -// -// SetNamedTransformFeedbackBinding -// -> MGPipeSlots().Acquire() calls GetLifetimeId() through its reference -// parameter `buffer`, which the analysis cannot root in a member -// -// The claim it marks is plain: the bound arm of this function bumps the transform-feedback -// target's bind-point generation (BufferState::NoteBindPointChanged), which is exactly what -// bit 17's shutter reads since P5e. The mark is on the BIT the kPulledPartialShutter row -// names, not on the pull, so what this list suspends is the narrowing claim and never the -// answer that holds on every path. +// X(Mutator, Bit): bit answers above that gen_pipe_dirty_surface.py cannot decide. --check +// fails on an undecided pair missing here and on a listed pair it can now decide. Each answer +// is true, but its mutator reaches by name a body with a write the analysis cannot root in a +// member: UseProgram via DestroyProgramSlot() (`attachedShaders`), BindVertexArray via +// ImageTextureBinding::Bind (`Access`), SetNamedTransformFeedbackBinding via +// MGPipeSlots().Acquire() (reference parameter `buffer`). Marking keeps the taint rule strict; +// for SetNamedTransformFeedbackBinding only the bit after kPulledPartialShutter is suspended. #define MGP_DIRTY_SURFACE_UNDECIDED_LIST(X) \ X(UseProgram, NEW_SHADER) \ X(UseProgram, NEW_SAMPLER_VIEWS) \ diff --git a/MobileGL/MG_Pipe/FatalFamilies.def b/MobileGL/MG_Pipe/FatalFamilies.def index b8c55323..d7648c94 100644 --- a/MobileGL/MG_Pipe/FatalFamilies.def +++ b/MobileGL/MG_Pipe/FatalFamilies.def @@ -6,66 +6,12 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// P6 `dl` (CONTRACT-P6 5.2, D2 ruling). THE INTERNAL FATAL VOCABULARY, and its projection onto -// the SEVEN-value wire FatalCode. -// -// a6 censused 30 distinct family words against a 7-value wire enum, so Session::Fail(FatalCode, -// detail) cannot carry the vocabulary in its first argument - the word would collapse to one of -// seven and the log would say "ProtocolCorruption" where the code said "UnmigratedVerb". So the -// family is an INTERNAL enum, generated from this file, and the wire keeps FatalCode. Each row -// carries the coarse code the PEER is told when this family kills the session; the rich word -// stays in the local log and in the SessionFault frame's `family` string. -// -// THE PROJECTION IS A TOTAL TABLE, NOT A SWITCH WITH A DEFAULT. This build has no -Werror -// anywhere (measured: CMakeLists, every *.cmake, both workflows), so a gate premised on -Wswitch -// catching a missing arm is fiction. A row here IS the arm; a family with no row does not -// compile a call that names it. -// -// WHY EACH CODE. The wire FatalCode answers one question for the peer: what KIND of end was -// this. The buckets: -// ProtocolCorruption - the peer sent, or the session reached, something that cannot be -// honoured correctly: a malformed record, an unimplemented verb/surface, -// an invariant the roles must hold, a reply that does not fit its -// contract. The peer's takeaway is "we disagree about the protocol". -// RingOverrun - a shared ring or event channel overran. A transport-capacity fact. -// SegmentMismatch - a shared SEGMENT's geometry could not hold what a role needed to put -// in it. Distinct from a ring overrun: the segment is the wrong SIZE, -// not merely full this instant. -// AbiMismatch - the two builds do not agree on layout or protocol version. -// ServerCrashed - a liveness end the WAITING side attributes to the other: a drain that -// never completed, an apply thread that is gone or never started, a native -// resource the server had to have and could not obtain. The device-lost -// latch is armed from the doorbell, never from these, but the peer that -// was waiting is told the server is gone. -// DeviceLost - reserved: set by the latch (5.3), never raised as a Fatal family, so no -// row projects onto it. -// -// P7 WAVE 0 ADDED ELEVEN ROWS, for the eleven words the widened census gate found in the server -// image outside MG_Remote/ (scripts/ci/fatal_census.py rule 3). Ten are ProtocolCorruption for -// the reason that bucket already states - a record, a mask or a reply that cannot be honoured -// correctly. The eleventh, ResourceUnavailable (Managers.cpp's three image-promotion sites and -// VkBufferManager.cpp's eight), is ServerCrashed rather than SegmentMismatch: SegmentMismatch -// means a SHARED SEGMENT was the wrong size, and telling a peer that when a VkBuffer allocation -// or a native readback failed would be a lie about whose geometry was wrong. The honest thing to -// tell a waiting peer is the one ServerCrashed already says. The wire enum has no "the server -// could not obtain a resource" value and adding one would move `wireFingerprint`, which is a -// protocol change no wave-0 instrument is entitled to make (recorded for the integrator). -// -// P12 (on-screen server window) ADDED ONE ROW, ServerWindowLost: the display server's window was -// destroyed (surfaceDestroyed) under a live on-screen session. The session latches by name after -// its surface is released, and the WAITING client is told what ServerCrashed already says - the -// server-side resource its session had to have is gone - so it reads a clean device loss. Not -// DeviceLost: that code is the latch's own, never a family's projection (see its bucket above). -// -// BackendDeviceLost: the GPU device (Magma's VkDevice, Espryt's EGL context) a session renders -// with was lost to a GPU fault or hang. It is only ever LATCHED (MGPipeSessionLatch), so the one -// session ends and the display server's other sessions keep running; the waiting client is told -// what ServerCrashed says and reads a lost context. Not DeviceLost, for ServerWindowLost's reason. -// -// Two words were NOT given rows, they were collapsed instead: `InitialBytesNotCarried` -// (MG_Impl/Pipe/PipeFill.cpp) said the same thing as `UncarriedInitialBytes` (Client/ -// WireTables.cpp) about the same record - a respecify that crossed with no initial bytes - and -// two words for one family is exactly the drift a6 censused. PipeFill now uses the older word. +// The internal fatal families and the coarse wire FatalCode each one reports to the peer. The +// family word stays in the local log and in the SessionFault frame's `family` string. The table +// is total: a family without a row does not compile (the build has no -Werror, so -Wswitch +// cannot catch a missing arm). What each code means and how to choose one: ARCHITECTURE.md +// §15.3.1. DeviceLost is set only by the latch, so no row projects onto it. Reuse an existing +// family when it already describes the failure. // // clang-format off diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def index f2cb4935..22faf9db 100644 --- a/MobileGL/MG_Pipe/FieldOwnership.def +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -6,21 +6,13 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// P5f exit classification. This table describes SERVER reads, not monolith storage. -// RECORD_SUPPLIED is still derived from Coverage.def minus the whole-field refusal set. -// A handle record does not supply a legacy pointer-valued getter: those getters are FATAL, -// while their migrated consumers read the server record directly. Never relabel a pointer -// RECORD_SUPPLIED merely to make the debt count zero. -// -// APPLIER_DERIVED accessors answer server state. FATAL accessors must abort on any server -// read, including barriered/escalated records and non-strict/shared-block configurations. -// BARRIER_PULLED remains a schema class so generator controls can test debt admission, -// but no production row may use it after P5f. Monolith getter/fill behavior is unchanged. -// -// EmittedCallSuppliesTheWholeField retains its eight refusals: seven legacy pointer -// representations plus the two-sided pixel-store field. Deleting those refusals would -// falsely derive RECORD_SUPPLIED for the retired getters; server consumers bypass them. -// +// Server-read class of each PipeInputs field and sticky forward; the generator requires one. +// RECORD_SUPPLIED not listed: Coverage.def's emitted fields minus the refusals in PipeFill.cpp's +// EmittedCallSuppliesTheWholeField (legacy pointer getters, two-sided pixel +// store). Never relabel a pointer getter to zero the debt. +// APPLIER_DERIVED answered from server state. +// FATAL every server read aborts, whatever the record or configuration. +// BARRIER_PULLED schema only, for the generator's controls; no production row uses it. // clang-format off // X(Field, Class, RetiringPhase, Why) @@ -58,69 +50,24 @@ X(InvalidateCompileEnv, FATAL, "-", "transport caps republication replaces direct frontend invalidation") \ X(RecordError, FATAL, "-", "transport OnGlError posts an event before the legacy frontend-error-queue arm") -// 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 gPipeInputs->GetPixelStoreParameters -// call site in the tree passes `false`: DirectGLES.cpp's PackStateFromContext, StoreReadbackRowsToClient -// and ReadPixels, Utils.cpp's StoreClientRows, VulkanRenderer::ReadDepthStencilImageToClient() and -// RecordVerbSink::ReadBoundFramebufferTight() - 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. +// X(Field, Arg0, Class, Why) - argument-keyed exceptions. A row narrows one argument value of +// one field to another class; the field keeps its single row above, because it is one array +// (as GetBufferBindingSlot is in Coverage.def). +// GetPixelStoreParameters(isUnpack): only the pack half has a carrier, written by the applier, +// and every backend call site passes false. The unpack half is FATAL so a future backend read +// aborts by name instead of reading a stale struct. #define MGP_FIELD_OWNERSHIP_ARG_LIST(X) \ X(GetPixelStoreParameters, 1, FATAL, \ "the unpack half has no carrier AND no backend reader; all six gPipeInputs sites pass false") -// X(WireOp, Verb) - WHERE THE SERVER STAMPS. -// -// The stamp rule needs one thing the wire does not carry: which MGPipeVerb a record belongs -// to. The two name spaces are not the same and do not line up by name - the DRAW verb is -// `DrawArrays` and its record is `draw_vbo`, the BLIT verb is `BlitFramebuffer` and its record -// is `blit` - so the map is written here and checked against both sources (PipeCalls.def for -// the op, FillPoints.def for the verb) rather than believed. -// -// EXACTLY THE OPS THAT ARE VERB BOUNDARIES. CONTRACT-P5.md section 7 puts five slots in class -// B (emitted in P5): Clear, DrawArrays, ReadPixels, BlitFramebuffer and Present. FOUR OF THEM -// ARE HERE AND PRESENT IS NOT, and that is a ruling rather than an omission: FillPoints.def:21 -// says in so many words that "Present and SetSwapInterval go through BackendObject virtuals -// and read no frontend state, so they are not verbs here". There is no MGPipeVerb::Present to -// stamp for, MGPipeValidateForVerb is never called for it, and stamping at Present would -// retire the previous verb's answers with nothing to put in their place. -// -// An op that is not in this list is NOT a verb boundary and the applier must not stamp on it: -// a set_dynamic_state between two draws is part of the draw's verb, not a new one. -// -// THE GENERATOR REFUSES AN OMISSION, NOT ONLY A TYPO, and that is the half the first version of -// this file did not have. Every call that is VERB-SHAPED must have a row here or an exemption -// row below with a reason. "Verb-shaped" is the union of two mechanical tests, both derived: -// (a) PipeCalls.def gives the call kind kCtxVerb - the catalogue's own word for it; -// (b) the call's name is also an MGPipeVerb name in FillPoints.def. -// Eighteen rows and three exemptions cover all twenty-one (P5: twelve and three; P5b appended -// the five kCtxVerb rows 72..76 and settled ResourceCopyRegion). Only four of the rows could -// arrive in P5 (CONTRACT §7 class B minus Present); the rest are Fatal{UnmigratedVerb} on the -// client until a P5b migration package flips them and are mapped ANYWAY, because the failure -// mode of an absent row is silent: the applier would run the record under the PREVIOUS verb's -// serial, mask and name, and a field inside that mask would read FRESH while holding the -// previous verb's value. -// -// NOT MECHANICALLY DETECTABLE, and so not claimed: a call that is a verb boundary, is not -// kCtxVerb, and whose name differs from its verb's. `ResourceCopyRegion` WAS the one in the -// tree (its verbs could have been CopyImageSubData / CopyTexSubImage2D / CopyTexImage2D). P5b -// decided: resource_copy_region is glCopyImageSubData only, and the two framebuffer-sourced -// copies have their own row (copy_framebuffer_to_texture), so the choice is a row here rather -// than a rule in a reader's head. +// X(WireOp, Verb) - where the server stamps a verb boundary (ARCHITECTURE.md §9.2.1). Op and +// verb names differ (draw_vbo stamps DrawArrays, blit BlitFramebuffer), so the map is checked +// against PipeCalls.def and FillPoints.def. An unlisted op is not a boundary: a +// set_dynamic_state between two draws belongs to the draw's verb. The generator requires every +// verb-shaped call (kind kCtxVerb in PipeCalls.def, or named like an MGPipeVerb in +// FillPoints.def) to have a row here or in the exemption list, since a missing row would apply +// the record under the previous verb's stamp. A boundary that is neither goes undetected and +// needs a hand-written row. #define MGP_VERB_OP_LIST(X) \ /* CONTRACT §7 class B - the four that can actually arrive in P5. */ \ X(Clear, Clear) \ diff --git a/MobileGL/MG_Pipe/FillPoints.def b/MobileGL/MG_Pipe/FillPoints.def index 03e844ba..e2dc420f 100644 --- a/MobileGL/MG_Pipe/FillPoints.def +++ b/MobileGL/MG_Pipe/FillPoints.def @@ -6,72 +6,13 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The per-verb fill points of the PipeInputs strangler (ARCHITECTURE.md 9.2, phase A; the -// P1 brief D7). Three hand-maintained lists, read by scripts/gen_pipe.py (G5b) into -// generated/PipeFillPoints.inc: -// -// MGP_FILL_VERB_LIST every verb the frontend calls through GLFunctionsTable, with its class -// MGP_FILL_CLASS_LIST the verb classes -// MGP_FILL_FIELD_LIST the may-read table: which PipeInputs fields a class of verb may read -// -// The verb set IS the function-pointer member set of MG_Backend::GLFunctionsTable -// (MG_Backend/BackendObject.h), in declaration order: gen_pipe.py parses that struct and -// refuses a row set that is not exactly its member set in that order, so the MGPipeVerb enum -// and the table cannot drift apart. MG_Impl spells MGP_FILL(Verb) immediately before every -// call through the table (83 statements over these 69 verbs); Present and SetSwapInterval go -// through BackendObject virtuals and read no frontend state, so they are not verbs here. -// -// The seven sticky fields (Coverage.def, MGP_COVERAGE_STICKY_LIST) are implicit in every -// class and are not listed. The verify lane is the oracle for this table: a -// Fatal{UnmigratedPipeInput, "Field@Verb"} found there is fixed by adding the (class, field) -// row, never by marking the field sticky. -// -// --------------------------------------------------------------------------------------- -// THE VERDICT ON THE EIGHT STATICALLY OVER-APPROXIMATED ROWS (P2 brief C.1, MEASUREMENTS.md -// section 4). Every one of them is KEPT, and the reason is the same in all three groups: the -// row is not a guess, it names a concrete backend path, and the only evidence that could -// retire it is DYNAMIC - a corpus that never reaches the path proves nothing, because a row -// removed on that basis turns a rare path into Fatal{UnmigratedPipeInput} in a shipped build. -// -// kReadback + IsTransformFeedbackActive / IsTransformFeedbackPaused -// KEPT. The depth/stencil read emulation draws (ScopedEmulationDrawState, DirectGLES.cpp) -// and pauses an active capture around its own draw, so a glReadPixels of a depth or -// stencil attachment reads the transform-feedback state exactly as a draw does. Reached -// only when the emulation is armed, which is a driver-shaped decision, so no desktop -// corpus can decide it. -// -// kTextureOp + IsCapabilityEnabled, kDispatch + IsCapabilityEnabled -// KEPT. Magma's GenerateMipmap materialises a texture's queued clear before it blits and -// PrepareStorageImageTextures does the same for every storage image a dispatch writes; -// both go through VkClearManager::PreCompensateSrgbClearColor, which reads -// GL_FRAMEBUFFER_SRGB. The P2 contract gave that capability real storage for the first -// time, so this row went from reading a compile-time constant to reading real state - -// which is the opposite of a row that could be dropped. -// -// kBlitOrCopy / kTextureOp + the shader blit's viewport and vertex/buffer bindings -// (GetViewportIndexed, GetDepthRangeIndexed, GetProvokingVertexMode, GetBufferBindingPoint) -// KEPT. TryBlitToDefaultFramebufferWithShader is a real draw of a backend-owned helper -// program: ApplyGLViewportState -> ComputeGLViewport reads viewport 0 and its depth range, -// GetOrCreateBlitPipeline -> SelectProvokingVertexMode reads the provoking vertex, and -// BindProgramUniformBuffers' block resolvers read the frontend binding points. It is taken -// when a blit's destination is the default framebuffer and the driver cannot do it -// natively - again a driver-shaped decision. -// -// What WOULD retire a row: the poison build already answers "was this field read at this -// verb" exactly (MOBILEGL_PIPE_POISON_OMIT withholds one field's stamp for one verb and a -// read of it aborts naming the pair). Turning that into a retirement gate means running the -// omission across the full CTS caselist on both devices, not the desktop corpus. -// -// P3a STATUS: still not done, and deliberately not done here. P3a is one of the five -// architecture boundaries that owe a full gl44to46 caselist run on both devices, so the -// omission sweep rides that run rather than duplicating it - the verdict lands with the -// caselist result at the phase exit, off the critical path, and every row above stays in -// place until it does. A row retired on desktop evidence would be retired on evidence that -// cannot support it, which is the reason this item exists rather than a scheduling excuse. -// --------------------------------------------------------------------------------------- -// +// Per-verb fill points of PipeInputs (ARCHITECTURE.md §9.2), read by scripts/gen_pipe.py. +// MGP_FILL_VERB_LIST: every GLFunctionsTable function pointer in declaration order, with its +// class (gen_pipe.py refuses any other set). MGP_FILL_FIELD_LIST: the fields a class may read; +// sticky fields (MGP_COVERAGE_STICKY_LIST) are implicit. Fix Fatal{UnmigratedPipeInput} by +// adding a row, never by making it sticky. A row is removed only on a MOBILEGL_PIPE_POISON_OMIT +// run of the full CTS caselist on both devices; a corpus that never reaches a path proves nothing. // gen_pipe.py's block regexes end at a blank line: keep the empty line after each macro. -// // clang-format off // X(Verb, Class) - one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h), diff --git a/MobileGL/MG_Pipe/MGPipe.h b/MobileGL/MG_Pipe/MGPipe.h index d1c0786a..46d9302f 100644 --- a/MobileGL/MG_Pipe/MGPipe.h +++ b/MobileGL/MG_Pipe/MGPipe.h @@ -54,22 +54,13 @@ namespace MobileGL::MG_Pipe { kOptional = 1u << 5, }; - // P5e (docs/archive/contracts/CONTRACT-P5E.md §2.2). THE FIFTH COLUMN OF PipeCalls.def: what the client - // does after publishing a record of this call, on a server that publishes - // kCapRunAheadApply. Unscoped for MGPipeCallClass's reason - PipeCalls.def spells these as - // bare tokens so the same file can be read by the C++ preprocessor and by - // scripts/gen_pipe.py, which generates MGPipeWaitClassFor(op) from it. - // - // IT IS A VALUE, NOT AN OPCODE AND NOT A FLAG. The wire carries the opcode and both roles - // recover the class from it, exactly as they recover the flags - which is the whole reason - // MGPipeBarriered(op, payload, applierState) can be one function computed identically by - // the emit table and by the sink instead of two copies that agree until they do not. - // - // kWaitNone is 0 so that an uninitialised read is the SAFE direction in exactly one sense - // and the dangerous one in the other, which is why nothing reads this table without an - // opcode that came out of the catalogue: MGPipeWaitClassFor answers kWaitClassCount for an - // opcode it does not know, and a caller holding bytes off a stream must reach its own - // Fatal{ProtocolCorruption} rather than a wait decision. + // The fifth column of PipeCalls.def: what the client does after publishing a record of this + // call to a server that publishes kCapRunAheadApply (ARCHITECTURE.md §11.6). Unscoped so the + // .def can use bare tokens for both the preprocessor and scripts/gen_pipe.py, which generates + // MGPipeWaitClassFor(op). Both roles recover the class from the opcode, so + // MGPipeBarriered(op, payload, applierState) is one function for the emit table and the sink. + // MGPipeWaitClassFor answers kWaitClassCount for an unknown opcode; a caller holding stream + // bytes must raise its own Fatal{ProtocolCorruption} rather than make a wait decision. enum MGPipeWaitClass : Uint8 { kWaitNone = 0, kWaitApplied, @@ -107,35 +98,9 @@ namespace MobileGL::MG_Pipe { // the other. inline constexpr Uint64 kMGPipeSubsystemResources = 1ull << 7; inline constexpr Uint64 kMGPipeSubsystemVertexInput = 1ull << 8; - // P4a's four. FOUR AND NOT ONE, for P3a's reason one level out: a framebuffer path that - // regressed, a texture path that regressed, a sampler path that regressed and a program - // path that regressed are four different findings, and clearing one must not disarm the - // other three. - // - // FOUR OF THE SIX FAMILIES CARRY A DEPENDENCY and it is diagnosed at the first use, never - // half-run - one ResolveSubsystemArm per family beside the backend's existing - // ResolveResourceSubsystemArm, and lazy rather than at bring-up because a pre-flight child - // dying on a signal makes a whole lane SKIP green. - // - // THE ROWS ARE IN SubsystemDeps.def, ONCE (P3b/P4b R-5), and this comment no longer - // restates them. It used to, and it was WRONG in two ways that nothing could fail: it said - // "THREE OF THEM" when there are six rows, and it said "the mirror pairs (10 without 11, - // 10 without 9, 7 without 10) are all fine" when 10-without-11 is precisely D-K2's FOURTH - // row (ID-14/ID-15) - refused by the server and withheld by the client, with PipeFill.cpp - // saying so out loud: "The brief's original 'bit 10 without 11 is fine' is WITHDRAWN for - // P4a as built." The audit found the same pair of defects repeated in - // MG_Backend/DirectGLES/Managers.h's header, i.e. in the header of the file that - // implements the refusal. - // - // THE MIRROR PAIRS THAT REALLY ARE FINE, said out loud rather than left as an absence, - // because a table is only trustworthy if what it does NOT contain was decided: - // - bit 10 set, bit 9 clear: FINE. The legacy FBO sync reaches the texture twin through - // SyncTextureObjectToBackend, which dispatches to the handle arm by itself. - // - bit 11 set, bit 9 clear: FINE, for the same reason - a sampler view names a texture, - // never a framebuffer. - // - bit 7 set, bit 10 clear: FINE, and it is P3a's shipped configuration. - // - bit 12 set with any or none of 9/10/11: FINE, per its own row. - // - bit 10 set, bit 11 clear and its mirror are NOT fine; that is the fourth row. + // One bit each for the framebuffer, texture-resource, sampler and program families. Bits + // 0-13 are fixed on (ConfigLoader.cpp refuses a cleared one at startup); the dependencies + // between bits are in SubsystemDeps.def. inline constexpr Uint64 kMGPipeSubsystemFramebuffer = 1ull << 9; // set_framebuffer_state inline constexpr Uint64 kMGPipeSubsystemTextureResources = 1ull << 10; // texture + renderbuffer // resource_*, set_texture_params diff --git a/MobileGL/MG_Pipe/MGPipeCallbacks.h b/MobileGL/MG_Pipe/MGPipeCallbacks.h index c0a8f2c6..76980f8a 100644 --- a/MobileGL/MG_Pipe/MGPipeCallbacks.h +++ b/MobileGL/MG_Pipe/MGPipeCallbacks.h @@ -12,43 +12,12 @@ #include "MGPipeHandles.h" #include "MGPipeTypes.h" -// The backend -> frontend reverse channel, named (plan B section 7.1). -// -// Today this traffic is 95 call sites across 17 methods poked directly into frontend -// objects. An in-process driver interface has no vocabulary for shadow writeback, GPU-write notification or -// default-framebuffer geometry, because in such a design the state tracker and the driver share an -// address space. Naming them as callbacks is the deliberate deviation (D8). -// -// SEVEN, NOT THE TEN PLAN B WROTE. Three went, each because nothing produces it. -// -// OnTexturePullRequest, the server asking the client to re-send a texture's levels after -// re-minting it (with op 50 ResourceSubDataComplete as the forward terminator). P9 W2 measured -// that no re-mint needs the client: Espryt's server reads the GPU level back and replays its own -// staged store, Magma copies GPU to GPU, on every arm (docs/archive/notes/p9/W2-REMINT.md). So the callback -// went; op 50 stays in the opcode table as a retired row (opcodes only ever grow) and its decoder -// still declines it. -// -// OnXfbScatterReady went with the -// design it belonged to: plan B put the XFB scatter on the CLIENT (the server would hand -// back the packed scratch and the client would run the patch loop over its own shadow), so -// there had to be a callback that told the client the layout. P5c/P5f went the other way - -// the server reads its OWN staged shadow, scatters there and returns the reconciled range -// through OnBufferWriteback, which needs no second callback - and the declaration was left -// behind with zero producers, zero consumers and no EventKind of its own. Deleted here -// (P3b/P4b espryt D1 slice 3) rather than carried: an entry that cannot fire is one every -// reader of this file has to rule out by hand, and the size assertion below made it look -// load-bearing. -// -// And OnLog, deleted in P9 W3 for the same reason: zero producers, zero consumers, no -// EventKind. Plan B pictured the server's log lines as reverse-channel records; they never -// were. They travel the CONTROL plane as LogLine frames (protocol.fbs), written by the server's -// logger through its log forward (MG_Remote/Transport/LogForward.h) - which is also where the -// severity policy this entry's comment promised ("<= WARN lossy, >= ERROR lossless and rate -// limited") now lives, on the level LogLine has always carried. -// -// Installed at context creation. In a monolith these are direct calls; under split they are -// records on the reverse channel, and their ORDER is a correctness requirement rather than -// an optimization (section 7.4). +// The backend -> frontend reverse channel as named callbacks (ARCHITECTURE.md §8.1): shadow +// writeback, GPU-write notification, surface geometry and the like, which an in-process driver +// interface has no names for. Installed at context creation; direct calls in a monolith, +// records on the reverse channel under split, where their order relative to the command stream +// is a correctness requirement (§8.2). Server log lines travel the control plane as LogLine +// frames (MG_Remote/Transport/LogForward.h), not this channel. namespace MobileGL::MG_Pipe { struct MGPipeCallbacks { // A driver-detected GL error that only the server could have seen. The text is @@ -76,22 +45,12 @@ namespace MobileGL::MG_Pipe { // Null-initialized: a backend that installs nothing sends nothing. inline MGPipeCallbacks gMGPipeCallbacks{}; - // HOW MUCH OF ITSELF ONE REVERSE-CHANNEL RECORD MAY BE, and it is not a tuning knob: a - // producer that ignores it kills the server process. - // - // OnBufferWriteback's bytes travel INLINE in one SEG_EVENT record, and a ring producer - // refuses any record above Capacity()/2 OUTRIGHT (Transport/Ring.h) - not "when full", but - // always, because above half a capacity a record is placeable at some head offsets and not - // at others, so waiting for room would be a hang. The server's answer to a refusal is - // Fatal{EventRingOverflow}, and SEG_EVENT defaults to 256 KiB, so ANY producer that posts a - // whole range wider than ~128 KiB is a server that dies on a large enough buffer. - // - // THE PRODUCERS ARE IN MG_Backend, which cannot reach MG_Remote::Client's - // BufferWritebackSliceBytes (that one is the CLIENT-linkage answer to the same question, - // for requests going the other way) and must not reach ServerSession either. So the - // transport publishes the one number through this hook, exactly as it publishes segment - // resolution through gMGPipeSegmentResolver: the SERVER role installs it at Accept and - // releases it at Close, and a monolith build leaves it null. + // Size of the SEG_EVENT ring, which bounds one reverse-channel record. OnBufferWriteback + // carries its bytes inline, and the ring producer always refuses a record above half its + // capacity (Transport/Ring.h) with Fatal{EventRingOverflow}, so producers slice wide ranges + // (MGPipeBufferWritebackSliceBytes). The producers live in MG_Backend and cannot reach the + // transport, so the server role installs this query at Accept and clears it at Close; a + // monolith build leaves it null. using MGPipeEventRingCapacityQuery = Uint64 (*)(); inline MGPipeEventRingCapacityQuery gMGPipeEventRingCapacityBytes = nullptr; diff --git a/MobileGL/MG_Pipe/MGPipeHandles.h b/MobileGL/MG_Pipe/MGPipeHandles.h index e06f1f7d..e985906d 100644 --- a/MobileGL/MG_Pipe/MGPipeHandles.h +++ b/MobileGL/MG_Pipe/MGPipeHandles.h @@ -80,22 +80,14 @@ namespace MobileGL::MG_Pipe { // Framebuffer), so a real allocation starts at 1. inline constexpr Uint32 kMGPipeFirstAllocatableSlot = 1; - // ShaderCso slot space. The top 1/16 of it is reserved for PROGRAM PIPELINE COMPOSITES - // (section 5.6.3): a composite is minted client-side out of the stage programs bound to - // a pipeline object, and the server never learns it is a composite - it is just another - // ShaderCso. Reserving a band rather than a flag keeps the composite resolver's - // lifetime bookkeeping out of the ordinary program slot allocator. - // - // THE ONE ENTRY POINT INTO THE BAND is MGPipeSlotAllocator::AllocateComposite(lifetimeId) - // (MG_Impl/Pipe/SlotAllocator.h, P4a D-H7). MGPipeSlotAllocator::Allocate REFUSES the band - // for kind ShaderCso, which is what makes "an ordinary program can never be handed a - // composite slot" a property of the allocator rather than of its callers; the band carries - // its own exhaustion assert, so exhausting it is a named Fatal rather than silent slot - // theft from ordinary programs. A composite's slot has TWO independent release paths - the - // pipeline cache's LRU eviction and the composite ProgramObject's own destructor - and - // both go through one client-side death helper (MG_Pipe/PipeMutation.h's - // MGPipeEmitShaderCsoDestroyAndFree), whose second call is a proven no-op because Free - // refuses a slot that is not live at that generation. + // ShaderCso slot space. The top 1/16 is reserved for program pipeline composites + // (ARCHITECTURE.md §5.6): a composite is minted client-side from a pipeline object's stage + // programs and is just another ShaderCso to the server. A band rather than a flag keeps the + // composite resolver's lifetime bookkeeping out of the ordinary program slot allocator. The + // only way in is MGPipeSlotAllocator::AllocateComposite (MG_Impl/Pipe/SlotAllocator.h); + // Allocate refuses the band for ShaderCso, and the band has its own exhaustion check. A + // composite slot has two release paths (pipeline cache LRU eviction, the composite + // ProgramObject's destructor), both through MGPipeEmitShaderCsoDestroyAndFree (idempotent). inline constexpr Uint32 kMGPipeShaderCsoSlotLimit = 1u << 20; inline constexpr Uint32 kMGPipeShaderCsoCompositeSlotBase = kMGPipeShaderCsoSlotLimit - (kMGPipeShaderCsoSlotLimit >> 4); diff --git a/MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp b/MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp index 6a7d636b..b0e45151 100644 --- a/MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp +++ b/MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp @@ -6,49 +6,13 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The definitions behind MGPipeRenderStateSpans.h and behind the two arrays -// generated/PipeSpanTable.inc has declared since P0. Compiled ONLY under -// MOBILEGL_PIPE_PUSH (CMakeLists.txt appends it to SOURCE_FILES there), which is how the -// pull build gains no symbol from the split - a declaration emits nothing. -// -// PROVENANCE OF THE PIPELINE HALF. It began as the enumeration -// VulkanRenderer::ComputePipelineStateHash carried above itself, which was the contract -// that function had without being able to say so; it moves here because this file is now -// that contract. Verbatim, from VulkanRenderer.cpp at feat/disaggregated@48268068: -// -// Value hash over every fixed-function GL state the pipeline payload reads that -// the memo key's other fields (mode, program hash, vertex-input hash, render-pass -// hash, transform flags) do not already pin down. Enumerated against the payload -// build in GetOrCreatePipeline - any new GL-state read there must be added here: -// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides -// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, -// PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask -// - patch vertices, polygon mode, cull face mode, depth func, logic op, -// min sample shading, the glSampleMaski word -// - front/back stencil ops + compare funcs (ref/mask are dynamic state) -// - per draw buffer up to the render pass's colour span: indexed blend enable, -// blend factors/equations, indexed colour write mask (broadcast from index 0 -// when the device lacks independentBlend - the same read the payload does) -// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are -// pinned by the render-pass hash key, exactly as the version-keyed memo relied on. -// -// P2's pipeline half is a strict SUPERSET of that list. It adds SampleCoverageValue, -// SampleCoverageInvert, FrontFaceModeSetting, ProvokingVertexModeSetting, -// ScissorTestEnabledMask, PolygonModeBack, the eleven capability bools the hash never read -// (DebugOutput, DebugOutputSynchronous, Dither, LineSmooth, PolygonOffsetLine, -// PolygonOffsetPoint, PolygonSmooth, SampleAlphaToCoverage, SampleAlphaToOne, SampleCoverage, -// ProgramPointSize) and the three capabilities P2 gave storage to (FramebufferSrgb, -// DepthClamp, TextureCubeMapSeamless). All of them are written by a setter that calls -// BumpVersions(), so under the header's rule they are pipeline. The alternative - demoting -// those setters to ++m_version - would change MG_State semantics in the PULL build for the -// sake of the push path. Growing the subset costs nothing measurable: the hash runs only -// when m_pipelineStateVersion moves, which is exactly when Magma recomputed -// ComputePipelineStateHash before. -// -// The render-pass facts are deliberately NOT here. ComputePipelineStateHash's signature is -// (colorAttachmentCount, rasterizationSamples) and it folds ResolveEffectiveSampleMask, so -// it was never a pure function of RenderStateParameters; a CSO handle cannot replace it on -// its own and Magma keeps renderPassHash as a separate memo-key component. +// Definitions behind MGPipeRenderStateSpans.h and the two chunk arrays that +// generated/PipeSpanTable.inc declares (ARCHITECTURE.md §5.3). The pipeline half holds every +// byte a BumpVersions() setter writes, which covers all fixed-function state Magma's pipeline +// build reads and more; the extra bytes cost nothing because the subset is hashed only when +// m_pipelineStateVersion moves. Render-pass facts (colour attachment count, rasterization +// samples and the effective sample mask derived from them) are not in RenderStateParameters, +// so Magma keeps renderPassHash beside the CSO handle in its pipeline memo key. #include #include diff --git a/MobileGL/MG_Pipe/MGPipeRenderStateSpans.h b/MobileGL/MG_Pipe/MGPipeRenderStateSpans.h index 9fef1683..b32b85db 100644 --- a/MobileGL/MG_Pipe/MGPipeRenderStateSpans.h +++ b/MobileGL/MG_Pipe/MGPipeRenderStateSpans.h @@ -12,30 +12,14 @@ #include "MGPipeTypes.h" #include "MGPipeValueTypes.h" -// G7: the pipeline/dynamic split of RenderStateParameters, written in EXACTLY ONE PLACE -// (ARCHITECTURE.md 5.3, D-B1). -// -// The rule that decides the split, and it is the only rule: -// -// A byte of RenderStateParameters is in the PIPELINE half if and only if some public -// RenderState setter that calls BumpVersions() writes it. Every other byte is in the -// DYNAMIC half. There is no third set. -// -// That makes the G7 invariant - the pipeline-subset hash moves IF AND ONLY IF -// m_pipelineStateVersion moves - true by CONSTRUCTION rather than by inspection, and it is -// what MG_Test/Pipe/RenderStateSpansTest.cpp walks every setter to confirm. -// -// The chunks alternate: chunk 0 is dynamic, chunk 1 is pipeline, and so on, so the whole -// table is 16 BOUNDARIES rather than 15 hand-written ranges. Every boundary is an offsetof -// or a sizeof - never a literal - because a python guess at a layout it cannot see is -// exactly the drift the setter-consistency test exists to catch. 8 dynamic chunks + 7 -// pipeline chunks = 15, and both counts fit the Uint32 ChunkMask of MGPRenderStateDesc and -// MGPDynamicState with room to spare. -// -// Note the two splits are ORTHOGONAL and coexist (ARCHITECTURE.md 5.3): DirectGLES' -// head [0, 312) / blend [312, 536) / tail [536, 1168) spans cut ACROSS this table, and -// nothing about them changes. StencilFaceState is deliberately NOT reordered - reordering -// would move Espryt's shadow bytes for no gain. +// The pipeline/dynamic split of RenderStateParameters, written only here (ARCHITECTURE.md §5.3). +// Rule: a byte is in the pipeline half if and only if some public RenderState setter that calls +// BumpVersions() writes it; every other byte is dynamic. So the pipeline-subset hash moves +// exactly when m_pipelineStateVersion moves; MG_Test/Pipe/RenderStateSpansTest.cpp checks every +// setter. Chunks alternate dynamic/pipeline from chunk 0, so 16 boundaries describe 15 chunks +// (8 dynamic, 7 pipeline; each count fits the Uint32 ChunkMask), every one an offsetof or +// sizeof, never a literal. DirectGLES' head/blend/tail spans cut across this table +// independently; StencilFaceState is not reordered, as that would move Espryt's shadow bytes. namespace MobileGL::MG_Pipe { namespace MGPipeRenderStateChunkDetail { diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 871d05eb..75404047 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -12,24 +12,14 @@ #include "MGPipeHandles.h" #include "MGPipeHostSpan.h" -// Every MGPipe payload (plan B section 4.5). Each one is a flat POD with explicit padding, -// carries a static_assert on trivial copyability and one on its exact size, and never -// contains a pointer: MGHostSpan, the one shape that changes with the transport, only ever -// rides in a variable tail (draw_vbo's user indices, set_shader_buffers' named-UBO bytes), -// never inline in a fixed payload. -// -// Sizes are asserted rather than merely documented because the wire records generated from -// these structs (generated/PipeWire.inc) are memcpy'd; a field silently changing width is a -// protocol break that no test would otherwise see. -// -// P0.5 DEBT, half repaid. The MG_State half is gone: the (since deleted) ResidualValueBlock's -// RenderStateParameters and PixelStoreParameters now come from MGPipeValueTypes.h, so -// this header no longer reaches RenderState.h (its closure still touches TextureEnum.h, -// through BackendObject.h, for the reason in the next sentence). What remains is MGPCaps embedding -// MG_Backend's DynamicBackendParameters - deliberate, the caps block IS that struct -// (section 4.4.1) - and that one include is what still keeps purity gate A (section -// 10.3) off this header; the gate asserts MGPipeValueTypes.h instead. The caps block -// needs fixed-width members before it can move (a type change, not a move): P1/P7. +// Every MGPipe payload (ARCHITECTURE.md §4). Each is a flat POD with explicit padding, a +// static_assert on trivial copyability and one on its exact size, and never contains a pointer: +// MGHostSpan, the one shape that changes with the transport, rides only in a variable tail. +// Sizes are asserted because the generated wire records (generated/PipeWire.inc) are memcpy'd, +// so a field changing width is a protocol break no other test would see. +// BackendObject.h supplies DynamicBackendParameters (embedded in MGPCaps), BackendType and, +// through TextureEnum.h, the texture enums; that reach into MG_Backend is why the include-purity +// gate (scripts/check_include_closure.py) checks MGPipeValueTypes.h rather than this header. #include #include "MGPipeValueTypes.h" @@ -133,22 +123,13 @@ namespace MobileGL::MG_Pipe { // buffer. So the answer is the SERVER's table, published as a bit and read through // MGL_BACKEND_SLOT_CAP. The first of the "XFB span family" bits SlotCaps.h predicted. kCapBackendOwnsXfbCapture = 1ull << 9, - // P5e (docs/archive/contracts/CONTRACT-P5E.md §1, §6). THE SERVER APPLIES AN UNBARRIERED RECORD - // WITHOUT READING CLIENT MEMORY: every object it needs is resolved from a handle the - // record or the applier state carries, so the client may publish and move on instead - // of waiting for appliedSeq at every verb (rule F). - // - // It is set in the DirectGLES arm of the server's CallMask and NEVER in the - // DirectVulkan one - Magma keeps the lockstep for the whole of P5e (§6), its - // apply-thread allocator debt is real, and a bit published there would turn that debt - // into torn reads rather than into the accounted pulls it is today. On Espryt the arm - // is gated on kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp), false until the P5e - // integration commit, so every package lands with the wait rule inert. - // - // The client latches RunAheadArmed() = m_barrierArmed && Ipc.RunAhead && - // Caps().HasCap(kCapRunAheadApply) at the first caps adoption after Start; a later - // snapshot may only turn it OFF, because a caps re-run must not be able to make a - // lockstep server run-ahead in the middle of a frame. + // The server applies an unbarriered record without reading client memory: every object + // it needs is resolved from a handle in the record or the applier state, so the client + // may publish and move on instead of waiting for appliedSeq (ARCHITECTURE.md §11.6). + // Published through MGPipeRunAheadCapBitsFor, gated per backend by a readiness constant + // in MG_Backend/Init.cpp. ClientSession::LatchRunAheadFromCaps arms run-ahead at the first + // caps adoption after Start; a later snapshot may only disarm it, because records already + // published without a wait cannot be taken back. kCapRunAheadApply = 1ull << 10, // P11 B2 (docs/archive/contracts/CONTRACT-P11.md B2). THIS SESSION MAY RUN T0: the client asked for // it in its Hello (LinkTerms.adoptTier = 0) on a shared-segment plane, the server's @@ -358,31 +339,14 @@ namespace MobileGL::MG_Pipe { Uint32 StorageFlags; // glBufferStorage flags Uint8 HasDefinedContent; // false after a NULL-data respecify - STORAGE-DEFINING Uint8 ImageBindableHint; // client-side everImageBound; pre-emptive allocation - // THE SCOPE OF A RESPECIFY (P5 R-13.3b, CONTRACT-P5.md table 1 row 19b). Zero means - // "this respecify redefines the WHOLE resource"; non-zero means it redefines exactly - // the (RespecifiedUploadTarget, RespecifiedLevel) pair below and nothing else. - // - // It exists because the applier ALREADY takes that scope - as a trailing - // const MGPRespecifiedLevel* (PipeApply.h:792-795) - and MGPResourceDesc could not - // express it, so it was the second of resource_respecify's two companions with no wire - // carrier. PH-4 extends that carrier with exact W/H/D for the named level, which the - // server validates before staging bytes. The difference is not cosmetic: a null scope drops EVERY pending upload, - // because every level's coordinate system has just been replaced, while a per-level - // one drops only that key. Crossing without the scope would make every mutable - // per-level glTexImage*D on the far side look like a whole-resource redefinition and - // silently eat the texels of every other level - exactly the loss the server-side - // pending-upload set exists to prevent. - // - // READ IT THROUGH THE HELPERS BELOW, never by touching the fields: the presence byte, - // target/level pair and exact per-level extent form one value. For a named image level, - // BufOffset/BufSize temporarily carry W/H/D; the target makes this safe because a - // texture-buffer range cannot legally name a mip level. The applier clears that carrier - // before storing the descriptor. - // - // NOT STORAGE-DEFINING, and not metadata either: it does not describe the resource at - // all, it describes what this CALL replaces. MGPipeResourceRespecifyNeedsAck and the - // storage-defining field set below are unaffected by it, which is why it is not named - // in either. + // The scope of this respecify. Zero: it redefines the whole resource and the applier + // drops every pending upload. Non-zero: it redefines only the (RespecifiedUploadTarget, + // RespecifiedLevel) pair below and drops only that level's uploads, so a per-level + // glTexImage*D keeps the other levels' texels. Use the helpers below: the presence byte, + // the pair and the level's exact extent are one value, and for a named level + // BufOffset/BufSize carry W/H/D (a texture buffer cannot name a level) until the applier + // clears them. It describes what the call replaces, not the resource, so it is neither + // storage-defining nor metadata (see MGPipeResourceRespecifyNeedsAck below). Uint8 HasRespecifiedLevel; Uint8 Pad0; // ImageBindableHint and BindMask above are the two METADATA fields the rule exists @@ -558,23 +522,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPDynamicState, 32); - // Both views travel, and neither is derivable from the other: the resolved - // VertexAttribute[32] AND the binding points, because a pointer-call stride of 0 means - // "element size" while a binding-model stride of 0 means "every vertex reads the same - // element" (section 4.5.3). IsLong and Type == Float64 are carried separately. - // - // THE BLOB RULE - ONE RULE FOR EVERY RECORD IN THIS HEADER THAT CARRIES AN MGPBlobRef, - // and MGPSubData below is the other one. `Blob.Size` is the record's own statement of how - // many bytes its blob holds, and the applier holds the record to that statement WHENEVER - // THE RECORD MAKES IT: a non-zero Blob.Size that disagrees with the byte length the - // record's other fields describe is Fatal{ProtocolCorruption} and the call is refused. A - // ZERO Blob.Size means "this record does not declare its blob", which is what a monolith - // emission is - the bytes travel beside the record through the apply entry point's - // companion `const void*` and no MGPBlobRef is filled - and it is not a fault. Either way - // the bytes read are bounded by the record's OTHER fields (the two counts here, the - // destination range there), so the length is a cross-check and never the safety property. - // A transport that fills these in gets a real gate on the first record it truncates; a - // client that leaves them zero gets no verify-build abort for a field it never used. + // Both views travel and neither derives from the other: the resolved VertexAttribute[32] + // and the binding points, because a pointer-call stride of 0 means "element size" while a + // binding-model stride of 0 means "every vertex reads the same element". IsLong and + // Type == Float64 are carried separately. + // Blob rule for every record here with an MGPBlobRef: a non-zero Blob.Size must equal the + // length the record's other fields describe, or the applier refuses the call with + // Fatal{ProtocolCorruption}; zero means "not declared" (monolith passes the bytes beside the + // record). The other fields always bound the read; Blob.Size is only a cross-check. struct MGPVertexElements { MGPipeHandle Cso; Uint32 AttributeCount; @@ -730,66 +685,24 @@ namespace MobileGL::MG_Pipe { Uint16 Level; Uint32 Layer; Uint16 UploadTarget; // static_cast(MobileGL::TextureUploadTarget) - // P4a, ID-12 / esprytobj DV-5: WAS Pad0, and the size did not move - the two bytes - // were already here. static_cast(MobileGL::TextureTarget), and - // kMGPipeSurfaceNoTextureTarget on every point that is not a texture. - // - // THE FOUR CROSS-OBJECT MASKS ARE WHY IT EXISTS. IsSnormFallbackAttachment, - // IsUnormFallbackAttachment and IsAlphaWidenedColorAttachment all reduce to - // (format, TEXTURE TARGET) - ShouldUseCaveatTextureFormat(format, target) and - // BackendTextureFormatAddsAlpha(format, target) - and no TextureUploadTarget -> - // TextureTarget inverse exists anywhere in the tree, so UploadTarget cannot answer - // them. Without this field D-C1's promise that the inline InternalFormat makes the - // masks "fall out at push time with no lookup" is unkeepable and the backend keeps - // reading the frontend attachment objects. - // - // CONSULTED ONLY WHEN Kind == kMGPipeSurfaceKindTexture. A zero-initialised record - // carries 0, which is TextureTarget::Texture1D and not the sentinel; that is not a - // defect, because such a record is Kind == None and names no texture at all. Gating - // on Kind is the reader's contract. + // static_cast(MobileGL::TextureTarget), or kMGPipeSurfaceNoTextureTarget on a + // point that is not a texture. The snorm/unorm-fallback and alpha-widened attachment + // masks depend on (format, texture target), so the backend answers them from this record + // instead of reading the frontend attachment objects. + // Read it only when Kind == kMGPipeSurfaceKindTexture: a zeroed record carries 0 + // (Texture1D), which is harmless only because its Kind is None. Uint16 TextureTarget; }; MGP_ASSERT_POD(MGPSurface, 24); - // P4a, D-C2/D-C3 and ID-19: THE RECORD DESCRIBES A FRAMEBUFFER OBJECT, and Target says - // whether it ALSO moves a binding. - // - // A `Named` record describes the framebuffer object it names (Fbo) and changes NO - // binding. `Draw` / `Read` / `Both` records describe the same object AND set the bound - // handle(s) of the target(s) they name. - // - // The applier therefore keeps records PER FRAMEBUFFER OBJECT, keyed by the handle's slot - // (the generation is checked on lookup and a stale one refuses; a framebuffer has no wire - // lifetime - D-I2, the catalogue has no framebuffer delete - so a successor's record - // simply OVERWRITES the slot), plus the two bound handles. And every DSA entry point that - // hands a framebuffer to the server BY NAME - BlitNamedFramebuffer, the four - // ClearNamedFramebuffer*, and the DSA attachment / draw-buffer / read-buffer setters at - // their validate point - is PRECEDED BY A Named RECORD, so that any framebuffer the - // server is about to receive by name already has one. - // - // That last rule is the phase's main correction, not a nicety. With only the two - // bound-target records, glClearNamedFramebufferfv(fbo) on an unbound fbo made the backend - // mint a fresh driver framebuffer with NO ATTACHMENTS, find no record for it, decline, - // and then issue the clear against it anyway - GL_INVALID_FRAMEBUFFER_OPERATION and - // nothing cleared, where the legacy arm cleared correctly. Writing such an object into - // the bound-target record instead would have been worse: the applier would then claim it - // is bound. - // - // GL has two independent framebuffer bindings and one record carries one Fbo, so a - // Draw/Read pair is two records and one object bound to both targets is one record with - // Both. The draw-buffer array belongs to the OBJECT the record names; it reaches the - // driver's bound draw framebuffer only for a record whose Target is Draw or Both - - // Espryt's own comment records the Minecraft 26.x OIT bug where a READ-only sync landed - // glDrawBuffers on the wrong framebuffer - and a Named record's draw buffers are applied - // when that object is next configured, never to whatever happens to be bound. ReadSurface - // is resolved from THAT framebuffer's own read buffer in EVERY record, Named included, - // which is what makes the read-buffer-shared-FBO defect class unrepresentable rather than - // merely fixed. - // - // Target IS A ContentHash INPUT (the hash covers the whole record), and the emitter's - // suppressor must be keyed by the framebuffer the record names, not by one global slot: - // two different objects' Named records in a row must both go out, and a Named record must - // never be suppressed against the same object's bound record or the reverse. + // Every framebuffer record describes the object named by Fbo; Target says whether it also + // moves a binding. Draw/Read/Both set the named binding(s), Named sets none. GL has two + // independent bindings, so a draw/read pair is two records and one object bound to both is + // one Both record. The applier stores records per framebuffer object (Fbo's slot, generation + // checked; a successor overwrites the slot) plus the two bound handles. Every DSA entry point + // that hands a framebuffer over by name is preceded by a record for it (Named when the object + // is bound to neither binding); without one the backend would meet an object with no record + // and act on an empty driver framebuffer. enum class MGPipeFramebufferTarget : Uint8 { Draw = 0, Read = 1, @@ -813,37 +726,13 @@ namespace MobileGL::MG_Pipe { static_assert(kMGPipeMaxColorAttachments == MobileGL::kMGMaxDrawBuffers, "MGPFramebufferState::Color[] and DrawBuffers[] are one array width"); - // ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by Fbo - and Target says - // whether it also moves a binding (P4a, D-C2 as corrected by ID-19; see - // MGPipeFramebufferTarget above for the failure that forced it). - // - // A Named record describes that object and changes NO binding. Draw / Read / Both records - // describe that object AND set the bound handle(s) of the target(s) they name. The applier - // keeps these records PER FRAMEBUFFER OBJECT, keyed by Fbo's slot (generation checked on - // lookup; a framebuffer has no wire lifetime - D-I2 - so a successor's record simply - // overwrites the slot), plus the two bound handles; every DSA entry point that hands a - // framebuffer to the server by name is preceded by a Named record. - // - // WHAT Target CHANGES, FIELD BY FIELD. NO FIELD IN THIS RECORD REFERS TO "the currently - // bound framebuffer" - every one of them describes the object named by Fbo - and that is - // the invariant a reader depends on: - // - // Fbo, Color[], Depth, Stencil, ReadSurface, Width/Height/Layers/Samples, - // FixedSampleLocations, IsDefault, Complete - // properties of the object named by Fbo, identical in meaning under every Target. - // In particular ReadSurface is resolved from THAT framebuffer's own read buffer - - // on a Named record too - never from whichever framebuffer is bound to GL_READ. - // DrawBuffers[] - // a property of the named object; it reaches the driver's bound draw framebuffer - // only when Target is Draw or Both. Under Named it is stored with the object and - // applied when that object is next configured. - // Target - // the only binding-specific field: Draw/Read/Both name the binding(s) this record - // also sets, Named names none. It is a ContentHash input. - // ContentHash - // per RECORD, not per object, and the emitter's suppressor is keyed by the - // framebuffer named: a Named record must never be suppressed against the same - // object's bound record, nor one object's Named record against another's. + // No field here refers to "the currently bound framebuffer"; all describe the object named + // by Fbo (see MGPipeFramebufferTarget). ReadSurface is that object's own read buffer under + // every Target. DrawBuffers[] reaches the driver's bound draw framebuffer only for Draw or + // Both; under Named it is stored and applied when the object is next configured. Target is + // the only binding-specific field and a ContentHash input. The emitter's suppressor is keyed + // per named framebuffer, so a Named record is never suppressed against the same object's + // bound record or against another object's Named record. struct MGPFramebufferState { MGPipeHandle Fbo; // kMGPipeDefaultFramebuffer for the default framebuffer MGPSurface Color[8]; @@ -1096,25 +985,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPStreamOutputTargets, 24); - // ---- set_program_bindings (P5e, opcode 80; CONTRACT-P5E.md §1 and §5.5) -------------- - // - // THE THREE POST-LINK MUTABLE REFLECTION FIELDS. create_shader_state carries the archive - // as LINKED; glUniformBlockBinding, glUniform1i on a sampler and glShaderStorageBlockBinding - // all move a binding AFTER the link, inside the frontend's LinkArtifacts, so an archive - // snapshot alone is wrong and the server would answer a draw with the bindings the program - // was linked with rather than the ones it is bound with. - // - // Three tails, in this order, because the three live in three different index spaces: - // Int32 BlockBindings[BlockBindingCount] dense, GL uniform-block index - // MGPProgramSamplerUnit SamplerUnits[SamplerUnitCount] sparse, ascending Location - // MGPProgramStorageOverride StorageOverrides[StorageOverrideCount] - // The storage-override map is NAME-keyed BY DESIGN (ProgramObject.h: three index spaces, - // one coordinate), so its key travels as a host span staged whole, exactly as - // set_storage_block_binding's single name does. - // - // Emitted BEFORE create_shader_state at the same validate point: a rebuild inside the verb - // must already see the bindings. A re-issued create CLEARS all three tails, for the reason - // GlobalConstants is cleared - a relink replaces the archive the tails index into. + // ---- set_program_bindings ------------------------------------------------------------ + // glUniformBlockBinding, glUniform1i on a sampler and glShaderStorageBlockBinding change + // bindings after link, and create_shader_state carries only the linked archive. Three tails, + // in this order, one per index space: Int32 BlockBindings[] (dense block index), + // MGPProgramSamplerUnit SamplerUnits[] (ascending Location), MGPProgramStorageOverride + // StorageOverrides[] (keyed by block name, staged whole). Emitted before create_shader_state + // at the same validate point so a rebuild inside the verb sees them; a re-issued create + // clears all three, because a relink replaces the archive they index into. struct MGPProgramSamplerUnit { Uint32 Location; // the frontend uniform location Int32 Unit; // the texture image unit glUniform1i put there @@ -1278,50 +1156,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPSubRegion, 40); - // Carries the union box AND the region list so the SERVER picks the upload shape - the - // decision belongs on the side that pays the GPU cost. Mali prices texture upload by - // JOB COUNT: ~100 sprite rects against one union box measured +6 ms/frame. - // - // `Target` IS TWO FACTS IN ONE Uint16 (P4a, D-D3 / ID-12), and MGPipePackSubDataTarget - // under the struct is the only spelling of the encoding - nothing may open-code a half: - // - // low byte = MGPipeResourceTarget - WHICH KIND of storage the destination is. - // The applier branches on it: a buffer - // target dispatches into MGPipeResourceOps, - // every other target accumulates a pending - // upload for the texture sync to consume. - // high byte = MobileGL::TextureUploadTarget - WHICH cube face / upload target the level - // belongs to. It is NOT derivable from the - // resource target - six faces share TexCube - // - and 26 enumerators leave a byte ample. - // - // WHY THAT WAY ROUND, AND WHY THE ENCODING LIVES HERE RATHER THAN IN EACH EMITTER. The - // applier's SubDataNamesABuffer tests the WHOLE field == 0, and - // TextureUploadTarget::Texture1D is 0 - so a texture record carrying the bare upload - // enumerator is indistinguishable from a buffer record exactly when its owner is a 1D - // texture, and that texture's upload is dispatched into the buffer path. With the - // resource target in the LOW byte a buffer record's Target stays EXACTLY - // kMGPipeResourceTargetBuffer - P3a's buffer records are unchanged on the wire, their - // upload byte being zero too - while a texture record can never be zero, because no - // texture's MGPipeResourceTarget is. The static_assert under the struct holds that - // invariant, and the applier's whole-field test stays right either way. - // - // THE BUFFER HALF. With Target == Buffer there is no level and no box, so the destination - // byte range rides in the box's first coordinate and first extent: UnionBox.X is the byte - // offset, UnionBox.W the byte size, Y = Z = 0, H = D = 1, Level = 0, RegionCount = 0. - // - // THE BLOB RULE, THE SAME ONE MGPVertexElements ABOVE CARRIES: `Blob.Size` is the record's - // own statement of how many bytes its blob holds and the applier holds the record to that - // statement whenever the record makes it - a non-zero Blob.Size that is not exactly the - // record's own byte size (MGPipeSubDataBufferSize below) is Fatal{ProtocolCorruption} and - // the write is refused. A zero Blob.Size means "this record does not declare its blob", - // which is what a monolith emission is: the bytes travel beside the record through - // MGPipeApplyResourceSubData's companion `const void*`. The destination range is what - // bounds the write in both cases. That caps ONE record at a 2^31-1 offset and a - // 2^32-1 size; a range beyond either is split by the emitter - the same rule, and at - // SEG_STAGE's 32 MiB the far tighter one, that the ring's half-capacity bound already - // imposes on it. MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size below are - // the only spelling of this convention; nothing else reads the box for a buffer. + // Carries the union box and the region list so the server picks the upload shape + // (ARCHITECTURE.md §6). Target packs MGPipeResourceTarget in the low byte and the + // TextureUploadTarget (the cube face) in the high byte; use MGPipePackSubDataTarget and the + // accessors below. That order keeps a buffer record's Target exactly + // kMGPipeResourceTargetBuffer (SubDataNamesABuffer tests the whole field) and no texture + // record 0. For a buffer, UnionBox.X/W are the byte offset/size (MGPipeSetSubDataBufferRange); + // one record holds at most a 2^31-1 offset and a 2^32-1 size, and the emitter splits beyond. + // Blob.Size follows the blob rule at MGPVertexElements: zero or exactly that byte size. struct MGPSubData { MGPipeHandle Res; // Target is PACKED - see the block above, and read it only through @@ -1330,25 +1172,12 @@ namespace MobileGL::MG_Pipe { // Replaces the backend's `uploadData == mipData` pointer comparison: are these // bytes an untransformed level shadow? Uint8 SourceIsVerbatimLevelShadow; - // P5 (b1): DOES THIS RESOURCE HAVE A LIVE HOST WRITER RIGHT NOW? One byte out of the - // original pad; the complete mip extent below is a separate payload extension. - // - // It is here rather than on MGPResourceDesc, and that is a ruling with a reason. The - // fact is CONTENT-shaped - "someone may be writing these bytes without telling you" - - // and MGPResourceDesc's only carrier is resource_respecify, which for a BUFFER is - // never classified as metadata-only (PipeApply.cpp's RespecifyRedefinesNoStorage - // refuses the buffer target outright, so that ID-18 M4's "no reallocation ack" is true - // by construction). Announcing a map on a descriptor would therefore have cost a - // spurious reallocation per map - precisely the failure the brief warned about - or a - // change to that rule, which is a P4a contract the buffer family should not be - // re-opening for a flag. - // - // WHO SETS IT: MGPipeEmitResourceSubData, from BufferObject::HasLiveHostWritesForWire. - // WHO READS IT: ApplyBufferWrite, into MGPipeResourceRecord::HasLiveHostWrites, which - // is what IsBufferDrawCleanByHandle asks instead of the frontend object's IsMapped(). - // ZERO IS THE ANSWER, NOT THE ABSENCE OF ONE: a record that does not set it says "no - // live writer", which is what every monolith emission means and why the monolith path - // needs no edit at all. + // Whether the buffer has a live host writer (a persistent write map) right now. It rides + // the content record, not MGPResourceDesc: a buffer resource_respecify is never + // metadata-only, so announcing a map there would reallocate. Every buffer content emitter + // in PipeFill.cpp sets it from BufferObject::HasLiveHostWritesForWire, and ApplyBufferWrite + // assigns it to MGPipeResourceRecord::HasLiveHostWrites, so zero means "no live writer". + // IsBufferDrawCleanByHandle reads that record instead of the frontend's IsMapped(). Uint8 HasLiveHostWrites; Uint8 Pad0[2]; MGPBox UnionBox; @@ -1411,75 +1240,24 @@ namespace MobileGL::MG_Pipe { } inline Uint64 MGPipeSubDataBufferSize(const MGPSubData& record) { return record.UnionBox.W; } - // P3a, D-A5: the per-record half of resource_respecify's kNeedsAck. - // - // Flags are a PER-CALL static property and resource_respecify serves BOTH glBufferData - // and glBufferStorage. A bare kNeedsAck on the call would acknowledge every glBufferData - // in a world upload; only glBufferStorage is a real synchronous allocation and only it is - // allowed a synchronous ack. So kNeedsAck on the call means "records of this call MAY - // require an acknowledgement" and THIS predicate decides per record. In monolith the ack - // is ((void)0) - the applier is one function call away - and the transport wires the - // doorbell to this predicate when it lands. - // - // P4a, D-A2: THE PREDICATE IS NARROWED TO NAME THE BUFFER TARGET, and that is a - // requirement rather than a tidy-up. glTexStorage* also sets Immutable - it is a real - // descriptor fact the backend reads, and the client must set it - but texture allocation - // is already deferred to sync time in monolith (glTexImage*/glTexStorage* only - // MarkStorageDirty; even glRenderbufferStorage* allocates lazily inside SyncToBackend), so - // splitting changes no observable behaviour and this batch must NOT ack. glBufferStorage - // stays the only entry point allowed a synchronous acknowledgement. - // - // PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage drives glTexStorage2D and - // glRenderbufferStorage idioms through it, and is the negative control for a future - // widening. + // The per-record half of resource_respecify's kNeedsAck: the call flag says a record may + // need an acknowledgement, and this decides. Only an immutable buffer (glBufferStorage) is a + // synchronous allocation; glBufferData is not. glTexStorage* also sets Immutable, but texture + // and renderbuffer storage is allocated lazily at sync time, so neither may ack + // (ARCHITECTURE.md §8.3). PipeCatalogue.ResourceRespecifyAcksOnlyImmutableStorage + // (PipeCatalogueTest.cpp) is the negative control against widening it. inline Bool MGPipeResourceRespecifyNeedsAck(const MGPResourceDesc& desc) { return desc.Immutable != 0 && desc.Target == kMGPipeResourceTargetBuffer; } - // P4a, ID-18 M4: A RESPECIFY WHOSE STORAGE-DEFINING FIELDS ALL EQUAL THE STORED - // DESCRIPTOR IS A METADATA UPDATE, NOT A REALLOCATION. - // - // MGPResourceDesc::BindMask and ImageBindableHint are STICKY facts the client discovers - // AFTER allocation - a texture first bound as a shader image, first used as a render - // target - and they ride resource_create and every resource_respecify. An IMMUTABLE - // texture never has a later respecify, so without a rule those two would reach the server - // only by accident, or never; with one, a mask change after allocation emits a - // resource_respecify that REPEATS the storage the resource already has. - // - // The applier and both twins must classify such a record as a metadata update: - // - NO reallocation acknowledgement. MGPipeResourceRespecifyNeedsAck above still - // answers the per-record question, but a metadata update allocates nothing, so a - // record it classifies as metadata is not acked even when that predicate says the - // call may require one. - // - NO PendingUploads clear when the call names NO level. This REFINES the whole-resource - // clear: identical storage fields with a null MGPRespecifiedLevel clear NOTHING. (The - // level-scoped rule exists because clearing the whole vector on a level-1 definition - // silently dropped level 0's accepted texels; a metadata update must drop neither.) A - // call that NAMES a level is that level's redefinition whatever the descriptor says - - // a non-base level's extent is not a descriptor field - and drops exactly that level - // (P4a final review C-1); the client's mask republish passes null on purpose. - // - The stored descriptor's BindMask and ImageBindableHint ARE updated: the applier - // replaces the descriptor WHOLE with the one the client sent (PipeApply.cpp), and the - // mask in it is the CLIENT's sticky OR (TextureEmit.h's entry, never cleared), so the - // replacement can never lose a bit the record once carried. The twin re-derives its - // storage flags from the new mask on its next sync, recreating backend storage only - // where the backend actually needs it. The record itself is not a request to recreate. - // - // THE STORAGE-DEFINING FIELD SET, named here so that neither side has to guess and a - // later field cannot join it by silence. It is every MGPResourceDesc member except the - // three metadata ones and the padding: - // - // Target, StorageKind, InternalFormat, Width, Height, Depth, ArrayLayers, Levels, - // Samples, FixedSampleLocations, Immutable, Usage, StorageFlags, HasDefinedContent, - // ViewOf, BufferForTexBuffer, BufOffset, BufSize. - // - // `Resource` is the identity the stored descriptor is looked up BY, not a comparand. The - // three fields that may differ on a metadata update are exactly BindMask, - // ImageBindableHint and GlNameForDiag (diagnostics only, never an identity, never a memo - // key). HasDefinedContent is storage-defining ON PURPOSE: glBufferData(size, NULL) at an - // unchanged size is an orphaning reallocation and has to keep clearing, rather than being - // mistaken for a mask change. A field added to MGPResourceDesc must be placed in one of - // the two lists in the same commit - PipeCatalogue pins the struct's size for that. + // A non-buffer respecify whose storage-defining fields all equal the stored descriptor is a + // metadata update: it republishes a BindMask or ImageBindableHint learned after allocation + // (an immutable texture has no later respecify to carry them). It gets no reallocation ack + // and, naming no level, clears no PendingUploads; the descriptor is still replaced whole (the + // mask is the client's sticky OR) and the twin re-derives its storage flags at its next sync. + // Storage-defining is every MGPResourceDesc member but Resource, BindMask, ImageBindableHint, + // GlNameForDiag, the respecify scope and padding. RespecifyRedefinesNoStorage (PipeApply.cpp) + // compares them field by field, so a new member must be added there. // The forward terminator for a server-initiated texture pull (section 7.1). May carry // zero regions - that is how a pull that needs nothing is answered. @@ -1505,29 +1283,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPReadback, 24); - // resource_copy_region = glCopyImageSubData (P5b, CONTRACT-P5B.md i1). The record had no - // producer and no consumer before P5b, so its shape is settled here for the first time: - // - // Src / Dst the two endpoints' handles - kind Texture, or kind Renderbuffer when the - // matching *Target is GL_RENDERBUFFER. The handle is the identity. - // SrcTarget / the GL target enum the APPLICATION named, verbatim (GL_TEXTURE_2D, - // DstTarget GL_TEXTURE_CUBE_MAP, GL_RENDERBUFFER, ...); every one fits a Uint16. - // Espryt's own MakeGLESCopyImageEndpoint does the ES lowering - // (DirectGLES.cpp) and must keep seeing the enum the app passed. - // SrcBox {SrcX, SrcY, SrcZ, Width, Height, Depth} of the copied region. - // SrcGlName / THE P5b DEBT, stated rather than hidden. GLFunctionsTable:: - // DstGlName CopyImageSubData takes two CopyImageEndpoints, each a frontend SharedPtr, - // and the server has none. Under inproc and the verb barrier the server - // rebuilds a TEXTURE endpoint from the GL name through the barrier-pulled - // sticky forward gPipeInputs->GetTextureObject(name) (counted in `rsp`, - // retired by P7 / P3b-P4b when the backend takes handles). A GL name is - // NEVER an identity (section 4.2.1); it is the lookup key of a pull the - // handle beside it will replace. No forward hands out a renderbuffer, - // so a RENDERBUFFER endpoint is refused by name in P5b - // (Fatal{UnmigratedVerb, "CopyImageSubData+RENDERBUFFER"}, ID-57's shape). - // - // 64 -> 72: the two names did not fit the four pad bytes, and the record had no wire history - // to preserve. + // resource_copy_region = glCopyImageSubData only; glCopyTex(Sub)Image2D use + // copy_framebuffer_to_texture. + // Src / Dst endpoint handles: kind Renderbuffer when the matching *Target + // is GL_RENDERBUFFER, else Texture. The handle is the identity. + // SrcTarget / DstTarget the GL target the application passed, verbatim: the backend's + // CopyImageSubData does the ES lowering and needs the original enum. + // SrcBox source origin and the copy's extent, which GL states once for both. + // SrcGlName / DstGlName diagnostics only, never an identity. struct MGPCopyRegion { MGPipeHandle Src, Dst; MGPBox SrcBox; @@ -1562,26 +1325,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPClear, 48); - // MGPClear's two discriminants, and THE ONLY SPELLING OF THEM (P5b, CONTRACT-P5B.md f1). - // Through P5 the list lived in this struct's comment and both the client emitter - // (MG_Remote/Client/EmitTables.h) and the server sink (MG_Remote/Server/PipeApplier.h) - // minted their own copy of the numbers, each flagged "this belongs in MGPipeTypes.h". A - // disagreement between the two copies is a clear of the wrong attachment with the wrong - // value type, which renders plausibly - table 0's "a decoder that open-codes it is the - // class-1 defect". Both files now alias these. - // - // Kind is the comment's order, left to right. ValueClass reuses the numbering - // MG_State/GLState/Core.h gives the identical three-way split on MGPAttribValue::ValueClass. - // - // WHICH GL ENTRY POINT PRODUCES WHICH KIND, so the emitter and the sink cannot disagree: - // glClear(mask) Whole, BufferMask = mask, DrawBufferIndex = -1 - // glClearBuffer{f,i,ui}v(GL_COLOR, i, v) Color, DrawBufferIndex = i, ValueClass by suffix - // glClearBufferfv(GL_DEPTH, 0, &d) Depth, DepthValue = d - // glClearBufferiv(GL_STENCIL, 0, &s) Stencil, StencilValue = s - // glClearBufferfi(GL_DEPTH_STENCIL, 0, d, s) DepthStencil, DepthValue = d, StencilValue = s - // The four glClearNamedFramebuffer* forms are the same rows with Fbo = the named - // framebuffer's handle (preceded by its MGPipeFramebufferTarget::Named record, ID-19); - // the bound forms carry Fbo = kMGPipeNullHandle, "the bound draw framebuffer". + // MGPClear's discriminants, shared by the emitter and the sink: a mismatch clears the wrong + // attachment with the wrong value type and still renders plausibly. ValueClass reuses the + // numbering of MGPAttribValue::ValueClass (MG_State/GLState/Core.h). + // glClear(mask) Whole, BufferMask = mask, DrawBufferIndex = -1 + // glClearBuffer{f,i,ui}v(GL_COLOR, i, v) Color, DrawBufferIndex = i, ValueClass by suffix + // glClearBufferfv/iv/fi on GL_DEPTH, GL_STENCIL, GL_DEPTH_STENCIL give Depth, Stencil and + // DepthStencil with DepthValue / StencilValue. Named-framebuffer forms set Fbo (after a + // framebuffer record for it); bound forms use kMGPipeNullHandle, the bound draw framebuffer. inline constexpr Uint32 kMGPipeClearKindWhole = 0; inline constexpr Uint32 kMGPipeClearKindColor = 1; inline constexpr Uint32 kMGPipeClearKindDepth = 2; @@ -1624,29 +1375,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPReadbackInfo, 64); - // P9 (W1, docs/archive/contracts/CONTRACT-P9.md §1): read_pixels_to_buffer / get_texture_image_to_buffer, - // the two readbacks whose destination is a bound GL_PIXEL_PACK_BUFFER. The destination is - // the pack buffer's RESOURCE HANDLE (CONTRACT-P5 row 23: "a PBO destination is a resource - // handle and needs no Seg field"), so nothing comes back and the client does not wait: the - // server reads exactly the tight pixels the reply-slot form would have answered and writes - // them into Dst row by row through the applier's ordinary buffer-write gate, and the client - // marks Dst GPU-written (GpuWritePending.h row 4). A later CPU read of Dst reaches the bytes - // through resource_readback, like any other buffer the GPU wrote. - // - // Src the reply form's record, unchanged: the box, format, type, target and - // level; Src.DstOffset / Src.DstSize are 0 / the tight w*h*d*bpp extent - // and are checked, not used for placement. - // Dst the pack buffer. - // DstBase byte offset in Dst of the first texel of the first row of the first image: - // the application's offset plus GL_PACK_SKIP_{PIXELS,ROWS,IMAGES} already - // applied, because the layout arithmetic is the client's (ReadbackLayout). - // RowStride bytes between two rows in Dst (GL_PACK_ROW_LENGTH and GL_PACK_ALIGNMENT - // applied). Never smaller than a tight row. - // ImageStride bytes between two images in Dst (GL_PACK_IMAGE_HEIGHT applied). Only read - // when Src.Box.D > 1. - // SwapGroup GL_PACK_SWAP_BYTES as the width of the byte group to reverse (2, 4 or 8), - // 0 for no swap - the client's ApplyReadbackByteSwap rule, precomputed there so - // the server does not re-derive the type table. + // read_pixels_to_buffer / get_texture_image_to_buffer read into the bound pack buffer, named + // by its handle Dst, so nothing comes back and the client does not wait: the server writes the + // tight pixels into Dst row by row through the applier's buffer-write gate, and the client + // marks Dst GPU-written so a later CPU read goes through resource_readback. Src is the reply + // form's record (DstOffset/DstSize = 0 / tight extent, checked only). DstBase includes the + // application offset and GL_PACK_SKIP_*; RowStride (never below a tight row) and ImageStride + // (read only when Src.Box.D > 1) apply row length, alignment and image height; SwapGroup is + // GL_PACK_SWAP_BYTES as the byte-group width to reverse (2, 4 or 8; 0 = no swap). struct MGPReadbackToBuffer { MGPReadbackInfo Src; MGPipeHandle Dst; @@ -1720,22 +1456,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPDrawRange, 12); - // Present when the draw is indirect: draw_vbo's second tail under kDrawIsIndirect (P5b, - // CONTRACT-P5B.md d1), 8-aligned behind the (empty) range tail exactly where the user-index - // span would sit. The client resolves the COUNT itself, so the server never reads an - // indirect command block to learn how many draws there are: - // glMultiDraw*Indirect DrawCount = drawcount, ParameterBuffer = null - // glMultiDraw*IndirectCount DrawCount = maxdrawcount, ParameterBuffer = the bound - // GL_PARAMETER_BUFFER, ParameterOffset = drawcount (the byte - // offset the GL call spells as GLintptr drawcount). The server's - // backend slot takes exactly that pair and reads the real count - // where it does today; nothing about the parameter buffer's - // CONTENT crosses. (ROADMAP P8's HostResolve.cpp is where a - // client-side count resolve would live if a backend ever needed - // the number rather than the buffer; no backend does.) - // Buffer / Offset / Stride the bound GL_DRAW_INDIRECT_BUFFER handle, the command byte - // offset the GL call passed as `indirect`, and the stride (0 = - // tightly packed, as GL spells it - the backend normalises). + // draw_vbo's second tail under kDrawIsIndirect, 8-aligned where the user-index span would + // sit. The client resolves the draw count, so the server never reads a command block for it: + // glMultiDraw*Indirect DrawCount = drawcount, ParameterBuffer = null + // glMultiDraw*IndirectCount DrawCount = maxdrawcount, ParameterBuffer = the bound + // GL_PARAMETER_BUFFER, ParameterOffset = drawcount (a byte + // offset); the backend reads the real count from that buffer + // Buffer / Offset / Stride the bound GL_DRAW_INDIRECT_BUFFER, the `indirect` byte offset + // and the stride (0 = tightly packed; the backend normalises). struct MGPDrawIndirect { MGPipeHandle Buffer; MGPipeHandle ParameterBuffer; @@ -1819,25 +1547,12 @@ namespace MobileGL::MG_Pipe { MGP_ASSERT_POD(MGPSwapInterval, 8); // --------------------------------------------------------------------------------- - // P5b: the five appended rows (docs/archive/contracts/CONTRACT-P5B.md). APPENDED, never inserted: - // the wire opcode is a call's position in PipeCalls.def, so these sit after - // FenceWaitServer at opcodes 72..76 and no earlier opcode moved. - // - // Every one of them is a GLFunctionsTable VERB the class-C census measured as a first - // blocker and for which no existing row could carry the call. Four of the five exist - // because the backend slot does REAL WORK AT THE CALL rather than at the next validate, - // so a validate-time set record (set_shader_images, set_patch_state) cannot stand in for - // it: Espryt's BindImageTexture syncs the unit's binding to the driver on the spot, - // PatchParameteri pushes glPatchParameteri, BindTransformFeedback rebinds the driver - // object and invalidates the capture-binding shadows, and ShaderStorageBlockBinding - // pushes a rebinding onto an already-built driver program. The fifth, - // copy_framebuffer_to_texture, is a copy WITH the read framebuffer as its source, which - // resource_copy_region (resource -> resource) cannot express. - // - // NONE OF THEM HAS AN MGPipeApply* ENTRY POINT AND NONE GAINS ONE. They reach - // MG_Remote::Wire::WireVerbSink like the five P5 class-B verbs (the census's correction: - // a verb reaches the sink, not an applier), and under monolith they have no producer at - // all - the backend's own slot is called directly, byte for byte as today (G2/G14). + // Verbs whose backend slot acts at the call. bind_shader_image, patch_parameter, + // bind_stream_output and set_storage_block_binding cannot be validate-time set_* records, + // because the backend rebinds or pushes to the driver at the call itself; + // copy_framebuffer_to_texture copies from the read framebuffer, which resource_copy_region + // (resource to resource) cannot express. None has an MGPipeApply* entry point: each reaches + // the MGPipeVerbSink. // --------------------------------------------------------------------------------- // bind_shader_image = glBindImageTexture (P5b i1). The arguments VERBATIM plus the handle: @@ -1909,23 +1624,14 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPStorageBlockBinding, 40); - // copy_framebuffer_to_texture = glCopyTexImage2D / glCopyTexSubImage2D (P5b f1). The - // source is the READ framebuffer the server already has bound (set_framebuffer_state), so - // no source handle crosses; the destination is the texture bound to the active unit at - // Target, which the backend slot resolves through the barrier-pulled unit state exactly as - // generate_mipmap does. glCopyTexImage2D (SubImage = 0) also REDEFINES the level, and the - // frontend's own TexImage2D_State has already emitted that level's resource_respecify - // (GL_Texture.cpp's CopyTexImage2D validator) before the backend slot is reached, so the - // record carries the copy and nothing about storage. - // Dst the destination texture's handle (P8 form; the P5b sink does not - // dispatch on it). - // Target the GL texture target enum verbatim (a 2D target or a cube face). - // Level the destination level. - // InternalFormat glCopyTexImage2D's internalformat (GL enum); 0 for the sub-image form. - // X, Y, Width, Height the read-framebuffer rectangle. - // XOffset, YOffset the sub-image form's destination origin; 0 for the image form. - // SubImage 0 = glCopyTexImage2D, 1 = glCopyTexSubImage2D (unmeasured; shares - // the row because the two calls differ in exactly these two fields). + // copy_framebuffer_to_texture = glCopyTexImage2D / glCopyTexSubImage2D. The source is the + // read framebuffer the server already has bound, so no source handle crosses; for + // glCopyTexImage2D the frontend has already emitted the level's resource_respecify. + // Dst the destination texture, resolved by the client at the active unit + // Target, Level the GL target verbatim (a 2D target or a cube face) and the level + // InternalFormat glCopyTexImage2D's internalformat; 0 for the sub-image form + // XOffset, YOffset the sub-image destination origin; 0 for the image form + // X, Y, Width, Height the read-framebuffer rectangle; SubImage 1 = glCopyTexSubImage2D struct MGPCopyFromFramebuffer { MGPipeHandle Dst; Uint32 Target; @@ -1981,47 +1687,13 @@ namespace MobileGL::MG_Pipe { }; MGP_ASSERT_POD(MGPBindContext, 8); - // shared_image = opcode 85 (docs/archive/notes/anland/plan-ahb-dmabuf.md). Colour - // buffers the SERVER allocates and exports as dma-buf descriptors, so one client's frames - // reach another client's textures without leaving GPU memory. One row, five operations, all - // answered through the reply slot with an MGPSharedImageReply: - // - // Allocate a new Width x Height Format image; the reply names it, and its descriptor - // reaches the client on the aux socket BEFORE the reply is posted. - // Import the descriptor the client queued on the aux socket (sideband: a - // MGPSharedImageFdOffer naming this record's seq) is identified as an image this - // server exported; the reply names it. Width/Height/Format must match it. - // Release the session's reference to ImageId goes. - // Present the current context's default framebuffer is copied, top row first, into - // ImageId (a window frame becoming a wl_buffer's contents); the reply is posted - // when the copy has completed on the GPU. - // Attach the texture Texture's storage becomes ImageId (glEGLImageTargetTexture2DOES); - // it lasts until the texture's next storage-defining respecify. - // QueryBufferAge EGL_BUFFER_AGE_EXT of the current context's draw surface, answered in the - // reply's BufferAge: how many presents ago the buffer the next frame draws into held - // the frame then presented, 0 = unknown. Asked in the stream, so it is answered - // for the buffer that frame's records will draw into. Format carries - // kMGPBufferAgeDamageRegionFollows when the application declares the region it draws - // (EGL_KHR_partial_update) before it draws. - // SetDamageRegion eglSetDamageRegionKHR for the current draw surface: Damage is the region - // the frame is about to draw (GL window coordinates; 0 rectangles = all of it). - // Flush glFlush/glFinish of a session that renders INTO images (an X server's glamor): - // every image the session used since its last boundary is published as written - // and read by the flush's fence (implicit sync); answered once published. - // NativeFence EGL_ANDROID_native_fence_sync's fence command: a sync_file that signals - // once everything the session submitted so far has completed (and, in a session - // that writes images, what Flush does) - // reaches the client on the aux socket BEFORE the reply is posted, with the reply's - // Format = kMGPNativeFenceFdFollows. Format = 0: the server has no descriptor to give - // (it cannot export fences, and none it gave before can stand in), and the work has - // completed by the time the reply is posted. - // - // A Present carries the region of the frame to copy (DamageCount rectangles of Damage, GL - // window coordinates, origin bottom-left; 0 = all of it): the client keeps per image what the - // image missed since it was last written, and sends that, so the rest of the image is already - // the frame. - // - // ImageId is a server-minted id, never reused within the server's life. + // shared_image: colour buffers the server allocates and exports as dma-buf descriptors, so + // one client's frames reach another client's textures without leaving GPU memory. Every op + // answers through the reply slot with an MGPSharedImageReply; a descriptor (Allocate, + // NativeFence) reaches the client on the aux socket before the reply is posted. Damage holds + // GL window-coordinate rectangles, origin bottom-left, 0 rectangles = the whole surface. + // ImageId is server-minted and never reused within the server's life. Per-op rules: + // ARCHITECTURE.md §15.4. enum MGPSharedImageOpKind : Uint32 { kMGPSharedImageAllocate = 1, kMGPSharedImageImport = 2, diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 86234187..c029dd5d 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -483,21 +483,11 @@ namespace MobileGL::MG_Pipe { } // ---------------------------------------------------------------------------- - // D5: the 29 PipeInputs fields that are PURE FUNCTIONS of RenderStateParameters. - // - // Once bind_render_state / set_dynamic_state have assembled the working block, - // copying these out of GLContext a second time would be exactly the per-verb pull - // P2 exists to remove - so the applier DERIVES them instead. Each line below is a - // transcription of the RenderState getter of the same name (RenderState.cpp); - // GLContext's accessors are one-line forwards to those, so this block and the pull - // path answer the same question from the same bytes. - // - // This departs from P1 brief D4's "no derivation logic is re-implemented in - // PipeInputs", deliberately and with a guard: MOBILEGL_PIPE_VERIFY's compare-at-read - // re-reads every one of these from the live context AT EVERY BACKEND READ and - // compares field-wise, so a transcription error is caught on the first draw that - // reads it. RenderStateSpansTest.DerivationMatchesTheFrontendGetters walks every - // setter and checks all 29 against GLContext on top of that. + // The PipeInputs fields that are pure functions of RenderStateParameters. The applier + // derives them from the assembled working block instead of copying them from GLContext: + // each line transcribes the RenderState getter of the same name (RenderState.cpp), which + // GLContext's accessors forward to. MOBILEGL_PIPE_VERIFY's compare-at-read and + // RenderStateSpans.DerivationMatchesTheFrontendGetters catch a transcription error. // ---------------------------------------------------------------------------- // RenderState::IsCapabilityEnabled, transcribed against the assembled block. One of @@ -1030,38 +1020,14 @@ namespace MobileGL::MG_Pipe { // ID-12 says it does, and c0c's own static_assert is what keeps the two readings in // step: a buffer record's Target is exactly kMGPipeResourceTargetBuffer. - // ID-18 M4: DOES THIS RESPECIFY REDEFINE ANY STORAGE, OR IS IT CARRYING METADATA? - // - // The question exists because a sticky BindMask / ImageBindableHint bit has exactly one - // way onto the wire - a respecify - and an IMMUTABLE texture has no further respecify to - // ride on. So B re-publishes the descriptor when the mask moves, and the applier has to - // tell that call apart from a real redefinition: the first must NOT drop the pending - // uploads standing against the storage (nothing replaced them), the second must. - // - // THE COMPARISON IS FIELD BY FIELD AND NOT A memcmp. MGPResourceDesc carries Pad0/Pad1 - // and neither side of this comparison is guaranteed to have written them - the client - // fills a stack descriptor field by field - so a byte comparison of 88 bytes is a coin - // flip on padding, which is the defect clientsp's SamplerParameters cache already paid - // for once. - // - // WHAT IS NOT COMPARED IS THE SHORT LIST AND IT IS THE DEFINITION: BindMask and - // ImageBindableHint (the two this call exists to carry), GlNameForDiag (diagnostics, and - // never an identity - section 4.2.1) and the two padding words. EVERYTHING ELSE IS - // COMPARED, which is deliberately WIDER than "the fields that define an allocation": - // being wide can only mis-classify a metadata update as a redefinition, whose cost is - // one level's pending upload dropped by a call B does not emit; being narrow would - // mis-classify a REDEFINITION as metadata and keep boxes in a coordinate system that no - // longer exists, which is an upload past the end of the new level. The asymmetry decides - // the direction. - // - // A BUFFER IS NEVER METADATA-ONLY, and that is a rule rather than a consequence. - // glBufferData at an unchanged size is the canonical ORPHANING idiom - a real - // reallocation whose whole purpose is that the old store is gone - and glBufferStorage - // is the one entry point in the catalogue allowed a synchronous acknowledgement - // (MGPipeResourceRespecifyNeedsAck). Excluding the target here is what makes ID-18 M4's - // "no reallocation ack" true by construction instead of by a second suppression rule, - // and it costs nothing: the mask that M4 is about is a TEXTURE's (and a renderbuffer's), - // and a buffer has no pending upload for the arm to protect anyway. + // True when a non-buffer respecify carries only metadata: every storage-defining field + // (the set listed beside MGPipeResourceRespecifyNeedsAck in MGPipeTypes.h) equals the + // stored descriptor. The client resends the descriptor when a sticky BindMask / + // ImageBindableHint bit moves (for an immutable texture the only way the bit reaches the + // server), and that call must not drop pending uploads. Compared field by field because + // padding is not reliably written. The set is wide on purpose: too wide can only drop + // uploads on a call the client never sends, too narrow keeps boxes for a level that no + // longer exists. A buffer is never metadata-only: same-size glBufferData orphans its store. Bool RespecifyRedefinesNoStorage(const MGPResourceDesc& stored, const MGPResourceDesc& next) { if (next.Target == kMGPipeResourceTargetBuffer) return false; if (stored.Target != next.Target) return false; @@ -1088,38 +1054,14 @@ namespace MobileGL::MG_Pipe { return true; } - // WHY A DEAD HANDLE IS NOT A TRIP WIRE HERE, and the bounds faults below are - and - // why it is nonetheless COUNTED rather than silently dropped. - // - // A make-current no longer takes the records with it (MGPipeApplierReset), so the - // shared-store case that used to arrive here every context switch does not arrive at - // all: a buffer that outlives a switch keeps its record and its writes keep landing. - // What is left is (a) a genuine protocol error - an unknown slot, a stale generation - - // and (b) ONE legal sequence, which is why this is still a no-op and not a wire: - // - // the served context is torn down - // -> MGPipeApplierReleaseObjectRecords() (the applier goes away with it) - // -> ~BufferObject / ~VertexArrayObject for every object the context still owns - // -> resource_destroy / delete_vertex_elements, each naming a record that the - // line above has already dropped. - // - // Every one of those death notices is legal, unavoidable and arrives after the - // records are gone, and a wire here would abort a verify lane on the ordinary shutdown - // of a context. So the refusal stays a DEFINED no-op - nothing stored, nothing - // dispatched, no serial moved, and the debug assertion names it, which is the shape - // bind_render_state already uses for a dead CSO. - // - // But MOBILEGL_ASSERT compiles out at INFO, which is what all three gate builds and - // every shipped build are, so a no-op alone would make case (a) - a dropped - // glBufferSubData - invisible in every build that matters. Both refusal paths - // therefore go through ResolveResource / ResolveVertexElements below, which COUNT into - // MGPipeApplierState::Refused{Resource,VertexInput}Calls. That is the observable: a - // legal sequence leaves it at 0 and a refused call moves it, in every build. - // - // The faults below are the other class entirely: a record that does not describe its - // own bytes would have the BACKEND read or write outside a store, which is memory - // corruption rather than a dropped call, so those get the tag ARCHITECTURE.md reserves - // for exactly this - Fatal{ProtocolCorruption} - and the record's identity in the line. + // A call naming a record this applier lacks is a defined no-op, not a trip wire, because + // one legal sequence produces it: context teardown runs MGPipeApplierReleaseObjectRecords, + // then the destructors of objects the context still owns send resource_destroy / + // delete_vertex_elements for records already dropped. The refusal stores, dispatches and + // moves nothing; MOBILEGL_ASSERT compiles out at INFO, so the resolvers below count it + // (MGPipeApplierState::Refused*Calls) and a dropped write stays visible in every build. A + // record that would make the backend touch memory outside a store is the other class: + // Fatal{ProtocolCorruption}, with the record's identity in the line. constexpr const char* kResourceRefusalNote = "the record is not this applier's; the call is dropped, not applied"; @@ -1299,22 +1241,14 @@ namespace MobileGL::MG_Pipe { static_cast(zEnd - z)}; } - // D-D5's SAFETY NET, and the whole reason it is server-side state. The client clears - // its own dirty flags AT EMISSION, for the levels whose record this applier accepted; - // Espryt's upload loop has bail arms - an incomplete texture returns early, a - // multisample target refreshes and skips - that today leave the frontend flag set, so a - // naive move of the clear to the client would lose exactly those texels. The emitted - // shape accumulates here instead, it survives any number of bails, and Espryt consumes - // and clears an entry only where it actually uploads. - // - // ACCUMULATION IS THE CLIENT'S OWN MODEL, ONE LEVEL UP. MipmapStorage keeps a union box - // and, behind it, a bounded disjoint rect list, and answers "0 rects" for everything it - // cannot describe that way - which means "upload the box instead" and covers every - // reason at once. So: boxes union; rect lists concatenate; and the moment either side - // says "box only", or the list would outgrow its bound, the entry becomes box only. - // Never a dropped region - the box still covers every texel the dropped list named. - // Returns false when the record names more distinct (target, level) keys than any - // texture can have, which the caller reports as a corrupt record. + // The server-side pending-upload set (ARCHITECTURE.md §6.1). The client clears a level's + // dirty flags when this applier accepts its record, but a backend's upload loop can bail + // (Espryt returns early for an incomplete texture and skips a multisample target), so the + // accepted shape accumulates here until the backend actually uploads it. Boxes union and + // rect lists concatenate; once either side is box-only or the list would exceed + // kMGPipeMaxPendingUploadRegions, the entry becomes box-only, which still covers every + // texel. Returns false when the record names more (target, level) keys than any texture + // can have; the caller reports that as a corrupt record. Bool AccumulatePendingUpload(MGPipeResourceRecord& stored, const MGPSubData& record, const MGPSubRegion* regions) { for (MGPipeResourceRecord::PendingUpload& entry : stored.PendingUploads) { @@ -1358,50 +1292,14 @@ namespace MobileGL::MG_Pipe { void PinNoLiveHostWrites(const MGPipeResourceRecord&, MGPipeHandle, const char*) {} #if MOBILEGL_PIPE_VERIFY - // P5's pin, RELAXED BY P7 WAVE 3 (CONTRACT-P7 §6) - and relaxed rather than deleted, - // because the thing it was guarding is now reachable instead of hypothetical. - // - // WHAT IT USED TO SAY. "The respecify SCOPE has a wire carrier (MGPResourceDesc's - // HasRespecifiedLevel + the pair) and NO PRODUCER: every descriptor P5 builds is - // whole-resource, and the per-level scope still arrives the old way, as the trailing - // MGPRespecifiedLevel* this function does not look at." So any descriptor arriving with - // the carrier set was, by definition, a producer nobody had announced, and a verify - // build refused it. - // - // WHY THAT IS NOW WRONG, AND EXACTLY HOW FAR. The producer landed: - // Wire_Escape_ResourceRespecify (Client/WireTables.cpp:425-433) writes the carrier from - // the pointer through MGPipeSetRespecifiedLevel, and the server codec - // (Wire/PipeWireCodec.cpp:1677-1683) rebuilds the pointer from the carrier. So under a - // split transport EVERY per-level glTexImage*D arrives here with the carrier set, and - // the old pin made verify x split unmeasurable - it fired on the server's apply thread - // during bring-up, before a single verify case could arm: - // - // Fatal{PipeRespecifyScope} resource_respecify {slot=12, gen=0}: the descriptor - // carries a per-level respecify scope (target=258, level=0), and no path in this - // phase may set one - // - // WHAT SURVIVES, AND IT IS THE WHOLE INVARIANT. The pin never cared that the carrier - // was set; it cared that THE TWO HALVES OF THE SCOPE COULD DISAGREE - "the first thing - // that can go wrong is exactly one of the two moving". That hazard did not go away when - // the producer landed, it became LIVE, and it is silent: the applier below drops pending - // uploads by the POINTER, so a descriptor that declares (target, level) while this call - // hands over a null pointer takes the WHOLE-RESOURCE arm and eats every other level's - // texels, and one that hands over a different pair erases the wrong key and keeps the - // one the client stopped owing. Neither shows up as anything but missing pixels, a - // frame later, on whichever level the wire dropped. - // - // So the rule becomes CONTRACT-P7 §6's: A PRODUCER OF A PER-LEVEL SCOPE MUST COVER THE - // RANGE IT DECLARES. Carrier set => the call passes a pointer, and that pointer names - // exactly the declared (uploadTarget, level). - // - // ONE-DIRECTIONAL ON PURPOSE. A whole-resource descriptor says NOTHING about the - // pointer, and must not: that is the monolith shape and it is still the majority of the - // calls in the tree. MG_Impl/Pipe/TextureEmit.h builds a value-initialized descriptor - // (carrier clear, by MGPipeClearRespecifiedLevel's own "the safe default") and states - // the scope in the trailing pointer alone, which is legal, load-bearing and untouched - // here. Checking that direction too would red every monolith glTexImage*D in the tree - // and would be asserting a convention, not an invariant - the pointer is the caller's - // statement, and a caller that has not been converted to the carrier has not lied. + // Verify builds: a per-level respecify scope declared in the descriptor must match the + // `level` pointer. The wire escape (MG_Remote/Client/WireTables.cpp) writes the carrier + // from the pointer and the codec rebuilds the pointer from it. The applier drops pending + // uploads by the pointer, so a declared scope with a null pointer would take the + // whole-resource arm and eat every other level's texels, and a different pair would drop + // the wrong key; both show up only as missing pixels. One-directional: in-process emission + // (MG_Impl/Pipe/TextureEmit.h) leaves the carrier clear and states the scope in the + // pointer alone, which is legal. void PinRespecifyScopeCoversItsDeclaration(const MGPResourceDesc& desc, const MGPRespecifiedLevel* level, MGPipeHandle res, const char* call) { @@ -1649,24 +1547,13 @@ namespace MobileGL::MG_Pipe { } // ---------------------------------------------------------------------------- - // P4a: the three kVarTail unit sets share one body. + // The body shared by the three var-tail unit sets, which differ only in the entry type. // - // They differ in exactly one thing - what an entry IS - and in nothing else: the same - // window rule, the same bound, the same "the entries outside the window are not - // cleared", the same serial discipline. set_vertex_buffers wrote this arithmetic once - // already; a second, third and fourth copy of it would be three more places to get the - // Start + Count overflow wrong. - // - // THE WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED. The record is "the - // last set as received": a set that names four units has said nothing about the other - // 188, and clearing them would unbind textures the client never mentioned. - // - // THE ENTRY'S OWN Unit FIELD IS NOT POLICED, deliberately and for MGPVertexBuffer:: - // BindingIndex's reason: the DESTINATION is Start + i, which is the window this - // function has already bounded, and the field beside it is the client's own label for - // the entry. Policing it would hand the emitter a contract this applier cannot justify - // - the two agree by construction or the emitter is broken in a way a unit test on the - // emitter's side is the right place to catch. + // The window [start, start + count) is bounded against the destination (in 64 bits), and + // entries outside it are not cleared: the record is the last set as received, and a set + // naming four units says nothing about the other 188. An entry's own Unit field is not + // checked; the destination is start + i and the field is only the client's label, as + // with MGPVertexBuffer::BindingIndex. template Bool ApplyUnitWindow(const char* call, Uint32 start, Uint32 count, Uint64 contentHash, const Entry* tail, ArrayT& destination, Uint32& destinationStart, @@ -1794,24 +1681,14 @@ namespace MobileGL::MG_Pipe { const MGPipeResourceOps* MGPipeGetResourceOps() { return g_resourceOps; } namespace { - // P4a's BELT (ID-39). See MGPipeApplierState::RefusedNoConsumer for the whole argument; - // in one line: acceptance is a contract with the client, and accepting a record on a - // backend that consumes none of them makes the emitter clear a dirty flag the legacy - // pull path still owed. - // - // IT IS THE SAME SIGNAL THE CLIENT'S GATE READS, deliberately - g_resourceOps is the - // table a backend installs at its own bring-up and uninstalls at teardown, and - // MGPipeApplierReset does NOT touch it (see there), so the gate cannot flap between a - // make-current and the emissions that follow it. The two therefore agree on every path - // except the one this exists for: an emitter called directly, without passing PipeFill. - // - // EVERY CALLER PLACES IT AFTER THE RECORD'S OWN SHAPE CHECKS AND BEFORE ANYTHING MOVES. - // A malformed record is Fatal{ProtocolCorruption} whether or not anything would have - // read it - a trip wire that fires only on some backends is a trip wire nobody can - // trust - so the refusal is the LAST thing asked and the first thing that stops the - // write. The three unit-set entry points are the exception and say so at their call - // site: ApplyUnitWindow validates and writes in one step, so the question has to be - // asked in front of it. + // True (and counted in MGPipeApplierState::RefusedNoConsumer) when nothing in this + // process consumes texture, framebuffer, sampler and program records: accepting one makes + // the client clear dirty flags nobody will act on. It asks what the client's gate + // (PipeFill.cpp's P4aFamilyHasItsConsumer) asks: the published consumer bit under a + // transport, the registered op table in process. MGPipeApplierReset leaves g_resourceOps + // alone, so the answer cannot flap across a make-current. Callers ask after the record's + // shape checks (a malformed record is Fatal on every backend) and before anything moves; + // calls that start with a resolution, and the unit sets, ask first. Bool NoP4aConsumer() { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { @@ -1844,27 +1721,14 @@ namespace MobileGL::MG_Pipe { void MGPipeApplierReset() { #if MOBILEGL_BUILD_DISAGGREGATED - // P5c (ct), CONTRACT-P5C.md §5.1 / §6 layer 2. With an active transport g_applier is - // SERVER-PRIVATE and the GL thread's reset crosses as the applier_reset RECORD - // (PipeFill.cpp's FreshlyPrimed arm emits it; ServerVerbSink::OnApplierReset runs this - // function ON the apply thread). Reaching here from any other thread WITH A LIVE WIRE - // is the direct call the record replaced - a write into server memory with no wire - // shape, and exactly the revert the red-once gate must catch - so it is a named Fatal - // rather than a silent reset of state another role owns. The window the Fatal - // deliberately does NOT cover is a configured-but-wireless transport: before - // ClientSession::Start() there is no wire for a record to cross (§6 layer 2's - // documented bring-up exception), and a ServerLoop fixture with no client at all has - // none either - there this call is the only reset that exists. Monolith keeps the - // direct call, byte for byte (G1); in a pull build none of this is compiled at all. - // D10: THE MIDDLE CONJUNCT WAS ClientSession::Active() != nullptr ALONE, WHICH IS - // PERMANENTLY NULL IN A SERVER PROCESS - so this guard could not arm at all under - // spawn, in the shape where calling the reset off the apply thread is most likely and - // least recoverable. MGPipeSessionLive() is the same question asked in a way both roles - // can answer, and MGPipeServerArm() is the process-scoped half of "am I the server". - // - // BOTH ARE ADDED AS DISJUNCTS, not substituted. The originals are the only facts a - // fixture without a full session has, and swapping them out silently disarms every such - // test - measured. + // Under a transport the applier is server-private: the GL thread's reset crosses as the + // applier_reset record, and ServerVerbSink::OnApplierReset runs this on the apply thread. + // A direct call from a client-role thread (not the apply thread, not MGPipeServerArm()) + // while a session is live writes server memory with no record behind it: a named Fatal. + // Before a session exists (before ClientSession::Start, or a ServerLoop fixture with no + // client) the direct call is the only reset there is. MGPipeSessionLive() and + // MGPipeServerArm() sit beside ClientSession::Active() and OnApplyThread(), not in their + // place: fixtures without a full session have only the latter two. if (MG_Config::Transport != MG_Config::TransportMode::Monolith && (MG_Remote::Client::ClientSession::Active() != nullptr || MG_Pipe::MGPipeSessionLive()) && !MG_Remote::Server::ServerLoop::OnApplyThread() && !MG_Pipe::MGPipeServerArm()) { @@ -1879,22 +1743,13 @@ namespace MobileGL::MG_Pipe { MGPipeApplier().ScatteredChunkBits = 0; MGPipeApplier().PatchCarrierComparisons = 0; MGPipeApplier().PatchCarrierDivergences = 0; - // P3a. THIS RUNS AT EVERY CHANGE OF THE CURRENT CONTEXT, not once per fresh one: - // MGPipeTracker::Update resets itself whenever the context pointer moves and the - // emitter calls this from the walk that follows, so a make-current BACK to a context - // that is still alive lands here too. Everything cleared below is therefore something - // a returning context may not inherit, and nothing else is cleared. - // - // THE OBJECT RECORDS ARE NOT CLEARED. A GL object lives in a share group, not in a - // context: the buffer a returning context is about to write to is the same buffer with - // the same storage, and its record is where the extent and the mutation serial that - // D-A4 re-keys IsBufferDrawClean onto now live. Dropping them here made every - // glBufferSubData after a context switch resolve to nothing and be dropped, with the - // only trace an assertion that compiles out at INFO. They go at the object's own death - // (resource_destroy, delete_vertex_elements) and at MGPipeApplierReleaseObjectRecords. - // - // The OP TABLE is deliberately not cleared either - it is installed and uninstalled by - // the backend's own bring-up and teardown, not by a state reset. + // This runs at every change of the current context, including a make-current back to a + // live context (MGPipeTracker::Update resets whenever the current context changes), so it + // clears only what a returning context may not inherit. Object records stay: GL objects + // live in the share group, and a buffer's record holds the extent and mutation serial its + // later writes and IsBufferDrawClean need. They go at the object's own death and at + // MGPipeApplierReleaseObjectRecords. The op table stays too; the backend installs and + // removes it at bring-up and teardown. MGPipeApplier().RefusedResourceCalls = 0; MGPipeApplier().RefusedVertexInputCalls = 0; MGPipeApplier().RefusedObjectCalls = 0; @@ -2201,28 +2056,13 @@ namespace MobileGL::MG_Pipe { const FloatVec4 outer(patch.Outer[0], patch.Outer[1], patch.Outer[2], patch.Outer[3]); const FloatVec2 inner(patch.Inner[0], patch.Inner[1]); - // THE SECOND TRIP WIRE (D6, D10). The patch trio travels TWICE - once in pipeline - // chunk P0, because it is pipeline state, and once as set_patch_state, because both - // backends bake it into the synthesized control stage from a shader-build path. The - // redundancy is the point: if the two carriers ever part, a stale set_patch_state - // silently clobbers what bind_render_state scattered and the tessellation levels a - // draw uses stop being the ones its CSO was minted for. - // - // Compared BITWISE, because a NaN outer level is a legal glPatchParameterfv value - // (ARCHITECTURE.md 5.2) and must compare equal to itself. - // - // ARMED BY THE APPLIER'S OWN SCATTER LEDGER, not by "some CSO has been bound" - // (PipeApply.h, MGPipeApplierState::ScatteredChunkBits). The question is whether the - // chunk-P0 bytes in the working block are the applier's, and that single condition - // covers both contracts this wire needs: the ORDERING one - a set_patch_state that - // legitimately precedes the first bind of a context has nothing to agree with yet - - // and the VERB-CLASS one - with the render-state subsystem off those bytes are the - // per-verb fill loop's, and FillPoints.def does not publish GetRenderStateParameters - // at kDispatch or kTextureOp, so they go stale there. - // - // It runs in the shipped push build too, because a wire that is compiled out of - // every build a device runs is not a wire. The cost is a 24-byte memcmp on a call - // that is emitted when the tessellation state CHANGES, i.e. about once per program. + // Patch carrier trip wire. The patch trio travels in pipeline chunk P0 and again as + // set_patch_state (both backends bake it into the synthesized control stage); if the two + // ever part, a stale set_patch_state overwrites what bind_render_state scattered. Compared + // bitwise so a NaN level equals itself. Armed only once this applier has scattered the + // trio's chunk itself (kChunksPatchTrio in ScatteredChunkBits): before the context's first + // bind there is nothing to agree with. Runs in every build; the cost is a 24-byte compare + // on a call emitted only when tessellation state changes. if ((MGPipeApplier().ScatteredChunkBits & kChunksPatchTrio) == kChunksPatchTrio) { ++MGPipeApplier().PatchCarrierComparisons; const Bool agrees = working.PatchVertices == patch.Vertices && @@ -2304,26 +2144,13 @@ namespace MobileGL::MG_Pipe { } } - // ================================================================================ - // P3a: the nine resource entry points (D-A1, D-A2). + // The resource entry points. // - // WHAT THE APPLIER OWNS HERE IS IDENTITY, EXTENT AND ORDER - NOT CONTENT. A buffer's - // bytes are the backend's; what crosses is a {slot, gen} handle, a flat descriptor and, - // where the call carries content, the client's own shadow base. So each body below does - // three things in this order: resolve the handle against this applier's record, check the - // record against its own declared extent, and only then move the record and hand the call - // to the backend. - // - // NOTHING REGISTERS MGPipeResourceOps IN THIS PACKAGE, so every dispatch below is a null - // check that falls through, and the tree behaves exactly as it did. That is deliberate and - // it is what makes this commit landable on its own: the frontend still dispatches the op - // table these replace, the backend that will register one is a later package, and the - // records these bodies keep are already correct when it does. - // - // THE THREE SERIALS ARE MGGen-CLASS: server-owned, monotone, never crossing the line. No - // MGPipe call may require the client to supply or know one (ARCHITECTURE.md 4.2.2), which - // is why they are incremented here rather than carried in a payload. + // The applier owns identity, extent and order, not content: a buffer's bytes are the + // backend's. Each body resolves the handle against this applier's record, checks the record + // against its own declared extent, and only then moves the record and calls the backend. + // Serials are server-owned and monotonic and never travel in a payload (ARCHITECTURE.md §2.2). // ================================================================================ Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc) { @@ -2478,44 +2305,13 @@ namespace MobileGL::MG_Pipe { // sampling-resolution shutter guarded - the server-side answer moves with it. MGPipeApplierNoteTextureStateMoved(); - // A RESPECIFY REDEFINES A STORE, SO THE PENDING UPLOADS AGAINST THE STORE IT REPLACES - // GO WITH IT - AND ONLY THOSE. They are boxes and rects in a level's coordinate system - // and that space has just been replaced, so a box kept across a shrink would have - // Espryt upload past the end of the new level. But WHICH storage a respecify replaces - // is the call's to say and the descriptor's to say nothing about: - // - // - `level == nullptr` is a WHOLE-RESOURCE redefinition - every glBufferData / - // glBufferStorage, every glTexStorage* (which defines every level at once), every - // texture view - and every key goes. - // - a non-null `level` is ONE glTexImage*D on a mutable texture, which redefines that - // (uploadTarget, level) AND NOTHING ELSE. Dropping the other keys here would lose - // texels the client no longer owes: MG_State's AllocateStorage / MarkStorageDirty - // are per (uploadTarget, level) too, so the re-mark this call causes covers ITS - // level only, while every other level's dirty flag was cleared at its own emission - // (D-D5 step 1). The canonical sequence that loses them is glTexImage2D(0, data) -> - // draw (the shape is emitted and accumulated; Espryt bails because the texture is - // not yet mipmap-complete, Managers.cpp's incomplete-texture early return, which is - // the arm this set exists for) -> glTexImage2D(1, data), which under a blanket - // clear destroys level 0's entry before anything ever uploaded it. - // - // - and a METADATA update (ID-18 M4) with a NULL level drops NOTHING. It refines the - // whole-resource arm rather than contradicting it: the rule is "the uploads against - // the storage this call REPLACES go with it", and a call whose storage-defining - // fields all equal the stored descriptor replaces no storage, so no level's - // coordinate system has moved and every pending box is still described in the - // space it was accumulated in. B re-emits the descriptor when a sticky bind bit - // moves - with a null level, deliberately - which can land between a - // glTexSubImage2D and the sync that consumes it; eating those texels there would be - // C1's bug with a different trigger, and just as silent. - // - // - A NAMED LEVEL IS DROPPED WHETHER OR NOT THE base descriptor moved (P4a final - // review C-1). The companion scope now carries that level's exact W/H/D as well as - // its (uploadTarget, level) key, so the texture twin and staged-byte store update the - // same coordinate system. A metadata-only mask republish passes null and therefore - // cannot consume a standing upload on its behalf. - // - // A buffer never has a pending upload at all, so all three arms are inert for P3a's - // half - which is also why a buffer is never classified as metadata-only (below). + // A respecify drops the pending uploads against the storage it replaces, and only those + // (ARCHITECTURE.md §6.1): a box kept across a shrink would upload past the end of the new + // level. A null `level` replaces the whole resource and drops every key, unless the call + // is metadata-only, which replaces no storage and drops nothing. A named level (one + // glTexImage*D) drops exactly that (uploadTarget, level), even if the descriptor did not + // move; the other levels' dirty flags were cleared at emission, so their keys are the only + // record of texels the backend still owes. Buffers never have pending uploads. if (level != nullptr) { // The keys are unique by AccumulatePendingUpload's construction - it looks for the // pair before it appends - so this erases at most one entry and stops. @@ -2547,23 +2343,12 @@ namespace MobileGL::MG_Pipe { DefineTextureWithoutBackendHook(desc, level); } - // resource_respecify is the catalogue's only kNeedsAck call, and the per-record half - // of that flag is MGPipeResourceRespecifyNeedsAck(desc): glBufferStorage is a real - // synchronous allocation and the only entry point allowed a synchronous ack, while - // glBufferData travels through the same call and must not acknowledge one. In monolith - // the acknowledgement IS the return of this function - the applier is one call away - - // so the predicate has nothing to gate here and is deliberately not branched on: a - // branch whose arms were identical would be dead code the transport would then have to - // find and remove. PipeCatalogueTest.ResourceRespecifyAcksOnlyImmutableStorage is what - // keeps the predicate honest until the doorbell reads it. - // - // ID-18 M4 ASKS FOR "NO REALLOCATION ACK" ON A METADATA UPDATE, AND IT IS TRUE HERE BY - // CONSTRUCTION RATHER THAN BY A BRANCH: the predicate is false for every target that is - // not a buffer, and RespecifyRedefinesNoStorage refuses the buffer target outright, so - // MGPipeResourceRespecifyNeedsAck(desc) is false for every record this applier - // classifies as metadata-only. That is why the classification excludes buffers rather - // than suppressing the ack afterwards - a suppression would have been a second rule the - // transport would then have to learn. + // resource_respecify is the only kNeedsAck call, and MGPipeResourceRespecifyNeedsAck(desc) + // narrows the ack to an immutable buffer, i.e. glBufferStorage (ARCHITECTURE.md §8.3). In + // process the ack is this function's return, so the predicate is not branched on; + // PipeCatalogue.ResourceRespecifyAcksOnlyImmutableStorage pins it. A metadata-only update + // never acks: the predicate is false for every non-buffer target, and + // RespecifyRedefinesNoStorage never classifies a buffer as metadata-only. if (desc.Target != kMGPipeResourceTargetBuffer) return true; if (g_resourceOps != nullptr && g_resourceOps->Respecify != nullptr) { g_resourceOps->Respecify(desc.Resource, desc, initialBytes); @@ -2889,20 +2674,11 @@ namespace MobileGL::MG_Pipe { #endif // ================================================================================ - // P3a: the five vertex-input entry points (D-G, D-H, D-I). + // The five vertex-input entry points. // - // THESE FIVE DISPATCH TO NOBODY, and that is not an omission: MGPipeResourceOps is the - // RESOURCE family's table, and the vertex-input calls have no backend hook because the - // backend does not act on them when they arrive. It reads them at its own draw-time sync, - // out of the state below, which is what the three serials here are for - they are what - // retires the twin's wrapping-Uint16-plus-identity patches, so a compare that used to ask - // "is my Uint16 configuration version still the frontend's?" asks "is my Uint64 serial - // still the server's?" instead. - // - // Nothing emits them in this package either: their dirty bits map to no subsystem, the two - // subsystem bits are not in MG_Impl/Pipe/PipeFill.cpp's kMGPipeWiredSubsystems, and the - // emitters beside them are stubs. The state below is therefore correct and unread until - // the packages that wire both ends land. + // None of them calls the backend. The backend reads this state at its own draw-time sync + // and compares the three Uint64 serials kept here (ContentSerial, VertexBuffersSerial, + // IndexBufferSerial) instead of a wrapping frontend version. // ================================================================================ void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes) { @@ -2910,25 +2686,12 @@ namespace MobileGL::MG_Pipe { "create_vertex_elements named the reserved slot 0"); if (desc.Cso.Slot < kMGPipeFirstAllocatableSlot) return; - // THE COUNTS ARE CHECKED BEFORE A BYTE OF THE BLOB IS TOUCHED, and the check is the - // record's own self-description: the blob is MGPVertexAttribWire[AttributeCount] - // immediately followed by MGPVertexBindingPointWire[BindingPointCount]. THIS is the - // reason the second view travels at all - a record that declares a BindingPointCount - // it does not carry would otherwise be a shape this gate had to police forever with - // nothing to police it against. - // - // Both counts are bounded by GL's attribute limit, which is also the size of the two - // arrays they are unpacked into, so the bound and the destination cannot drift apart. - // THE COUNTS ARE WHAT BOUNDS THE READ; the declared blob length is a cross-check. - // - // THE BLOB RULE IS THE SAME ONE resource_subdata IS HELD TO (SubDataBoxFault above, - // and MGPipeTypes.h states it on both records): a non-zero Blob.Size must be exactly - // the length the record's other fields describe, and a zero Blob.Size means "this - // record does not declare its blob" - which is what a monolith emission is, since the - // bytes travel beside the record through blobBytes and no MGPBlobRef is filled. One - // rule, both blob-carrying families: a transport that fills the field gets a real gate - // on the first truncated record, and a client that leaves it zero does not abort a - // verify build over a field it never used. + // The counts are checked before any blob byte is read, against the record's own layout: + // MGPVertexAttribWire[AttributeCount] then MGPVertexBindingPointWire[BindingPointCount]. + // Both are bounded by kMGPipeMaxVertexAttribs, which also sizes the destination arrays, so + // the counts bound the read. Blob.Size follows resource_subdata's rule (SubDataBoxFault): + // non-zero must equal the length the counts describe; zero means the record does not + // declare its blob, as with in-process emission, which passes the bytes in blobBytes. const Uint64 attributeBytes = Uint64{desc.AttributeCount} * sizeof(MGPVertexAttribWire); const Uint64 bindingBytes = Uint64{desc.BindingPointCount} * sizeof(MGPVertexBindingPointWire); const Uint64 declared = attributeBytes + bindingBytes; @@ -3143,31 +2906,13 @@ namespace MobileGL::MG_Pipe { } // ================================================================================ - // P4a: the fifteen object and working-state entry points + // The object and working-state entry points // ================================================================================ // - // NOT ONE OF THEM DISPATCHES TO A BACKEND FUNCTION POINTER, and that is the structural - // decision the whole phase rests on rather than an omission. Nothing in these families - // reaches the backend at GL-call time today - texture storage only marks a level dirty, - // texture parameters run from the draw-time sync, renderbuffer storage is allocated inside - // SyncToBackend, a sampler twin is created lazily from the program pass, and the - // framebuffer, the unit sets and the program are all resolved at PrepareForDraw - so every - // call below is either an OBJECT RECORD the applier stores or WORKING STATE the applier - // stores, and the backend reads the applier at the sync points it already has, keyed on a - // server-owned Serial instead of a frontend version. MGPipeResourceOps is unchanged. - // - // THE SAME THREE STEPS, IN THE SAME ORDER, AS P3a's NINE: resolve the handle against this - // applier's record, check the record against its own declared extent, and only then move - // the record. The two verdicts are the same two, and the difference between them is the - // whole of this file's discipline - a call naming a record this applier does not have is a - // DEFINED NO-OP that is COUNTED (RefusedObjectCalls), because one legal sequence produces - // it; a record that would make the server index or allocate outside its own storage is - // Fatal{ProtocolCorruption} with the record's identity in the line. - // - // NOTHING EMITS ANY OF THEM IN THIS PACKAGE. The four subsystem bits are not in - // kMGPipeWiredSubsystems, every client emitter beside them is still a stub, and no backend - // reads the records yet - so the state below is correct and unread until the packages that - // wire both ends land, and this tree is behaviourally identical to the contract commit's. + // None calls a backend function pointer: the backend reads these records at its own sync + // points, keyed on server-owned serials. Same order as the resource entry points: resolve, + // check the record against its own extent, then move it. A record this applier lacks is a + // counted no-op (RefusedObjectCalls); one that would index outside storage is Fatal. // ================================================================================ // w1: the framebuffer record, per bound target @@ -3220,24 +2965,13 @@ namespace MobileGL::MG_Pipe { return; } - // NO HANDLE IS RESOLVED HERE AND NONE MAY BE. A framebuffer has a handle but no wire - // lifetime - the catalogue has no framebuffer create and no framebuffer destroy, - // because a framebuffer is state and this call is the only one that names one - so - // there is nothing to look up, this entry point never counts a refusal, and the record - // it writes OVERWRITES whatever the slot held. That last part is the point rather than - // a compromise: the slot's previous occupant is dead by construction (the client frees - // a framebuffer's slot when the object dies and the allocator bumps the generation on - // reuse), and the record arriving now describes whatever holds the slot today. - // - // The surfaces' Res handles are not resolved either: an attachment PINS its texture, - // and in monolith the frontend's own SharedPtr is that keep-alive, so a refusal here - // would be enforcing a lifetime rule monolith cannot need and split has not defined. - // P4a's belt, after the record's own shape checks and before the table is touched. This - // entry point is the ONE the client can reach without passing PipeFill's gate - - // GL_Framebuffer.cpp's PipePublishFramebufferByName calls the emitter directly at the - // fifteen DSA sites - so on a backend with no consumer this is where those records - // stop. RefusedNoConsumer and not RefusedObjectCalls: the framebuffer family's counter - // contract (see the header) is that no framebuffer call ever moves that one. + // No handle is resolved: a framebuffer has a handle but no wire lifetime (no create or + // destroy record), so this record overwrites the slot; the client frees the slot when the + // object dies and reuse bumps the generation. Attachment Res handles are not resolved + // either: attachments pin their textures on the client (ARCHITECTURE.md §5.6). + // The consumer check follows the shape checks. This is the one entry point the client + // reaches without PipeFill's gate (PipePublishFramebufferByName calls the emitter + // directly), and it counts into RefusedNoConsumer, never RefusedObjectCalls. if (NoP4aConsumer()) return; MGPipeFramebufferRecord* record = @@ -3523,24 +3257,14 @@ namespace MobileGL::MG_Pipe { return true; } - // The three of them, and NO STAGE DIMENSION on any of them: MobileGL's texture-unit space - // is one merged array of 192, the same unit may be sampled from two stages, and stage is - // derived server-side from the reflection archive only where the target API needs it. - // - // A NULL HANDLE IN A TAIL ENTRY IS LEGAL EVERYWHERE HERE and is not a refusal: a unit the - // program does not resolve carries a null view, a unit with no sampler object carries a - // null sampler CSO (the texture's built-in sampler applies then, exactly as today), and a - // unit with no texture carries a null resource. None of the three is resolved against a - // record either - a set is WORKING STATE, the records it names are OBJECT state, and the - // backend resolves the pair at its own sync point where both are current. - // - // P4a's belt sits IN FRONT of ApplyUnitWindow on all three, and this is the one place it - // is not last: ApplyUnitWindow validates the window and writes it in the same step, so - // there is no point between the two to stand at. The cost is that on a backend with no - // consumer a malformed window is declined rather than Fatal - which is the right trade the - // one way round it can be made, because that backend would never have been handed the - // window at all (the client's gate stops it) and the shipped configuration that DOES - // consume these still trips the wire. + // The three unit sets carry no stage dimension: the texture-unit space is one merged array + // of 192, a unit may be sampled from two stages, and the server derives stage from the + // reflection archive only where the target API needs it. A null handle in a tail entry is + // legal (unresolved unit; no sampler object, so the texture's built-in sampler applies; no + // texture), and nothing is resolved here: the backend pairs a set with the object records at + // its own sync point. The consumer check runs before ApplyUnitWindow, which validates and + // writes in one step, so a backend with no consumer declines a malformed window instead of + // tripping; the client's gate never sends it one, and a consuming backend still trips. void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail) { if (NoP4aConsumer()) return; if (!ApplyUnitWindow("set_sampler_views", hdr.Start, hdr.Count, hdr.ContentHash, tail, @@ -3592,25 +3316,14 @@ namespace MobileGL::MG_Pipe { // quiet return: the picture would be wrong and the lane would be green, which is the one // failure mode this campaign spends its refusals on. - // set_shader_buffers, ONE RECORD PER CLASS (CONTRACT-P5E.md §5.6, rulings 10/11). The whole - // body is "validate the window, copy the tail into that class's array, take the mask, move - // the serial" - the binding points are WORKING STATE and the objects they name are resolved - // by the backend at its own sync point, from the handle each range carries. - // - // IT DOES NOT GO THROUGH ApplyUnitWindow, and the reason is worth a line rather than a - // diff: that helper's refusal message names "the merged texture-unit space" and the three - // unit sets share one capacity, while this row has THREE windows keyed by Class and two - // refusals of its own that the contract names by string ("SetShaderBuffers.Class" / - // ".Count"). A shared helper that had to grow a class parameter and a message parameter - // would be the same code with more ways to pass the wrong one. - // - // AND THERE IS NO NoP4aConsumer BELT HERE, deliberately. P4a's belt exists because a client - // clears a per-level dirty flag on ACCEPTANCE, so accepting on a backend that consumes - // nothing loses uploads (ID-39). This family's liveness gate is the client's own - // P5eFamilyIsLive (PipeFill.cpp), which asks R-8 for bit 13 in particular - a server that - // does not publish bit 13 never receives one of these records at all - and storing a window - // no backend reads costs nothing and loses nothing, because the emitter's only latch is the - // set-hash suppressor and the record it latched did cross. + // set_shader_buffers: one record per class (uniform, storage, atomic counter). The body + // validates the window, copies the tail into that class's array, takes the writable mask and + // moves the serial; the backend resolves each range's handle at its own sync point. It does + // not use ApplyUnitWindow, which serves the one merged texture-unit space, because this row + // has three windows keyed by Class and its own named refusals. There is no consumer check: + // the client sends these only to a server that publishes the buffer-binding consumer + // (PipeFill.cpp's P5eFamilyIsLive), and an unread window loses nothing because the emitter + // latches only its set-hash suppressor. void MGPipeApplySetShaderBuffers(const MGPShaderBuffers& hdr, const MGPBufferRange* tail) { if (hdr.Class >= kMGPipeShaderBufferClassCount) { MGP_TRIP_WIRE_REPORT("MGPipe: " MGP_TRIP_WIRE_TAG("ProtocolCorruption") @@ -3734,27 +3447,12 @@ namespace MobileGL::MG_Pipe { if (op == MGPWireOp::DrawVbo && payload != nullptr) { const auto& draw = *static_cast(payload); if ((draw.Flags & static_cast(kDrawClientArrays)) != 0) return true; - // ---- ESCALATION (iii) WAS HERE AND IS WITHDRAWN (P5e ra2, ID-133 then ID-136) --- - // - // ID-133 escalated a PLAIN multi-draw (`NumDraws > 1 && !kDrawIsIndirect`) so that - // MultiDrawImpl::RunIndirect's read of the client's GL_DRAW_INDIRECT_BUFFER binding - // became a legal barriered pull instead of an unbarriered Fatal. It worked, and it - // cost a rendezvous on EVERY plain glMultiDraw* on the DEFAULT tier: there is one - // draw opcode - all twenty entry points collapse onto draw_vbo - and the tier is - // chosen on the server per batch (MultiDraw.cpp's ResolveTierForBatch), so no - // predicate both roles can compute names the arm that actually needed it. - // - // THE CHECK ON AN ESCALATION IS "WHICH ARM PAYS FOR IT", NOT "WHICH LANE GOES - // GREEN", and that is the rule this pair of rulings exists to record. The read was - // a save/restore of a GL binding NAME around the tier's own scratch buffer, not a - // data dependency, so the answer was to ask the side that did the binding: - // BoundDrawIndirectBufferId now takes the handle arm its neighbour - // ResolveBoundIndexBuffer already had, and the pull is GONE rather than legalised. - // - // Both halves landed in ONE commit on purpose: the pull retired without this clause - // withdrawn is a cost with no reason, and this clause withdrawn without the pull - // retired puts 18 lane entries back on the unbarriered arm. Neither is a state to - // gate or to measure, so neither was ever a head. + // A plain multi-draw is not escalated. Espryt's indirect multi-draw tiers resolve the + // GL_DRAW_INDIRECT_BUFFER binding from a handle (MultiDraw.cpp's + // BoundDrawIndirectBufferId) instead of reading client state, and the tier is chosen on + // the server per batch, so no clause both roles can compute would name the arm that + // needs it. Judge a new escalation by which arm pays for it (see MGPipeBarriered's + // declaration in PipeApply.h). } return false; } @@ -3793,20 +3491,12 @@ namespace MobileGL::MG_Pipe { } // ================================================================================ - // w3: the shader CSO, the two program bindings and the default uniform block + // The shader CSO, the two program bindings and the default uniform block. // - // AND THE SERVER NEVER RE-LINKS. glslang lives entirely on the client and SPIRV-Cross - // entirely on the server, a file-level cut: what crosses is the per-stage SPIR-V and the - // reflection archive, never source, and there is no server-side compile pool to hand a - // failure back from. Link failure needs no synchronous return either - it is one error log - // plus a bind-program-0 empty draw today, GL_LINK_STATUS is never withdrawn, and the - // synchronous query is answered by the client out of its own ProgramObject. - // - // WHAT THE SERVER STILL SPECIALISES IS NOT THIS RECORD'S BUSINESS. A backend program - // depends on eight more inputs than the artefacts - the draw framebuffer's clamp masks and - // fragColor broadcast count, the storage-block binding signature, the live image formats, - // the patch parameters - and every one of them now arrives through ANOTHER record this - // applier holds. create_shader_state publishes the artefacts; the verb specialises. + // The server never re-links: SPIR-V and the reflection archive cross, never source, and + // GL_LINK_STATUS is answered by the client from its ProgramObject (ARCHITECTURE.md §7). + // The other inputs a backend program is specialised on (framebuffer clamp masks, storage + // binding signature, image formats, patch parameters) arrive through other records. // ================================================================================ void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc, @@ -4064,27 +3754,13 @@ namespace MobileGL::MG_Pipe { ++stored->GlobalConstantsSerial; } - // THE MONOLITH BODY IS A NO-OP AND THAT IS THE WHOLE OF IT: the emulation this names still - // runs, exactly as it does today, on the same code path. What the call site buys is that - // the set of emulations a split server cannot serve is NAMED, GREPPABLE and PINNED, so P5 - // and P8 give it teeth by editing one function instead of rediscovering five call sites. - // - // It takes a literal and does nothing with it. Not a log line, not a counter: it sits on - // paths a frame can reach many times, and ROADMAP.md forbids committing hot-path - // instrumentation. - // P5 GIVES IT TEETH, and this is the whole of it: ONE function edit arms FIVE call sites - // (Managers.cpp:5334, DirectGLES.cpp:8051, :8702, :8997, :10623), exactly as the header - // and P4a's comment above promised. Every one of them reaches back into the CLIENT's - // address space - a texture shadow, a CPU mipmap fallback, a host-side copy - and a server - // has no client address space to reach into, so degrading silently is the one outcome that - // must not happen. - // - // THE ARM IS THE TRANSPORT, NOT THE BUILD. build-split runs MOBILEGL_TRANSPORT=monolith in - // every unit and integration-gpu lane, and several of the five are on ordinary monolith - // paths that those lanes exercise (glGenerateMipmap reaches two of them); arming on the - // build would turn 1117 integration cases red for running code that is correct in the role - // they run it in. None of the five is on P5's reduced path, so under a real transport this - // costs nothing and catches a lot. + // MGPipeUnmigratedEmulation, below: a backend emulation that reads or writes client memory + // names itself with a literal. In process it is a no-op and the emulation runs; under a + // transport the server has no client memory, so it aborts with Fatal{UnmigratedEmulation} + // rather than degrade silently. It arms on the transport, not the build, because split + // builds also run in-process lanes where such code is correct. No log or counter: it may sit + // on hot paths. Product code has no call sites; scripts/ci/unmigrated_emulation_sites.py + // and PipeCatalogue.EveryUnmigratedEmulationIsNamedOnce keep the names and sites equal. namespace { thread_local Bool tl_serverOriginatedWrite = false; } // namespace diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index b4c75764..cf5fcae7 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -16,34 +16,14 @@ // header in the same layer - MG_Pipe includes MG_Pipe - so the closure does not move. #include "MGPipe.h" -// The in-process applier: the SERVER half of the calls P2 emits. Under split this file is -// MG_Remote/Server/PipeApplier (ARCHITECTURE.md 8.3); in the monolith it writes -// MG_Backend/MGPipe/PipeInputs' gPipeInputs directly, so a call and its effect are one -// function call apart and nothing is serialised. -// -// THE SERVER'S PER-CONTEXT WORKING BLOCK *IS* PipeInputs::m_renderState. bind_render_state -// and set_dynamic_state scatter their chunks straight into it, which is why DirectGLES' -// SyncRenderState is not one line changed (ROADMAP.md P2, G5): the block Espryt binds by -// const reference is the assembled block. It is also what makes the MOBILEGL_PIPE_VERIFY -// comparator a real oracle instead of a tautology - the compare-at-read now proves -// "assembled == live", field by field, at every backend read. -// -// This header FORWARD-DECLARES PipeInputs rather than including it: the applier's callers -// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend. -// -// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol. -// P4a: create_shader_state carries the reflection ARCHIVE, and in monolith the archive does -// not travel - the two structs ride beside the record through the entry point's companion -// pointers, exactly as P3a's `const void* initialBytes` does (D-H3, the one Blob rule). So -// this header needs their NAMES and never their definitions; the forward declaration is the -// whole coupling and the closure gate is what keeps it one. The verify build is the only -// place the codec runs, and it runs from PipeApply.cpp. -// P5e (pg) ADDS A THIRD NAME AND KEEPS THE RULE: ProgramArchive is what a SPLIT create carries -// - the record's OWN copy of both structs plus the stage of each module - and the record holds -// it through a SharedPtr, which needs no definition either (the deleter is captured where the -// archive is constructed, in the decoder's TU). So the coupling is still three names and no -// closure change, and MG_Backend, which does need the definition, includes -// ProgramArtifactsCodec.h for it. +// The applier: the server half of every MGPipe call (ARCHITECTURE.md §10.1). In one process the +// routing tables call these entry points directly; under a transport the server's PipeApplier +// decodes each record and calls the same ones. bind_render_state and set_dynamic_state scatter +// their chunks straight into PipeInputs::m_renderState, the block Espryt's SyncRenderState +// reads, so the verify comparator checks the assembled block against live state at every read. +// PipeInputs and the program artefact types are only forward-declared (MG_Pipe sits below +// MG_Backend): entry points take artefacts by pointer and a record holds its archive by +// SharedPtr, whose deleter is captured where it is built; users include ProgramArtifactsCodec.h. namespace MobileGL::MG_State::GLState { struct LinkArtifacts; struct SpirvArtifacts; @@ -74,28 +54,13 @@ namespace MobileGL::MG_Pipe { // P3a: the handle-shaped resource op table (D-A1) // --------------------------------------------------------------------------------- - // The SECOND backend op table, beside BufferBackendOps. Registered by the active backend - // at bring-up and cleared at shutdown, exactly as that one is; a null table means "this - // backend has not taken the resource family over", and the frontend then dispatches the - // old way, which is what lets the client half land on its own and what keeps a backend - // whose buffer path is a later phase untouched. - // - // NO FRONTEND TYPE APPEARS HERE, and that is the whole point of the conversion: every - // hook it replaces took a frontend heap reference and four of them read that object's - // shadow bytes. A resource is an MGPipeHandle plus a payload record plus, where the call - // carries content, a companion `const void*`. - // - // THE COMPANION POINTER IS NOT A NEW IDEA - MGPipeApplyCreateRenderState already carries - // a blob beside its POD for the same reason: in monolith a blob needs no MGPBlobRef and - // the pointer is the client's own shadow base, so the call is zero-copy and behaviour is - // unchanged. How those bytes cross under a real transport is that phase's problem and - // that phase's flag edit; resource_respecify deliberately does NOT carry kHasBlob here, - // because a kHasBlob record must own an MGPBlobRef member and MGPResourceDesc has none. - // - // SubDataResident MAY BE NULL and stays nullable on purpose: one backend deliberately - // does not implement it (kOptional in the catalogue), the frontend checks it exactly as - // it checks the op table it replaces, and giving that backend a real implementation is a - // behaviour change that belongs in its own change, not in this migration. + // The backend's resource op table, registered at bring-up and cleared at shutdown. Null means + // no backend consumes resource records (see RefusedNoConsumer and PipeFill.cpp's + // FamilyIsLive). No frontend type appears here: a resource is an MGPipeHandle, a payload + // record and, where the call carries content, a companion `const void*` (in one process the + // client's own shadow, so the call is zero-copy). resource_respecify carries no kHasBlob, + // because such a record must own an MGPBlobRef and MGPResourceDesc has none. + // SubDataResident is kOptional and may be null; callers check it. struct MGPipeResourceOps { void (*Create)(MGPipeHandle res, const MGPResourceDesc& desc); void (*Respecify)(MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes); @@ -157,27 +122,13 @@ namespace MobileGL::MG_Pipe { // P3a: the applier's own records (D-G4) // --------------------------------------------------------------------------------- - // THE SLOT A CLIENT MAY NAME IS BOUNDED, and the bound lives here rather than at the - // client's allocator because the two tables below are grown BY the slot index. An array a - // handle indexes is the right shape for a dense slot space (MGPipeHandles.h) and the price - // of that shape is that one corrupt Uint32 in a payload otherwise arrives at an allocator - // as a four-billion-entry request from inside the bounds gate's own commit. A slot at or - // above these is Fatal{ProtocolCorruption} - the same verdict as any other record that - // would make the server act outside its own storage - and never a resize. - // - // The two numbers differ because the two records do: a resource record is descriptor-sized - // and a vertex-elements record carries both unpacked views at ~1.3 KB, so one bound would - // mean two very different worst cases. Both are far above what a GL application has live - // at once, and NEITHER IS EVER ALLOCATED BY BEING NAMED: the tables grow to the client's - // own dense high-water mark and no further, so the bound costs nothing until a record is - // already corrupt. Package C bounds handle.Slot the same way before - // BackendSlotTable::EntryAt, which resizes on a client-supplied index too. - // P4a: THE BOUND IS PER KIND, not per table, and that is what keeps one number honest - // while the number of tables grows. The slot spaces of kinds Buffer, Texture and - // Renderbuffer are INDEPENDENT (MGPipeSlotAllocator allocates per kind), so three - // different objects can hold slot 7; the applier therefore keeps one Vector per resource - // KIND and indexes it by slot, rather than one Vector indexed by slot alone. Each is - // bounded by kMGPipeMaxResourceSlots and each grows only to its own dense high-water mark. + // Slot bounds. The tables below grow by the slot index a client names, so one corrupt Uint32 + // would otherwise become a four-billion-entry resize: a slot at or above its bound is + // Fatal{ProtocolCorruption}, never a resize. Naming a slot allocates nothing beyond the + // client's own dense high-water mark. The bounds differ because the records do (a + // vertex-elements record is ~1.3 KB). Resource bounds are per kind: Buffer, Texture and + // Renderbuffer slots are allocated independently (MGPipeSlotAllocator), so each kind has its + // own table, each bounded by kMGPipeMaxResourceSlots. inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20; inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16; // create_render_state records retain one 396-byte pipeline half each. Keep their @@ -326,34 +277,14 @@ namespace MobileGL::MG_Pipe { // The SamplerViewCso minted for this texture (P4a D-F2: one per ITextureObject, // re-issued on the same handle whenever the restrictions move). MGPipeHandle ViewCso = kMGPipeNullHandle; - // THE PENDING-UPLOAD SET, and it is server-side state on purpose (D-D5). The client - // clears its own dirty flags at EMISSION, for the levels whose record the applier - // accepted; Espryt's upload loop has bail arms - an incomplete texture returns early, - // a multisample target refreshes and skips - that today leave the frontend flag set, - // so a naive move of the clear to the client would lose those texels. The applier - // accumulates the emitted shape here instead, it survives any number of bails, and - // Espryt consumes and clears an entry only where it actually uploads. - // - // The verify lane's RETAIN MODE is what gates the shape: a consume-and-clear set - // cannot be recomputed after emission, so the tracker retains the pre-clear set and - // the comparator compares the emitted (UnionBox, RegionCount, Regions[]) against it - // field by field. - // - // THE SET IS KEYED (UploadTarget, Level) AND EVERY KEY IS INDEPENDENT OF EVERY OTHER. - // That is not a detail: a respecify redefines ONE level when it arrives from - // glTexImage*D (MGPipeApplyResourceRespecify's trailing MGPRespecifiedLevel*), so it - // may only drop that one key - the frontend's AllocateStorage / MarkStorageDirty are - // per (uploadTarget, level) too, and the other levels' dirty flags were cleared at - // THEIR emission, so nothing anywhere still owes them. - // - // THE ACCUMULATED RECT LIST MAY OVERLAP, AND A CONSUMER MUST TOLERATE THAT. Behind one - // level the frontend's own model is pairwise disjoint (MipmapStorage keeps it so), but - // this list CONCATENATES the lists of successive emissions and the applier's gate only - // asks that each rect be inside the record's own union box - so two emissions that - // touch the same texels leave two rects that do. Staging N rects therefore uploads - // those texels twice, which is a cost and never a correctness problem; nothing here - // de-duplicates and nothing downstream may assume "the frontend's model" means disjoint - // once the shapes have been accumulated. + // The pending-upload set, server-side state (ARCHITECTURE.md §6). The client clears a + // level's dirty flags once the applier accepts its record, but a backend's upload loop + // may bail without uploading (an incomplete texture, a multisample target), so emitted + // shapes accumulate here and the backend consumes and clears an entry only where it + // uploads. Keys (UploadTarget, Level) are independent: a per-level respecify drops only + // its own key. Regions concatenate successive emissions and may overlap; staging them + // uploads some texels twice, which costs time but stays correct, so no consumer may + // assume them disjoint. struct PendingUpload { Uint16 UploadTarget = 0; Uint16 Level = 0; @@ -399,24 +330,13 @@ namespace MobileGL::MG_Pipe { Uint64 Serial = 0; }; - // create/bind/delete_shader_state, plus set_global_constants' per-program half. - // - // THE ARTEFACTS ARE NOT HELD HERE IN MONOLITH: MGPProgramDesc's seven MGPBlobRefs are all - // declared with Size 0 ("this record does not declare its blob") and the LinkArtifacts / - // SpirvArtifacts ride beside the record through the entry point's companion pointers, so - // the applier stores the DESCRIPTOR and the identity and the server reads the frontend's - // own archive. That is what keeps the codec off the monolith hot path entirely; the verify - // build is where it is exercised, by serialising, deserialising and field-comparing before - // storing. - // - // GlobalConstants is the one allocation P4a adds per program, it is bounded by - // Desc.GlobalUboSize, and it is NOT on the hot path: set_global_constants is - // (ShaderCso, Version) keyed and fires at most once per program per frame. - // P5e (docs/archive/contracts/CONTRACT-P5E.md §1): one entry of set_program_bindings' third tail as the - // RECORD owns it. The wire carries the block name as an MGHostSpan into SEG_STAGE, which - // retires with the record that named it (rule C), so the applier copies the bytes here - // rather than keeping the span: a rebuild happens at the next bind, which may be a frame - // later, and by then the staged run is somebody else's. + // create/bind/delete_shader_state, plus set_global_constants' per-program half. The record + // stores the descriptor and adopts the link's artefacts as Archive (see + // MGPipeApplyCreateShaderState). GlobalConstants is bounded by Desc.GlobalUboSize and written + // at most once per program per frame, keyed by (ShaderCso, Version). + // MGPipeProgramStorageOverride is one entry of set_program_bindings' storage-override tail as + // the record keeps it: the block name is copied because its wire span into SEG_STAGE retires + // with the record that named it, while the bindings are rebuilt at a later bind. struct MGPipeProgramStorageOverride { String Name; Int32 Binding = 0; @@ -433,26 +353,13 @@ namespace MobileGL::MG_Pipe { Uint64 GlobalConstantsSerial = 0; Uint64 Serial = 0; - // ---- P5e's server-owned archive (CONTRACT-P5E.md §5.5, gap G-A) -------------------- - // - // FILLED BY PACKAGE pg, AND ONLY UNDER A TRANSPORT. This is the whole of rule F for - // this family: before P5e the record stored the DESCRIPTOR and the artefacts rode - // beside it as two companion pointers into the frontend's own ProgramObject, so every - // reflection question the program twin asked - forty accessors in SyncToBackend alone - - // was a read of client memory that `ProgramObject::Link()` REPLACES IN PLACE. Under - // run-ahead the client is already several records past the create, so those reads are - // torn or stale by construction and no amount of care at the read site can fix it. - // - // The client encodes the archive once per link (ProgramEmit.h -> SEG_STAGE), the - // decoder frames and deserialises it, and the record ADOPTS it here - a SharedPtr and - // not a Vector, because a program-pipeline composite and its stage programs are three - // records over one link's artefacts, and because the twin holds the pointer across the - // build it is running. It retires with the record, which is rule C: the staged run the - // bytes arrived in belongs to somebody else by the next frame. - // - // NULL UNDER MONOLITH, and that is the arm selection and not an omission (ruling 1): - // the push-monolith build keeps its frontend arms token for token, so its twin reads - // the frontend's archive as it always has and this pointer is never consulted. + // ---- The program archive the record owns ----------------------------------------- + // A snapshot of this link's artefacts, so every reflection read by the program twin is + // of memory the record owns: ProgramObject::Link() replaces the frontend's artefacts in + // place, and a client running ahead may already have relinked. Under a transport the + // decoder deserialises it from SEG_STAGE; in one process PipeRoute copies it without the + // codec. A SharedPtr because a program-pipeline composite and its stage programs share + // one link's artefacts and the twin holds it across a build. Every create replaces it. SharedPtr Archive; // ---- P5e's set_program_bindings tails (CONTRACT-P5E.md §1, §5.5) ------------------- @@ -563,35 +470,13 @@ namespace MobileGL::MG_Pipe { }; // ===================================================================================== - // P14 S5: THE SHARE-GROUP HALF OF THE APPLIER'S STATE. + // The share-group half of the applier's state (design/11-state-ownership.md). // ===================================================================================== - // - // THE TWO HALVES HAVE DIFFERENT LIVES, and until this slice they were one struct with one - // owner (the process), which is what made a second served context either share working state - // it must not share or lose object records it must not lose. They are two structs now because - // they are two OWNERS: - // - // MGPipeObjectRecords a GL object lives in a SHARE GROUP, so these records are keyed by - // {session, share group}: two contexts of one group see one set, two - // NON-shared contexts of one session see two sets - which is the whole - // reason the frontend gives each context its own name space - // (MG_State/GLState/ShareGroupState). A GL object survives a - // make-current and its record is the only thing the backend has left to - // read that object's storage extent and mutation serial out of (D-A4 - // re-keys IsBufferDrawClean onto exactly those two); dropping them at a - // make-current made every subsequent glBufferSubData on a pre-existing - // buffer resolve to nothing. - // - // MGPipeWorkingState what the next draw fetches with - bindings, current program, the unit - // windows, the CSO store, the serials. Keyed by {session, context}: a - // returning context may not inherit another's bindings, and two - // contexts of one share group may not overwrite each other's. - // - // THEY ARE CLEARED BY DIFFERENT EVENTS. A record here is cleared by the object's OWN death - // signal - resource_destroy, delete_vertex_elements, which is what D-L makes the buffer's death - // crossing - and by the share group's own teardown (MGPipeApplierReleaseGroupRecords) when its - // last context goes away. MGPipeApplierReset does NOT touch this struct; MGPipeApplierState's - // working half is what a make-current resets. + // GL objects live in a share group, so these records are keyed by {session, share group}: + // contexts of one group see one set, unshared contexts see separate sets. A make-current + // keeps them, because the backend reads a buffer's extent and serial from its record. A + // record is cleared by its object's death signal, or by MGPipeApplierUnregisterContext when + // the group's last context goes; MGPipeApplierReset clears only MGPipeWorkingState. struct MGPipeObjectRecords { // ---- object records: indexed by MGPipeHandle::Slot of kind Buffer / // VertexElementsCso, and NOT part of the working state. They are cleared by the object's @@ -621,23 +506,12 @@ namespace MobileGL::MG_Pipe { // handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it // exactly as they name any other program. Vector CompositeShaderCsos; - // AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot, - // for the reason MGPipeFramebufferRecord states: the two bound-target records the phase - // started with could not describe a framebuffer that is bound to neither binding, and - // the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand - // Espryt exactly that. - // - // IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also - // its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared - // between contexts, but its record is addressed by a slot out of one global allocator, - // so nothing aliases across a switch - and dropping the table would leave a - // DSA-only framebuffer with no record and no event that would ever re-emit one (the - // client's suppressor invalidation re-emits the two BOUND records and nothing else). - // - // P14 S5 keys it by SHARE GROUP like its siblings, which is safe for the reason just - // given: the slot space is global, so a group's table can only ever answer for handles - // its own contexts minted, and a shared group holding both contexts' records is a wider - // table rather than an aliasing one. + // Framebuffer records, keyed by the framebuffer handle's slot (see + // MGPipeFramebufferRecord), so a framebuffer bound to neither binding - the target of a + // DSA blit or clear - still has one. MGPipeApplierReset keeps this table: on a fresh + // context the client re-emits only the two bound records, so a framebuffer described + // only by name would lose its record. Slots come from one global allocator, so a share + // group's table answers only for handles its own contexts minted and never aliases. Vector FramebufferRecords; }; @@ -654,24 +528,13 @@ namespace MobileGL::MG_Pipe { Vector RenderStateCsos; // The last bind, so a rebind of the same handle can be answered without a scatter. MGPipeHandle BoundRenderStateCso = kMGPipeNullHandle; - // The GLOBAL chunk bits (MGPipeRenderStateSpans.h's numbering) this applier has - // itself scattered into the working block since the last reset - its own ledger of - // which bytes of PipeInputs::m_renderState are the APPLIER'S rather than the per-verb - // fill loop's. Both trip wires arm off it, and that is the whole of their contract: - // - // - with the render-state subsystem OFF (MOBILEGL_PIPE_PUSH bit 0 clear - the - // per-subsystem A/B of D14) nothing is ever scattered, the ledger stays empty and - // the wires say nothing. The working block is then the fill loop's, published per - // VERB CLASS (MG_Pipe/FillPoints.def), so at a kDispatch or kTextureOp verb - the - // two classes that publish IsCapabilityEnabled but NOT GetRenderStateParameters - - // it still holds the previous draw's bytes and is an oracle for nothing; - // - with it ON the applier is the block's only writer (D5 takes an emitted field - // out of the fill loop), so the bytes it has scattered are current at every verb - // of every class and comparing against them is honest. - // - // set_patch_state's own write to the working block deliberately does NOT enter the - // ledger: that is the OTHER carrier, and a wire comparing against bytes it had just - // written itself would be a tautology. + // The global chunk bits (MGPipeRenderStateSpans.h numbering) this applier has scattered + // into PipeInputs::m_renderState since the last reset: which bytes of the working block + // are the applier's rather than the residual fill loop's. MGPipeApplySetPatchState's + // trip wire compares against chunk P0 only once P0 is in this ledger: a set_patch_state + // before a context's first bind has nothing to agree with, and bytes the applier did not + // scatter are no oracle. set_patch_state's own write stays out of the ledger: comparing + // that carrier against itself would prove nothing. Uint32 ScatteredChunkBits = 0; // What the two trip wires last did. A wire nothing can observe is a gate that cannot @@ -691,57 +554,23 @@ namespace MobileGL::MG_Pipe { // build. Per context, like the four render-state wire counters above. Uint64 RefusedResourceCalls = 0; Uint64 RefusedVertexInputCalls = 0; - // P4a's, in the same shape and for the same reason: every sampler, sampler-view, - // program and texture-params call this applier refused because it named a record this - // applier does not have. One counter rather than four, because the families share one - // legal refusal sequence (teardown -> MGPipeApplierReleaseObjectRecords -> ~Object -> - // death notices naming records already dropped) and an operator reading a log wants to - // know that ANY object call was dropped; the log line names the call and the handle. - // - // set_framebuffer_state IS DELIBERATELY NOT ON THAT LIST AND CANNOT BE. D-I2 gives a - // framebuffer a handle and NO wire lifetime, so the call resolves no record - there is - // nothing to look up, nothing to find missing and therefore nothing to refuse - and - // MGPSurface::Res is likewise left unresolved on purpose (D-I3: the keep-alives are the - // frontend's SharedPtrs and enforcing them is a later phase's). Its only verdict is - // Fatal{ProtocolCorruption} on a malformed record, and this counter must stay at 0 - // across every framebuffer call in every build. ID-19(b) does not change that: the - // per-object table is WRITTEN by that call and never looked up by it, and the refusal - // that the table CAN produce - a lookup whose generation has moved on - happens on the - // server's own read path and is counted apart, in StaleFramebufferRecordLookups. - // - // THE OTHER CLASS IS NOT COUNTED HERE AND MUST NOT BE: a var-tail window outside its - // bound, or a set_texture_params whose BuiltinSampler is the null handle, would make - // the backend act outside its own storage or sample an object that does not exist - - // that is Fatal{ProtocolCorruption}, not a dropped call. + // Sampler, sampler-view, program and texture-params calls refused because they named a + // record this applier does not have. One counter, because the families share one legal + // refusal sequence (a teardown drops records before the objects' death notices arrive); + // the log line names the call and the handle. set_framebuffer_state never moves it: a + // framebuffer has no wire lifetime, so the call looks nothing up, and a stale lookup on + // the server's read path counts in StaleFramebufferRecordLookups. A malformed record (a + // var-tail window out of bounds, a null BuiltinSampler) is Fatal{ProtocolCorruption}. Uint64 RefusedObjectCalls = 0; - // P4a's BELT (ID-39): every call in one of the four families P4a migrates that this - // applier declined because NO BACKEND HAS REGISTERED MGPipeResourceOps - i.e. because - // nothing in this process consumes what the record publishes. - // - // WHY THE APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL, when it is otherwise - // backend-neutral: acceptance is a CONTRACT WITH THE CLIENT since ID-18 M3. The - // emitters clear a texture level's dirty flags, advance their descriptor mirrors and - // latch their suppressors on the answer this applier returns, so an applier that - // accepts a record nothing will ever read makes the client forget work the legacy pull - // path still owed - which is exactly how 66 texture-upload-shaped DirectVulkan cases - // went red on the push build (ID-39). The client's own gate - // (MG_Impl/Pipe/PipeFill.cpp's FamilyIsLive) stops the emission upstream; this is the - // belt under it, so a record that reaches here by any other route - GL_Framebuffer.cpp's - // PipePublishFramebufferByName calls its emitter directly, without passing PipeFill - - // is declined rather than accepted. - // - // IT IS NOT A DEFECT COUNTER, WHICH IS WHY IT IS SILENT. RefusedResourceCalls, - // RefusedVertexInputCalls and RefusedObjectCalls each mean "a record named something - // this applier should have had"; a non-zero value there is a seam defect. A non-zero - // value HERE is the designed steady state of a backend with no P4a twins, so logging it - // would put an ERROR line in every ordinary Magma run. The number is the observable. - // - // THE DEATH PATHS ARE DELIBERATELY NOT ON THIS LIST. resource_destroy, - // delete_sampler_state, delete_sampler_view and delete_shader_state are idempotent - // cleanup that must keep working whatever the registration did, and with no consumer - // there is no record for them to find anyway (they count their own refusal). Per - // context and cleared by MGPipeApplierReset, like the three above it. + // Non-buffer resource, sampler, program and framebuffer calls declined because nothing + // consumes them: no MGPipeResourceOps is registered and, under a transport, the session's + // caps do not name the family either. The client clears dirty flags, advances its mirrors + // and latches suppressors on acceptance, so accepting a record nobody reads loses work. + // PipeFill.cpp's FamilyIsLive stops these upstream; this catches emitters called directly, + // such as GL_Framebuffer.cpp's PipePublishFramebufferByName. Not a defect, so not logged. + // Idempotent death paths (resource_destroy, delete_*) are not counted. Cleared by + // MGPipeApplierReset. Uint64 RefusedNoConsumer = 0; // ---- working state: what the next draw fetches with. All of it is per context and @@ -894,24 +723,13 @@ namespace MobileGL::MG_Pipe { // both roles evaluate the same predicate over the same value (§2.1, escalation (i)). Bool IsTransformFeedbackActive = false; - // ---- P5c (rv, CONTRACT-P5C.md §5.3): the server-side answer for the three texture - // SHUTTERS. FieldOwnership.def moves GetSamplingResolutionGeneration / - // GetTextureBindGeneration / GetTextureContextId to APPLIER_DERIVED - "a shutter, not - // a value: the server answers from its own Serial", which their rows have said since - // P5 - and THESE are the serials, read by the PipeInputs accessors under a - // server-stamped verb instead of the client's residual-fill copy. - // - // TextureShutterSerial answers the two GENERATIONS. It is an MGGen like its seven - // siblings above: server-owned, monotone, ++ on every applied record that can move - // what the frontend's bind / sampling-resolution generations guard - the three unit - // sets, set_texture_params, a respecify or destroy of any resource, a bind_shader_image - // verb (the sink bumps it, PipeApplier.cpp) - and ADVANCED, never zeroed, by - // MGPipeApplierReset / MGPipeApplierReleaseObjectRecords. Over-firing is the safe - // direction for a memo key: a moved serial costs a re-sync, a stuck one renders stale. - // - // ContextSerial answers GetTextureContextId: stable within the served context, moved - // by every MGPipeApplierReset (a make-current is a fresh server, §5.1), which is all - // the backends' per-context memo keys need. + // ---- Server-side answers for the three texture shutters (FieldOwnership.def, + // APPLIER_DERIVED): the PipeInputs accessors answer from these serials under a + // server-stamped verb. TextureShutterSerial answers GetSamplingResolutionGeneration and + // GetTextureBindGeneration; it moves on every applied record that can change what they + // guard (the unit sets, set_texture_params, a resource respecify or destroy, + // bind_shader_image) and resets advance it, never zero it. Over-firing only costs a + // re-sync. ContextSerial answers GetTextureContextId; every MGPipeApplierReset moves it. Uint64 TextureShutterSerial = 0; Uint64 ContextSerial = 0; @@ -1035,22 +853,12 @@ namespace MobileGL::MG_Pipe { TextureShutterSerial = ContextSerial = base; } - // ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than - // members because the storage moved under them and their callers must not have to know - // it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members - // used to answer - "which record describes the framebuffer bound to this binding" - by - // resolving BoundFramebuffer[t] through FramebufferRecords. - // - // NULL IS A REAL ANSWER AND HAS EXACTLY THREE CAUSES: nothing is bound to that binding - // (the null handle, which is what a make-current leaves and is NOT an error), no record - // has been written at that slot, or the slot's generation has moved on under the handle - // (which IS an error and is counted and logged - see StaleFramebufferRecordLookups). A - // caller that used to test `MGPipeHandleIsNull(st.DrawFramebuffer.Fbo)` tests the - // pointer instead; the two are the same question. - // - // Defined in PipeApply.cpp rather than inline HERE so this header keeps its include - // closure: the stale-generation path logs, and MG_Util/Debug/Log.h is not in this - // header's closure and may not become part of it. + // ---- The framebuffer accessors. DrawFramebuffer() / ReadFramebuffer() resolve + // BoundFramebuffer[t] through FramebufferRecords. Null has three causes: nothing is + // bound (what a make-current leaves; not an error), no record was written at the slot, + // or the slot's generation moved on under the handle (an error, counted in + // StaleFramebufferRecordLookups and logged). Defined in PipeApply.cpp because the + // logging path needs MG_Util/Debug/Log.h, which must stay out of this header's includes. const MGPFramebufferState* FramebufferRecordFor(MGPipeHandle fbo) const; const MGPFramebufferState* DrawFramebuffer() const; const MGPFramebufferState* ReadFramebuffer() const; @@ -1088,29 +896,13 @@ namespace MobileGL::MG_Pipe { void MGPipeNotifyCompileEnvChanged(); // ===================================================================================== - // P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT? + // Whose applier is it? (docs/Disaggregated/design/11-state-ownership.md) // ===================================================================================== - // - // THE SAME SHAPE AS P14 S2's gPipeInputs RESOLVER AND S4's NATIVE TUPLE KEY, one level down: - // a process-wide hook the server's session registry installs ONCE, which answers for the - // CALLING thread. Nothing in MG_Backend or MG_Impl names a session, and no call site moved. - // - // THE KEY IS TWO PAIRS, because the two halves have two owners: - // - // {SessionKey, ContextToken} the WORKING state, one per served context; - // {SessionKey, ShareGroupKey} the OBJECT records, one per share group within the session. - // - // A SHARE GROUP DOES NOT CROSS A SESSION, and this is not a limitation: an EGL share group - // is a property of one client's connection (EGL 1.5 3.7.1 - contexts of one share group must - // belong to one display), and the two sessions are two connections. Cross-session sharing is - // an explicit non-goal of the phase, so keying the first by session is the whole of the rule. - // - // REGISTRATION, NOT INFERENCE. The session layer registers a context the moment the - // CreateContext control frame reaches the apply thread (ServerSession::CreateContext) and - // unregisters it at DestroyContext, so the resolver above stays a two-word read of state the - // session already keeps (its current token) and never has to reach into the session's map - // from a thread that does not own it. A context that was never registered resolves to its - // session's "no context" applier, which is the same object the session's bring-up reads. + // A resolver the session registry installs once answers for the calling thread, so + // MG_Backend and MG_Impl never name a session. Working state is keyed by {SessionKey, + // ContextToken}, object records by {SessionKey, ShareGroupKey}; a share group never spans + // sessions (EGL ties it to one display, i.e. one client connection). Contexts are registered + // on the apply thread at CreateContext; an unregistered token gets a group of its own. struct MGPipeApplierKey { Uint64 SessionKey = 0; Uint64 ContextToken = 0; @@ -1149,43 +941,14 @@ namespace MobileGL::MG_Pipe { // invent. Uint64 MGPipeApplierShareGroupKeyFor(Uint64 sessionKey, Uint64 contextToken); - // --------------------------------------------------------------------------------- - // P5e: THE BARRIERED PREDICATE (docs/archive/contracts/CONTRACT-P5E.md §2.1, ruling 3) - // --------------------------------------------------------------------------------- - // - // ONE FUNCTION, TWO CALLERS, AND THAT IS THE WHOLE POINT. The client calls it at the emit - // table to decide whether to wait after publishing; the sink calls it in ApplyOne to decide - // whether the record it is about to apply may read client memory. If the two answers ever - // differ the failure is silent: the client runs on while the server reads a value that has - // already moved. So it is a PURE FUNCTION OF WIRE-VISIBLE DATA - the static wait-class - // column, the record's own payload, and applier state that a record earlier on the ring put - // there - and both roles evaluate the same three clauses: - // - // 1. MGPipeWaitClassFor(op) != kWaitNone the static column (§2.2) - // 2. kCtxVerb with an open transform-feedback span escalation (i): XFB stays lockstep - // 3. draw_vbo carrying kDrawClientArrays escalation (ii): client vertex arrays - // - // NO OTHER RUNTIME ESCALATION EXISTS. Adding one is an integrator ruling and a row in the - // contract, not a condition somebody adds at a call site - because a fourth clause that only - // one side computes is the same silent failure one level down. - // - // THERE WAS AN ESCALATION (iii) AND IT WAS WITHDRAWN, which is worth a line here rather than - // only in the history: ID-133 barriered a plain multi-draw to legalise a pull in Espryt's - // indirect multi-draw tier, ID-136 retired the pull instead (MultiDraw.cpp's - // BoundDrawIndirectBufferId) and took the clause back out. The rule it leaves behind is the - // one to apply to the next candidate: ask WHICH ARM PAYS for an escalation, not which lane - // it turns green - (iii) was keyed on the only wire fact available and therefore charged the - // default tier for a read only two opt-in tiers made. - // - // Escalation (ii) is a REFUSAL under run-ahead (§5.1), so on a run-ahead server it never - // reaches the sink at all; it is here so the predicate is TOTAL and so the lockstep arm, - // where such a draw is legal, gets the barrier it needs. - // - // `payload` may be null, which answers "the fixed payload is not available here" and makes - // clause 3 fall through - a caller that has an opcode but no bytes (the emit table before it - // has built the record) must not be told a draw is unbarriered on that account, so DrawVbo - // with a null payload is treated as carrying no client arrays and the emit site passes the - // real MGPDrawInfo it is about to publish. + // ---- The barriered predicate (ARCHITECTURE.md §11.6.1) -------------------------------- + // The server's ApplyOne asks it whether the record it is about to apply may read client + // memory; the client evaluates the same clauses before it publishes (ClientSession's wait + // rule, PipeFill.cpp's ClientVerbIsBarriered). It reads only wire-visible data (the static + // wait class, the payload, applier state set by earlier records), so both sides can agree. + // Clauses: wait class != kWaitNone; a kCtxVerb inside an open transform-feedback span; a + // draw_vbo with kDrawClientArrays. A new clause goes here and into the client's mirror in + // the same change. A null `payload` skips the last clause. Bool MGPipeBarriered(MGPWireOp op, const void* payload, const MGPipeApplierState& st); // Whether the record the apply thread is INSIDE is a barriered one. Server-private: written @@ -1204,50 +967,22 @@ namespace MobileGL::MG_Pipe { // the call site (it is the line ra rebases onto); c0e owns the storage so both compile. void MGPipeApplierSetCurrentRecordBarriered(Bool barriered); - // ---- P5e (gl), ID-128: WAS THIS RECORD BARRIERED BY ESCALATION? ---------------------- - // - // MGPipeBarriered is the static wait class PLUS two payload-derived escalations (ID-83): an - // open transform-feedback span makes every context verb inside it barriered, and a draw - // carrying client vertex arrays makes that draw barriered. "Escalated" is therefore exactly - // - // MGPipeBarriered(op, payload, st) && MGPipeWaitClassFor(op) == kWaitNone - // - // and BOTH HALVES ARE ALREADY COMPUTED on every record - ApplyOne computes the predicate - // unconditionally for ID-103's reason and the static class is a table - so this is a bit - // carried beside the existing stamp rather than new plumbing. - // - // IT IS A SEPARATE FLAG AND NOT A REFINEMENT OF THE ONE ABOVE, because the two answer - // different questions. The stamp above answers "is the client parked behind this record", - // which the allocator guard and the frontend-keyed registry guard ask. This one answers "and - // was it parked for a reason the STATIC table cannot see", which only the strict knob asks - - // and it asks because the escalations happen exactly for XFB-active and client-array draws, - // both of which CONTRACT-P5E puts outside this phase (§5.7 and ID-82). A pull on such a - // record is a debt some later phase owes, not a migration P5e skipped. - // - // FALSE IS THE DEFAULT, which is the opposite of the stamp's default and deliberately so: - // "not escalated" is the answer that admits nothing, so a reader that runs before any writer - // is strict rather than lax. + // ---- Was the current record barriered only by escalation? ---------------------------- + // True when MGPipeBarriered(op, payload, st) holds but MGPipeWaitClassFor(op) == kWaitNone: + // the transform-feedback or client-array clause alone barriered it. Separate from the stamp + // above, which answers "is the client parked behind this record" for the allocator and + // registry guards. The only reader is the residual-pull check under + // MOBILEGL_IPC_STRICT_ERRORS (PipeInputs.cpp), which admits a pull on an escalated record. + // Defaults to false, so a reader that runs before any writer is strict. Bool MGPipeApplierCurrentRecordIsBarrieredByEscalation(); void MGPipeApplierSetCurrentRecordBarrieredByEscalation(Bool escalated); - // THE CLIENT'S HALF OF THE WAIT RULE IS NOT LANDED YET, AND THIS CONSTANT IS THAT FACT. - // MGPipeBarriered above describes what the client WILL do; until package ra changes - // EmitAndWaitTails the client still blocks after every record, so the answer the server - // must stamp is `true` whatever the wire says - the two named exemption scopes (§4.4) are - // legal exactly while the client is parked, and it is parked behind all of them today. - // IT FLIPS WITH kMGPipeP5eRunAheadReady, IN THE INTEGRATION COMMIT, AND NOT WHEN ra LANDS - // (ra's amendment to ID-103's wording, which said "the same commit that lands the wait - // rule"). The argument ID-103 makes is about whether the CLIENT is parked, not about - // whether ra's code exists: RunAheadArmed() is a conjunction whose third term is the - // server's caps bit, so between ra landing and the caps bit being published the client - // still blocks after every record - and stamping `false` there would withdraw §4.4's - // exemptions from probes that client's own wait still makes safe, which is exactly the - // "refusal with no defect behind it" ID-103 refused. The two constants are therefore ONE - // switch with two spellings, and the integration commit throws both. - // - // Computing the predicate anyway (PipeApplier::ApplyOne) is deliberate: it keeps the - // function exercised on every record for the whole phase rather than first run on the day - // it starts deciding. + // Whether the client implements the run-ahead wait rule MGPipeBarriered describes. The + // server stamps a record with the predicate only when this and its own run-ahead cap bit + // both hold (MGPipeApplierStampsBarriered); otherwise the client blocks after every record, + // so every record is stamped barriered. It moves together with kMGPipeP5eRunAheadReady + // (MG_Backend/Init.cpp). ApplyOne computes the predicate on every record either way, so it + // stays exercised. inline constexpr Bool kMGPipeP5eClientWaitRuleLanded = true; // P5c (rv): the two serials the PipeInputs texture-shutter accessors answer with under a @@ -1261,43 +996,14 @@ namespace MobileGL::MG_Pipe { // this, so the bump rule lives in exactly one place. void MGPipeApplierNoteTextureStateMoved(); - - // A MAKE-CURRENT, NOT A TEARDOWN - and the distinction is the whole of this function's - // contract. It runs on every change of the current GLContext (MGPipeTracker::Update resets - // the tracker whenever the context pointer moves, and the emitter calls this from the - // first walk that follows), including a make-current BACK to a context that is still alive - // and whose objects are all still there. - // - // So it drops what a returning context may not inherit - the render-state CSOs (whose - // client-side cache is dropped on the line above it, so both sides start over together), - // the residual mirror, and the vertex-input WORKING state - and it ADVANCES the two global - // vertex-input serials rather than zeroing them. It does NOT drop the resource or - // vertex-elements records: those describe share-group objects that the switch does not - // destroy, and dropping them is a dropped write on the far side of it. - // - // P4a EXTENDS BOTH HALVES AND THE RULE IS UNCHANGED (D-J4). Cleared: the two framebuffer - // BINDINGS, the three unit sets, DrawProgram / DispatchProgram / BoundShaderCso - all of it - // per-context working state - with their serials ADVANCED and never zeroed. Not cleared: - // texture and renderbuffer resources, sampler CSOs, sampler views, shader CSOs, the - // framebuffer RECORDS, and the texture params and pending uploads that ride on a resource - // record, because a texture lives in a share group exactly as a buffer does. - // - // ID-19(b) MOVED THE FRAMEBUFFER RECORD ACROSS THAT LINE and the reason is worth stating. - // Before it, the whole framebuffer state was working state and a make-current took it. Now - // the RECORD is an object record and only the two BOUND HANDLES are working state, so a - // switch clears the bindings - after which DrawFramebuffer() / ReadFramebuffer() answer - // null, exactly as the cleared records used to answer a null Fbo - and leaves the table - // standing. Dropping the table instead would silently lose the record of every framebuffer - // that is described by NAME and never bound, because the client's re-emission on a fresh - // context is driven by MGPipeSetHashSuppressor::InvalidateAll, which re-sends the two bound - // records and nothing else. - // - // AND THEREFORE NO P4a TRACKER NEEDS A RE-PUBLICATION PATH ON FreshlyPrimed, AND NONE MAY - // HAVE ONE: re-emitting create_sampler_state for a record the applier still holds would - // move its Serial for nothing. What DOES reset on a fresh context is each emitter's - // BOUND-HANDLE latch - the framebuffer and unit-set hashes through - // MGPipeSetHashSuppressor::InvalidateAll, and the program emitter's BoundShaderCso mirror - - // because those mirror working state this function just cleared. + // A make-current, not a teardown: runs on every change of the current context (the tracker + // resets when the context moves), including a switch back to a live context. It clears the + // working state a returning context may not inherit - render-state CSOs (the client drops + // its cache too), vertex input, framebuffer bindings, unit sets, buffer binding windows, + // program handles - and advances their serials rather than zeroing them. Object records + // survive: they belong to the share group. So on a fresh context emitters re-send only + // bound-handle state (MGPipeSetHashSuppressor::InvalidateAll); re-sending an object record + // would move its Serial for nothing. void MGPipeApplierReset(); // THE OTHER SCOPE: the served context is going away and its applier with it, so the object @@ -1415,93 +1121,26 @@ namespace MobileGL::MG_Pipe { // 0, a descriptor whose target names no resource kind, and a slot at or above // kMGPipeMaxResourceSlots. Bool MGPipeApplyResourceCreate(const MGPResourceDesc& desc); - // resource_respecify: replaces the stored descriptor and bumps Serial. `initialBytes` is - // the shadow when desc.HasDefinedContent, else null. kNeedsAck on the call, - // MGPipeResourceRespecifyNeedsAck(desc) per record - only an immutable store acks. - // - // P4a: `level` IS THE SCOPE OF THE REDEFINITION, and MGPResourceDesc cannot carry it - the - // descriptor describes the resource, and a mutable texture redefines its levels ONE - // glTexImage*D AT A TIME. Null means "this respecify redefines the WHOLE resource" - every - // glBufferData / glBufferStorage, every glTexStorage*, every texture view - and drops every - // pending upload, which is right because every level's coordinate system has just been - // replaced. Non-null names the single (uploadTarget, level) the call redefines and drops - // ONLY that key: the frontend's AllocateStorage / MarkStorageDirty are per - // (uploadTarget, level) as well (MG_State/GLState/TextureState/TextureObject.h), so a - // glTexImage2D(level 1) re-marks level 1 AND NOTHING ELSE, while the levels already - // emitted had their client dirty flags cleared at THEIR emission (D-D5 step 1) and nothing - // anywhere still owes them. Clearing the whole set here would lose exactly those texels, - // silently, in every build - the loss the server-side set exists to prevent. - // - // Trailing and defaulted for W1's reason: P3a's buffer call site (PipeFill.cpp:691) and - // every existing case compile unchanged. PACKAGE B PASSES THE PAIR IT JUST ALLOCATED at - // every per-level respecify; it has both halves in hand at the AllocateStorage call site. - // - // A METADATA RESPECIFY IS A RESPECIFY THAT REDEFINES NO STORAGE (ID-18 M4). A sticky - // BindMask / ImageBindableHint bit reaches the applier only on a respecify, and an - // IMMUTABLE texture has no further one - that is what immutable means - so the canonical - // order (glTexStorage2D, then glBindImageTexture or an FBO attachment) would leave the - // record's hint at 0 for ever, and the hint is what PREVENTS the texture re-mint (a - // server-side readback and re-upload, P9 W2). So B re-emits the descriptor when the mask moves, and a record whose - // STORAGE-DEFINING fields all equal the stored descriptor's is applied as a metadata - // update: - // - // - the descriptor is replaced, so BindMask and ImageBindableHint take their new values; - // - NO pending upload is dropped, whatever `level` says. This REFINES the rule above - // rather than contradicting it: that rule drops the uploads against the storage a - // respecify REPLACES, and a call that replaces no storage replaces no coordinate system - // either, so there is nothing to drop. A mask change arriving between a - // glTexSubImage2D and the sync that consumes it must not eat the texels; - // - the serial advances, which is the whole publication - the twin re-derives its storage - // flags from the new mask at its next sync and recreates only where the backend needs - // it (D's side); - // - and MGPipeResourceRespecifyNeedsAck is false for it BY CONSTRUCTION, because a buffer - // is never classified this way (see the body: glBufferData at an unchanged size is a - // real orphaning reallocation, and glBufferStorage is the one entry point allowed a - // synchronous ack). - // - // Returns true when the descriptor was stored - metadata updates included, since the record - // did move - and false when the call was refused: a descriptor whose target names no - // resource kind, or a handle this applier has no live record for at that generation. - // SHARED IMAGES: the texture `texture`'s level-0 storage becomes shared image `imageId` - // (MGPipeResourceRecord::SharedImageId). False when the texture has no live record. + // MGPipeApplyResourceRespecify (below) replaces the stored descriptor and bumps Serial; + // `initialBytes` is the shadow when desc.HasDefinedContent, else null, and only an immutable + // buffer store acks (MGPipeResourceRespecifyNeedsAck). A null `level` redefines the whole + // resource and drops every pending upload; a named level drops only its own key. A null + // level with unchanged storage-defining fields is a metadata update (how a BindMask or + // ImageBindableHint change reaches an immutable texture): it drops nothing, moves Serial. + // Returns false when refused. MGPipeApplyAttachSharedImage makes `texture`'s level-0 storage + // shared image `imageId` (MGPipeResourceRecord::SharedImageId); false without a live record. Bool MGPipeApplyAttachSharedImage(MGPipeHandle texture, Uint64 imageId); Bool MGPipeApplyResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, const MGPRespecifiedLevel* level = nullptr); - // resource_subdata, buffer half: the destination range rides in the record's box through - // MGPipeSetSubDataBufferRange, and a false from that helper is where the EMITTER split. - // The applier stores nothing per record - contents are the backend's - and bumps Serial. - // - // P4a: `regions` IS THE CALL'S VARIABLE TAIL - MGPSubRegion[record.RegionCount] - and it is - // a trailing DEFAULTED parameter rather than a second entry point. The call has carried - // kVarTail since P2 (PipeCalls.def) and the texture half cannot be applied without it: the - // applier's pending-upload set is (UnionBox, RegionCount, Regions[]) and the verify lane's - // retain mode compares all three. The buffer half declares no regions, so P3a's one call - // site and every existing case are unchanged by the default. - // - // THE RETURN IS THE ACCEPTANCE SIGNAL D-D5 STEP 1 NAMES: true when the record was stored - - // the buffer half landed its range, or the texture half accumulated the shape onto the - // record - and false when it was refused. THE EMITTER MUST GATE ITS DIRTY-FLAG CLEAR ON IT - // ("only for levels whose record the applier ACCEPTED"), because the two refusal paths are - // otherwise invisible to it: a dead or stale handle is a counted no-op and a corrupt record - // is a Fatal that does NOT move RefusedResourceCalls, so in a shipped push build a refused - // upload and an accumulated one are indistinguishable from the call site. A client that - // clears on the strength of having emitted drops those texels for good. - // - // THE RESOURCE-TARGET HALF OF record.Target PICKS THE HALF. MGPSubData::Target is PACKED - // (ID-12): low byte = MGPipeResourceTarget, high byte = the cube-face upload target. The - // buffer half is the whole field being 0 - the encoding the emitter is held to, since a - // buffer has no upload target - and the texture half additionally requires the low byte to - // name a TEXTURE target: Buffer, Renderbuffer and anything at or above - // MGPipeResourceTarget::Count are Fatal{ProtocolCorruption} rather than an upload onto - // whatever object holds that slot in the texture slot space. - // P13 W4b: A WRITE THE SERVER PRODUCED, going through the ordinary resource_subdata gate - the - // pack-buffer landing of a readback (PipeApplier.cpp LandReadbackInBuffer) is the one producer. - // Its bytes are the server's own scratch and live for the call only, so a store that ALIASES - // the frontend's shadow (monolith's record arm) must not take them as its base: it lands them - // in the GL store and marks it GPU-written instead, which is how the frontend and every CPU - // reader learn of them (SyncGpuWrites / SplitHostBytesForCpuRead). A copying store takes them - // as it always has. Thread-local; scoped by the producer. + // MGPipeApplyResourceSubData (below): the buffer half takes its range from the record's box + // (MGPipeSetSubDataBufferRange); the texture half adds `regions` (MGPSubRegion[RegionCount]) + // to the pending-upload set. record.Target packs the resource target (low byte) and the + // cube-face upload target; a buffer is the whole field 0, and a non-texture target on the + // texture half is Fatal{ProtocolCorruption}. Emitters clear dirty flags only on a true return. + // MGPipeServerOriginatedWriteScope marks a server-produced write (a readback landing in a pack + // buffer): its bytes live for the call only, so a store aliasing the frontend's shadow lands + // them in the GL store and marks it GPU-written instead of adopting them. Thread-local. Bool MGPipeApplyingServerOriginatedWrite(); class MGPipeServerOriginatedWriteScope { public: @@ -1573,53 +1212,22 @@ namespace MobileGL::MG_Pipe { void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record); // --------------------------------------------------------------------------------- - // P4a: the fifteen object and working-state entry points (D-A1, D-B1, D-J1) + // The object and working-state entry points // --------------------------------------------------------------------------------- - // - // NOT ONE OF THEM DISPATCHES TO A BACKEND FUNCTION POINTER, and that is the single most - // important structural decision in P4a rather than an omission. Nothing in these families - // reaches the backend at GL-call time today - texture storage only marks a level dirty and - // Espryt allocates lazily at sync, texture params run from SyncTextureObjectToBackend at - // draw sync, renderbuffer storage is allocated inside SyncToBackend on a four-field cache, - // a sampler twin is created lazily from the program pass, and the framebuffer, unit sets - // and program are all resolved at PrepareForDraw. So every call below is either an OBJECT - // RECORD the applier stores or WORKING STATE the applier stores, and Espryt reads the - // applier at the sync points it already has, keyed on a server-owned Serial instead of a - // frontend version. MGPipeResourceOps is therefore UNCHANGED - nine members, same - // signatures - and P4a adds no backend op table and no op-table member at all. - // - // The consequence for the four resource entry points above: they BRANCH on - // record.Desc.Target. A buffer target dispatches into MGPipeResourceOps exactly as P3a - // wrote it; every other target stores and returns. The branch is one comparison against - // kMGPipeResourceTargetBuffer and it is where a mis-typed descriptor becomes visible. - // - // The bodies are in PipeApply.cpp. The signatures are what the client, the backend and the - // gates compile against, and the records above are what they write into. + // None calls into the backend: each stores an object record or working state, which the + // backend reads at the sync points it already has (texture and renderbuffer storage, + // framebuffer, unit sets and program are resolved at draw sync), keyed on server-owned + // Serials. The resource entry points above branch on the target: a buffer reaches the buffer + // members of MGPipeResourceOps, a texture only the optional Texture* members. - // set_framebuffer_state. Fully resolved - nothing in the record requires a lookup on the - // far side. ContentHash covers every field including Fbo and DrawBuffers[8], which is what - // makes a suppressed record provably mean "the draw-buffer array did not move" and - // therefore "the fragColor broadcast count did not move". - // - // `state.Target` NOW SAYS TWO THINGS AT ONCE (ID-19(b)), and the record always does the - // first of them: - // - // - THE RECORD IS ALWAYS WRITTEN, at FramebufferRecords[state.Fbo.Slot], whatever the - // target is. The table is keyed by the framebuffer HANDLE, so one framebuffer's record - // can never displace another's, and a slot whose object has been recycled is simply - // overwritten by its successor's record (D-I2: no wire lifetime, so nothing to retire). - // - Draw / Read / Both ADDITIONALLY set BoundFramebuffer[Draw] / [Read] / both. - // MGPipeFramebufferTarget::Named sets NEITHER: it is how a DSA entry point hands Espryt - // a framebuffer it is about to blit into or clear WITHOUT claiming it is bound. - // - // FramebufferSerial advances on every applied record, Named included. - // - // Two refusals, both Fatal{ProtocolCorruption} and neither counted (see RefusedObjectCalls: - // this entry point resolves nothing and can only ever fault): a target above Named, a - // draw-buffer entry outside the record's own Color[], a slot at or above - // kMGPipeMaxFramebufferSlots, and the NULL HANDLE - a record that named {0,0} would install - // itself where "nothing is bound" is read, and every emitter has a handle for every - // framebuffer it describes (kMGPipeDefaultFramebuffer {0,1} for the default one). + // set_framebuffer_state. Fully resolved; ContentHash covers every field including Fbo and + // DrawBuffers[8], so a suppressed record means the draw-buffer array, and with it the + // fragColor broadcast count, did not move. The record is always written at + // FramebufferRecords[state.Fbo.Slot], overwriting a recycled slot's predecessor; Draw, Read + // and Both also set BoundFramebuffer, Named (DSA blits and clears) sets neither. + // FramebufferSerial advances on every record. Fatal{ProtocolCorruption}, never counted: a + // target above Named, a draw buffer outside Color[], a slot at or above + // kMGPipeMaxFramebufferSlots, or the null handle (the default framebuffer is {0,1}). void MGPipeApplySetFramebufferState(const MGPFramebufferState& state); // create_sampler_state. `parameters` is the client's canonical SamplerParameters copy, @@ -1687,23 +1295,13 @@ namespace MobileGL::MG_Pipe { const MGPProgramStorageOverride* storageOverrides, const char* const* storageOverrideNames); - // create_shader_state. THE ARTEFACTS TRAVEL BESIDE THE RECORD, by pointer: all seven of - // desc.Spirv[] and desc.Reflection are declared with Size 0 ("this record does not declare - // its blob"), which is what a monolith emission is, and the codec is NOT called - zero - // serialisation cost on the monolith path. A verify build serialises, deserialises and - // field-compares before storing, and a mismatch is Fatal{PipeVerifyDiffer, "program-archive"}. - // Splitting this record for a transport whose ring caps one record at half its capacity is - // P5's problem, not this entry point's. - // - // P5e (pg) ADDS THE FOURTH ARGUMENT AND DID NOT DEFAULT IT, for the reason PipeRoute.h - // gives about the three byte counts it also refused to default: a caller that HAS the - // archive and forgets to pass it would compile, and the failure would be a program twin - // reading the frontend's artefacts on the apply thread - silent, and exactly what this - // package exists to end. `archive` is null under monolith (the two companion pointers are - // the frontend's own and the handle arm is not taken there) and non-null under a - // transport, where `link` and `spirv` point INTO it: the record adopts it, so every later - // reflection read is of memory the server owns. A non-null archive whose Link/Spirv are - // not the two pointers passed beside it is a caller bug this entry point asserts on. + // create_shader_state. `link` and `spirv` are the program's artefacts and `archive` is the + // snapshot the record adopts (a null clears the previous one); desc's blob refs are not + // read. Under a transport the decoder passes its deserialised archive and pointers into it; + // in one process PipeRoute copies the frontend's artefacts into a new archive without the + // codec. The verify build round-trips the artefacts through the codec before storing, and a + // mismatch is Fatal{PipeVerifyDiffer, "program-archive"}. `archive` has no default so that a + // caller holding one cannot silently omit it. void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc, const MG_State::GLState::LinkArtifacts* link, const MG_State::GLState::SpirvArtifacts* spirv, diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 44337153..0d4cbf8f 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -6,157 +6,14 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The single source of truth for the MGPipe call catalogue (plan B section 4.1 / 4.4 / -// appendix A). One line per call; seven generators consume this file -// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test -// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic. -// -// X(Name, PayloadStruct, Class, Flags, WaitClass) -// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery -// kScreen lands in struct MGPipeScreen, every other class in struct -// MGPipeContext (plan section 4.3). -// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional -// WaitClass : kWaitNone | kWaitApplied | kWaitReply | kWaitPresent -// P5e (docs/archive/contracts/CONTRACT-P5E.md section 2.2). WHAT THE CLIENT DOES AFTER IT HAS -// PUBLISHED A RECORD OF THIS CALL, on a server that publishes kCapRunAheadApply. -// It is A VALUE AND NOT A NEW OP: the opcode order and the flag column are -// untouched, and gen_pipe.py emits the column beside kMGPipeCallFlags as -// MGPipeWaitClassFor(op) in generated/PipeWire.inc, so the emit table and the -// sink compute the barriered predicate from one table instead of two copies. -// -// kWaitReply the answer is not derivable and the client acts on it (R-5), so -// it blocks on the reply slot. EVERY kReplySlot ROW CARRIES IT and -// gen_pipe.py refuses a row where the two disagree - a reply-slot -// row that did not wait would be a waiter that hangs, and a waiting -// row with no slot would block on an answer nobody posts. -// resource_subdata is here because the row OWNS a slot; the buffer -// half's wantReply=false is a per-record ROUTE decision one level -// down (CONTRACT-P5E.md section 2.5), not a second wait class. -// kWaitPresent present, whose pacing is the present CREDIT taken BEFORE the -// encode rather than a wait after the publish (section 2.4). -// kWaitApplied the record's apply still reads a BARRIER_PULLED row or probes the -// client allocator, so the client parks in its own wait and the row -// keeps CONTRACT-P5C's semantics exactly: its pulls count into rsp -// and its probes are legal inside a scope. This column is the -// static half of MGPipeBarriered(op, payload, applierState); the -// other two escalations (an open XFB span, a draw with client -// vertex arrays) are payload-derived and live in that function. -// kWaitNone rule F holds for this row's sink: its apply names no client -// memory at all, so the client publishes and moves on. -// -// THE COLUMN IS INERT UNTIL THE INTEGRATION COMMIT publishes kCapRunAheadApply -// (MG_Backend/Init.cpp's kMGPipeP5eRunAheadReady): with the bit unpublished the -// client runs today's lockstep path and never reads the table. -// kNeedsAck on a call means records of this call MAY require an ack; a -// per-record predicate decides. resource_respecify carries it for -// glBufferStorage - a real synchronous allocation, and the only entry point -// allowed a synchronous ack - and MGPipeResourceRespecifyNeedsAck(desc) -// (MGPipeTypes.h) is what says so, which is why the same call still carries -// every glBufferData without acknowledging one. -// -// kHasBlob MEANS "THE PAYLOAD OWNS AN MGPBlobRef MEMBER", nothing weaker (P5 -// R-13.1). It is not "this call carries bytes": three calls carry bytes with no -// MGPBlobRef at all and they are named in CONTRACT-P5.md table 1, not flagged -// here, because a decoder that trusts kHasBlob has to find a member to read. -// The flags are EXPORTED, once, as kMGPipeCallFlags[] in generated/PipeWire.inc -// (P5 R-13.4): before that table existed every consumer hard-coded its own copy, -// which is how the two carriers below came to disagree with their payloads. -// -// RECORD NUMBERING NEVER CHURNS. Entries that are not implemented yet still occupy their -// line (plan section 11, P0: "the complete call catalogue, placeholders included"). A new -// call is APPENDED to its group; a retired call keeps its slot with a comment. The wire -// opcode is the 1-based position in this list, so reordering is a protocol break. -// -// --------------------------------------------------------------------------------------- -// COUNTS. MGP_CALL_LIST_DOCUMENTED_COUNT below is the authority; PipeCatalogueTest asserts -// that the expansion, the two generated tables and this number agree. -// -// class entries group (as the plan tabulates it) -// kScreen 14 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the -// appended server-side fence wait 1 and P5c's applier_reset 1 and -// P14 S1's bind_context 1 and the shared-image row 1 -// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2 -// kCtxCso 13 CSO create/bind/delete -// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state -// + P5c rv's set_context_values + P5e's set_program_bindings -// kCtxObject 11 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus -// P5c's object_death 1 and get_texture_image_to_buffer 1 -// kCtxVerb 20 3 context-reading transfer calls + the 10 commands, plus the five -// P5b-appended verbs (bind_shader_image, patch_parameter, -// bind_stream_output, set_storage_block_binding, -// copy_framebuffer_to_texture - docs/archive/contracts/CONTRACT-P5B.md), plus -// delete_stream_output 1 and read_pixels_to_buffer 1 -// total 85 -// -// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they -// do not add up to a set of UNIQUE records and this file has to hold unique records: -// - "screen 14" tabulates the fence and query families together with the screen block. -// Section 4.3 assigns the query NAMESPACE to the context ("VAO / FBO / XFB object / -// query namespaces, the command stream, present"), so the six query calls carry -// kCtxQuery and live in MGPipeContext. Screen keeps 10 of the plan's (11 with the appended -// FenceWaitServer, below). The eight EGL lifecycle entry points stay virtual functions on -// pActiveBackendObject and are deliberately NOT calls here (section 4.4.1, last row). -// - "CSO 15" is create/bind/delete x 5 kinds. Two of those binds are ALSO named in the -// set_* catalogue as their array forms - bind_sampler_states and set_sampler_views -// (section 4.4.3) - and a call may only exist once, so they are emitted under -// kCtxState and the CSO group holds 13: create/delete x 5 plus the three remaining -// binds (render state, vertex elements, shader). -// - "transfer 12" enumerates 11 calls in section 4.4.4 plus appendix A -// (resource_subdata, buffer_subdata_resident, resource_flush_range, resource_readback, -// resource_copy_region, blit, clear, generate_mipmap, read_pixels, get_texture_image, -// resource_subdata_complete). Eleven is what is emitted; the twelfth is not named -// anywhere in the plan. -// - "about 74 items" in section 4.1 is the sum of those headline numbers, so it inherits -// the same double counting. 68 unique records was the honest total of the plan's own -// catalogue. -// - Three LIVE GLFunctionsTable entries had no carrier in it at all: GetGpuTimestampNs -// (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp -// (glQueryCounter, a one-shot stamp rather than a begin/end pair) and WaitSync (the -// GPU-side wait, which FenceWait's client-side wait does not express). They are -// QueryTimestamp, QueryCounter and FenceWaitServer, APPENDED at the end of the list - -// not slotted into their groups - because the wire opcode is the position, so a record -// that arrives late goes last. 71 unique records. -// - P5b appended FIVE more after them (opcodes 72..76), for the same reason and by the same -// rule: five GLFunctionsTable verbs the class-C census measured as first blockers - the -// image bind, the patch parameter, the XFB object bind, the storage-block rebinding and -// the framebuffer-to-texture copy - for which no existing row could carry the call. -// 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the -// reflection archive" is CORRECTED by the row: the archive carries the bindings the -// program LINKED with, and glShaderStorageBlockBinding moves one AFTER link. -// - P5c (docs/archive/contracts/CONTRACT-P5C.md §5.1/§5.2) appended TWO more after those (opcodes 77..78), -// same rule: applier_reset, the make-current edge's server-side reset that used to be a -// GL-thread direct call into server memory, and object_death, the framebuffer family's -// FIRST wire delete opcode and the handle-keyed replacement for the death-notice mailbox. -// Neither has an MGPipeApply* entry point and neither gains one; both reach WireVerbSink. -// - P5c rv (§5.3) appended ONE more (opcode 79): set_context_values, the residual-value -// record that retires the value-class BARRIER_PULLED rows. Unlike the two control records -// it IS an ordinary set_* row: MGPipeApplySetContextValues writes gPipeInputs, the row is -// routed through gMGPipeContext like its seventeen siblings, and under monolith it has no -// producer at all - the fields keep coming through the residual fill, byte for byte as -// before (G1). 79 unique records. -// - P5e (docs/archive/contracts/CONTRACT-P5E.md section 1) appended ONE more (opcode 80): -// set_program_bindings, the three POST-LINK mutable reflection fields - the uniform-block -// bindings, the sampler uniform units and the name-keyed storage-block override map. They -// live inside the LinkArtifacts the reflection archive snapshots, and all three move AFTER -// the link, so no existing row can carry them: create_shader_state describes the link and -// this one describes what happened to it since. It lands with a NULL ROUTE and a sink that -// refuses it by name until package pg wires the emitter, exactly as set_shader_buffers has -// sat catalogued-and-dead since P4a. 80 unique records. -// - delete_stream_output (opcode 81) took the XFB object's deletion across; 81 records. -// - P9 (docs/archive/contracts/CONTRACT-P9.md §1) appended TWO more (opcodes 82..83): -// read_pixels_to_buffer and get_texture_image_to_buffer, the pack-buffer halves of the two -// readbacks, answered by nothing (ID-57's fire-and-forget). Like P5b's five they reach -// WireVerbSink only and have no monolith producer. 83 unique records. -// - P14 S1 (docs/Disaggregated/design/11-state-ownership.md) appended ONE more (opcode 84): -// bind_context, the in-band record that carries the client's CURRENT context token. It has -// the applier_reset shape - a kScreen control record, no MGPipeApply* entry point, no -// monolith producer - because what it moves is session attribution, not context state. The -// design's per-record ContextSlot was NOT taken here: the token is announced at the edge -// (a context change) instead of stamped on every verb, which costs one record per -// make-current and leaves the 71 context-family payloads untouched. 84 unique records. -// shared_image (85), the server-allocated colour buffers exported as dma-bufs. 85 unique -// records. -// --------------------------------------------------------------------------------------- +// The MGPipe call catalogue (ARCHITECTURE.md §3), one X(Name, PayloadStruct, Class, Flags, +// WaitClass) per call, read by scripts/gen_pipe.py into MG_Pipe/generated/*.inc. The wire +// opcode is the 1-based row position: append new calls at the end, keep retired rows in place. +// MGP_CALL_LIST_DOCUMENTED_COUNT is the row count, pinned by MG_Test/Pipe/PipeCatalogueTest.cpp. +// Class: kScreen rows go to MGPipeScreen, all others to MGPipeContext. kHasBlob means the +// payload owns an MGPBlobRef member; kNeedsAck means a per-record predicate may ask for an ack. +// WaitClass (MGPipeWaitClass) is what the client does after publishing under run-ahead; +// kWaitReply goes with kReplySlot, which gen_pipe.py enforces (ARCHITECTURE.md §3.2.1). #define MGP_CALL_LIST_DOCUMENTED_COUNT 85 @@ -386,21 +243,11 @@ X(SharedImage, MGPSharedImageOp, kScreen, kReplySlot, kWaitReply) // clang-format on -// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): -// - GetIntegeri_v / GetInteger64i_v. The six backend-owned answers they carry - -// GL_MAX_COMPUTE_WORK_GROUP_COUNT and GL_MAX_COMPUTE_WORK_GROUP_SIZE, three axes each, -// the only indexed pnames the device rather than the frontend answers - live in MGPCaps -// as DynamicBackendParameters::MaxComputeWorkGroupCount / MaxComputeWorkGroupSize, filled -// by both backends at capability init (DirectGLES from glGetIntegeri_v, DirectVulkan from -// VkPhysicalDeviceLimits) and floored by the frontend. Every other indexed pname names -// frontend state and is answered before any table is consulted. -// - GetProgramiv. GL_COMPUTE_WORK_GROUP_SIZE is a FRONTEND link artifact -// (ProgramObject::GetComputeLocalSize, what GL_Program.cpp has always answered from), not -// a backend answer at all; nothing a backend knows about a program crosses this way. -// - ShaderStorageBlockBinding WAS listed here as "folded into MGPProgramDesc's reflection -// archive". P5b's set_storage_block_binding row (opcode 75) replaces that: the archive -// carries the bindings as LINKED, and the call moves one after link, which the backend -// applies to an already-built driver program (DirectGLES.cpp ShaderStorageBlockBinding). -// set_pixel_unpack_state (no such state crosses the line - plan 4.6 D5), a -// compressed-format concept, pipe_transfer, and the stage dimension of set_sampler_views -// (MobileGL's texture unit space is merged, not per stage - plan 4.4.3). +// Not in the catalogue (ARCHITECTURE.md §3.2): +// - GetIntegeri_v / GetInteger64i_v: the only device-answered indexed pnames, the compute +// work-group count and size limits, travel in MGPCaps (DynamicBackendParameters); every +// other indexed pname is frontend state. +// - GetProgramiv: GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact +// (ProgramObject::GetComputeLocalSize). +// - set_pixel_unpack_state, compressed-format concepts, pipe_transfer, and a stage dimension +// on set_sampler_views (the texture unit space is merged across stages). diff --git a/MobileGL/MG_Pipe/PipeMutation.h b/MobileGL/MG_Pipe/PipeMutation.h index fda97270..ea71bfd4 100644 --- a/MobileGL/MG_Pipe/PipeMutation.h +++ b/MobileGL/MG_Pipe/PipeMutation.h @@ -9,23 +9,13 @@ #pragma once #ifndef MOBILEGL_MG_PIPE_MUTATION_H // belt and braces: reachable as and <..> #define MOBILEGL_MG_PIPE_MUTATION_H -// Push-on-mutation (P1 lane finding F2). MGP_FILL copies a verb's may-read set out of the -// live GLContext at the verb boundary; the backend then reads that copy for the whole verb. -// A backend that WRITES a frontend object inside its own verb - Magma synthesising a -// fallback texture for an unbound sampler, materialising a queued clear, or overriding a -// sampler's filter - moves a value the boundary already copied, and every read after that -// point sees a block that no longer equals the live context. That is a real divergence, not -// a harness artefact: the pull build reads the moved value and the push build does not. -// -// The frontend mutator that moves such a value spells MGP_NOTE_MUTATION(Field) right where -// it moves it. The notice refreshes that ONE field in the pushed block when the field -// belongs to the verb currently in flight, so "the pushed block equals the live context at -// every read" stays literally true and the push build keeps pull semantics. It refreshes -// the value only and never the poison stamp, so a withheld stamp (MOBILEGL_PIPE_POISON_OMIT, -// negative control B) stays withheld. -// -// In the pull build the macro is ((void)0) and this header includes nothing, so the pull -// build is byte-identical to a tree without it. +// Frontend mutation notices. The verb fill (MG_Impl/Pipe/PipeFill.cpp) copies a verb's may-read +// set out of the live GLContext at the verb boundary, and the backend reads that copy for the +// whole verb. A frontend value written inside the verb (Magma synthesising a fallback texture, +// materialising a queued clear, overriding a sampler's filter) would leave the copy stale, so +// the mutator calls MGP_NOTE_MUTATION(Field) where it moves the value. The notice refreshes +// that one field when it belongs to the verb in flight. It never touches the poison stamp, so a +// stamp withheld by MOBILEGL_PIPE_POISON_OMIT stays withheld. #include namespace MobileGL::MG_Pipe { // MG_Impl/Pipe/PipeFill.cpp (the client side, the only place that may spell pGLContext). @@ -33,25 +23,14 @@ namespace MobileGL::MG_Pipe { // class's may-read mask; a forwarded (sticky) field has no storage and is never copied. void MGPipeNoteFrontendMutation(MGPipeInputField field); - // ---- the aggregate generations (P2 brief D4, ARCHITECTURE.md 5.2) ---- + // ---- aggregate generations (ARCHITECTURE.md §5.2) ---- // - // MGP_NOTE_MUTATION answers "a backend moved a frontend value INSIDE its own verb". - // MGP_NOTE_AGGREGATE answers a different question, which is why it is a second macro - // and not an overload: "did ANY object of this class move since the last time the - // tracker looked", collapsed onto one monotonic Uint64 per class so a per-verb dirty - // walk is a handful of compares rather than a scan over 32 attributes, 16 attachments, - // 32 texture units and 84 binding points. - // - // The counters are members of the owning MG_State container, all guarded by - // MOBILEGL_PIPE_PUSH so the pull build's state objects do not change size (G1). The - // bump points sit on OBJECTS, which have no back-pointer to their state, so the macro - // goes through a free function that finds the live GLContext - the same shape, and for - // the same reason, as MGP_NOTE_MUTATION (MG_Impl/Pipe/PipeFill.cpp). It costs a global - // load on a path that has just written object state. - // - // Monotonic and never reset: the tracker widens and compares, it never subtracts. - // Over-firing is free (one extra re-push); under-firing renders stale, which is why - // every counter here is deliberately COARSER than the state it guards. + // MGP_NOTE_AGGREGATE answers "did any object of this class move since the tracker last + // looked", one monotonic Uint64 per class, so the per-verb dirty walk is a few compares + // instead of a scan over attributes, attachments, texture units and binding points. The + // counters live in the owning MG_State container; objects have no back-pointer to it, so the + // note finds the live GLContext through a free function. Never reset. Over-firing costs one + // re-push and under-firing renders stale, so each counter is coarser than the state it guards. enum class MGPipeAggregate : Uint32 { // VertexArrayState: any VAO attribute format / buffer / enable moved. VaoAttribute = 0, @@ -153,40 +132,14 @@ namespace MobileGL::MG_Pipe { // is asked rather than assumed. Bool MGPipeEmitVertexElementsDestroyAndFree(Uint64 lifetimeId); - // ---- P4a: ONE CLIENT-SIDE DEATH HELPER PER KIND P4a MINTS (brief D-I1) ---- + // ---- client-side death helpers, one per kind the client mints ---- // - // BACKEND-NEUTRAL FROM DAY ONE, and this is the P3a final-review lesson taken forward - // rather than repeated. Before it, the only thing that ever returned a VertexElementsCso - // slot was DirectGLES' StateObjectDeathOps table; under a backend that installs none - - // DirectVulkan/Magma, which keeps its own age-reclaimed identity table on purpose - every - // VAO ever created held its slot and its applier record for the life of the process, and - // past 65536 slots every create became a permanent Fatal{ProtocolCorruption}. P4a mints - // SIX kinds, so the rule is stated once and obeyed six times: whatever mints a handle owns - // the death of that handle, the client mints all six, and a backend death notice is a - // redundant SECOND path that must be idempotent - which it is, because it resolves through - // the same lifetimeId -> slot map these free, and MGPipeSlotAllocator::Free refuses a slot - // that is not live at that generation. - // - // THE ORDER INSIDE EACH IS FIXED AND IS NOT A PACKAGE'S CHOICE: - // 1. emit the wire delete FIRST - it drops the applier's record while the record still - // exists, so a recycled slot cannot inherit a field; - // 2. raise NotifyStateObjectDestroyed SECOND - it resolves the handle through the - // allocator, and a backend told after the Free could no longer find its twin, which - // moves the leak from the client to the driver object; - // 3. free the slot LAST, and a double free on a stale generation is a proven no-op - // because Free bumps no generation (the bump rides the next handout). - // - // ALL SIX TAKE THE LIFETIME ID rather than the object, for MGPipeEmitVertexElementsDestroy - // AndFree's reason: they run from a destructor, where the last SharedPtr has already - // dropped, and the lifetime id is what the slot allocator resolves the handle from. It is - // also what keeps this header a declaration-only coupling - no frontend class needs - // forward-declaring for any of them. - // - // Each returns whether its wire delete actually went out, which is the LATCH taken at the - // object's create and not a second reading of the subsystem predicate: an object born - // while a subsystem bit was clear and destroyed after it was set would otherwise free its - // slot with the applier's record still Live, on a slot about to be handed out again. The - // legacy path runs only when the answer is false. + // Whoever mints a handle owns its death, so these are backend-neutral; a backend death + // notice is a redundant, idempotent second path (MGPipeSlotAllocator::Free refuses a slot not + // live at that generation). Fixed order: emit the wire delete while the applier record + // exists, then NotifyStateObjectDestroyed while the allocator still resolves the handle, then + // free the slot. All take the lifetime id because they run from destructors. Each returns + // whether its delete went out, decided by the publication latch taken at create. // ResourceDestroy, and then the SamplerViewCso minted off this same lifetime id (P4a // D-F2: one sampler view per ITextureObject). Called from TextureObjectBase's VIRTUAL @@ -214,33 +167,14 @@ namespace MobileGL::MG_Pipe { Bool MGPipeEmitShaderCsoDestroyAndFree(Uint64 lifetimeId); #if MOBILEGL_BUILD_DISAGGREGATED - // ---- THE DEFERRED DESTROY QUEUE (the MOBILEGL_IPC_BATCH_WAITS class) ---- + // ---- deferred destroy queue ---- // - // Every helper above speaks on the CLIENT's behalf: it resolves the handle through the - // client allocator, emits a client wire record and frees a client slot - so it may only - // ever run on the GL thread. The per-record barrier made that free: the client waited - // out every apply, so a server-side SharedPtr (an applier's endpoint local, a - // gPipeInputs entry) could never be an object's last owner. Batched waits remove the - // guarantee - the client races ahead, drops its own references at teardown, and the - // apply thread's local CopyImageEndpoint becomes a texture's last owner, whose - // destructor then touches the client allocator from the server role - // (Fatal{RoleViolation, "MGPipeSlots"}). The helpers therefore START with - // MGPipeDeferDestroyAndFreeIfOnApplyThread: on the apply thread it enqueues the - // (kind, lifetime id) pair - the only thing a death announcement needs - and the GL - // thread replays the helper at the next verb hook (EmitTables' BeforeDrawVerb / - // BeforeReadOnlyVerb) through MGPipeDrainDeferredDestroys. A delayed free is safe: - // slots are plentiful, reuse is delayed rather than corrupted, and the replay runs the - // helper itself, so every helper's own three-step order is untouched. - // P5e (ra), CONTRACT-P5E §2.7 / ruling 13 (ID-91): THE QUEUE STAYS LIVE, AND AN - // UNBARRIERED ENQUEUE IS A FINDING. After the family packages land, an unbarriered apply - // holds no frontend SharedPtr at all - every object is resolved from a handle - so the - // only apply thread that can still be a last owner is one inside a BARRIERED record (a - // fill's O-class rows, XFB's pinned targets). An enqueue from an unbarriered record - // therefore means some site still pins a frontend object across a record, which is a - // migration this phase believes it finished: it logs once with the kind, and is Fatal - // under MOBILEGL_IPC_STRICT_ERRORS so the strict lane owns the red. The queue itself is - // NOT removed - it is the belt that keeps the barriered case from corrupting the - // allocator while the finding is being read. + // The helpers above act for the client, so they run only on the GL thread. When the apply + // thread drops an object's last reference, the helper its destructor calls starts with + // MGPipeDeferDestroyAndFreeIfOnApplyThread, which queues (kind, lifetime id); the GL thread + // replays the helper at its next verb hook (MGPipeDrainDeferredDestroys), so only slot reuse + // is delayed. Only a barriered record should hold such a reference: an enqueue from an + // unbarriered one logs once, and is Fatal under MOBILEGL_IPC_STRICT_ERRORS. Bool MGPipeDeferDestroyAndFreeIfOnApplyThread(MGPipeKind kind, Uint64 lifetimeId); void MGPipeDrainDeferredDestroys(); @@ -305,43 +239,13 @@ namespace MobileGL::MG_Pipe { void* MGPipeEmitMapPersistent(MG_State::GLState::BufferObject& buffer); // ================================================================================ - // P4a: THE BIRTH HALF, one hook per client path MG_State owns (D-C .. D-I) + // Birth hooks, one per client path MG_State owns // ================================================================================ // - // The death helpers above are half a lifetime. The other half is emitted from MG_State - // too - a texture's create from its constructor, a renderbuffer's respecify from its - // storage mutators, a texture's params from glTexParameter*, a sampler CSO from the - // sampler object, a shader CSO from the program - because that is where the event - // happens, exactly as P3a's buffer family emits from BufferObject's own dispatchers - // (ARCHITECTURE.md 5.1 names those as the ONE exception to push-at-validate). Only the - // texture sub-data DRAIN runs at the validate point, and even it is fed from here: the - // drain list is appended on a level's first dirty mark. - // - // WHY THEY ARE DECLARED HERE. This header is the one door MG_State has into the client - // (check_include_closure.py's mutation-header probe pins it: reaching - // MG_Impl/Pipe/*Emit.h from a frontend mutator would pull the client's emitters into the - // state machine that calls them). So a hook a frontend mutator calls is DECLARED here and - // DEFINED in MG_Impl/Pipe/PipeFill.cpp, which is package A's for the whole phase - the - // same "declaration here, definition there" split MGPipeMintResourceHandle and - // MGPipeEmitResourceCreate use, and the reason no file is touched twice. - // - // WHAT EACH BODY DOES, and the division is fixed: - // * PipeFill.cpp owns the GATE - the subsystem bit in MOBILEGL_PIPE_PUSH *and* the - // family's own kMGPipeWired*Subsystem constant, the same pair the validate point's - // `wants()` applies to every emission - and the four MINTS, which are pure allocator - // work and need no family knowledge; - // * the FAMILY EMITTER (MG_Impl/Pipe/Emit.h, owned by package B or C) owns the - // payload build, the handle rule for its own kind and the PUBLICATION LATCH below. - // PipeFill.cpp forwards to it through an entry point that is compiled only while that - // family's wired constant is non-zero, so this tree links against the STUB emitters - // and against the finished ones with no edit to PipeFill.cpp - and a family that sets - // its constant without providing the entry point is a COMPILE ERROR in its own commit - // rather than a surprise at the merge. The entry point each hook forwards to is named - // beside it and spelled out in PipeFill.cpp's contract block. - // - // NOTHING CALLS ANY OF THEM AT THE CONTRACT COMMIT. B and C add the call sites in the - // five MG_State directories C.7 gives them, in the SAME commit that gives the emitter its - // body - by EDITING an existing constructor/mutator body, never by adding one (G1). + // Emitted from MG_State where the event happens, like the buffer family (ARCHITECTURE.md + // §5.1). Declared here because this header is MG_State's one door into the client + // (check_include_closure.py); PipeFill.cpp owns the family gate and the mints and forwards to + // the family emitter (MG_Impl/Pipe/Emit.h), which owns the payload and the latch. // ---- the publication latch (D-I1), and it is the ONE answer both halves read ---- // @@ -381,22 +285,14 @@ namespace MobileGL::MG_Pipe { // door into it, and it is not a frontend construction event. void MGPipeMintShaderCsoHandle(MG_State::GLState::ProgramObject& program); - // ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h, package B ---- + // ---- textures and renderbuffers: MG_Impl/Pipe/TextureEmit.h ---- // - // resource_create from ITextureObject's constructor and RenderbufferObject's; - // resource_respecify from every storage-defining entry point, including - // RenderbufferObject::{SetInternalFormat, AllocateStorage, SetSamples}, which publish - // nothing at all today (D-D2); set_texture_params from the parameter mutators, which is - // where the READ-attachment-only gap D-E3 closes. - // - // Entry points MGPipeTextureEmitter must provide, all taking the frontend object by - // reference and returning void: - // EmitResourceCreate(ITextureObject&) - // EmitResourceRespecify(ITextureObject&, MGPipeTextureRespecifyScope, Uint32 uploadTarget, - // Uint32 level) - // EmitTextureParams(ITextureObject&) - // NoteLevelDirty(ITextureObject& storageOwner, Uint32 uploadTarget, Uint32 level) - // EmitRenderbufferCreate(RenderbufferObject&) / EmitRenderbufferRespecify(RenderbufferObject&) + // resource_create from the ITextureObject and RenderbufferObject constructors, + // resource_respecify from every storage-defining entry point (renderbuffer storage included), + // set_texture_params from the parameter mutators. MGPipeTextureEmitter provides, as void: + // EmitResourceCreate, EmitResourceRespecify(.., scope, uploadTarget, level), + // EmitTextureParams, NoteLevelDirty(storageOwner, uploadTarget, level), + // EmitRenderbufferCreate, EmitRenderbufferRespecify. Each takes the object by reference. void MGPipeEmitTextureResourceCreate(MG_State::GLState::ITextureObject& texture); // WHICH STORAGE A TEXTURE RESPECIFY REPLACES (P4a final review C-1). The applier scopes diff --git a/MobileGL/MG_Pipe/PipeRoute.cpp b/MobileGL/MG_Pipe/PipeRoute.cpp index b8d27338..1efd20e6 100644 --- a/MobileGL/MG_Pipe/PipeRoute.cpp +++ b/MobileGL/MG_Pipe/PipeRoute.cpp @@ -6,27 +6,14 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// The MONOLITH arm of R-17's routing: thirty-eight adapters that unpack a generated table -// row's parameters and call the MGPipeApply* entry point the call site used to call directly. -// Owner: package c1. See PipeRoute.h for why the arm exists and what R-17 actually cost. -// -// G2 IS THE WHOLE SPECIFICATION OF THIS FILE. Under monolith transport the thunks must reach -// EXACTLY the code they reach today - so every adapter below is a parameter shuffle and -// nothing else. There is no branch, no cache, no early return and no logging on any of them, -// because each of those is a way for `MOBILEGL_TRANSPORT=monolith` to stop being the control -// arm that the split arm is measured against. The one thing an adapter may do beyond calling -// through is POST A REPLY, and only the five rows whose call site consumes a value do that. -// -// WHO CALLS THE INSTALL, AND WHY IT IS NOT IN THIS FILE. `gMGPipeScreen` and `gMGPipeContext` -// are inline variables with CONSTANT initialisation, so they are zero - every entry null, -// which is the pre-migration state - before any dynamic initialiser runs. The installer is -// driven from an inline variable in PipeRoute.h rather than from a static initialiser here, -// and the comment beside `detail::gMonolithTablesInstalled` says why: a static initialiser in -// THIS file is only in the program if THIS object file is in the link, and over a static -// archive it was not. That makes "the tables are installed" an invariant rather than a step -// someone can forget, and it is why no MGP_* thunk needs a null check - which matters, because -// a null check on a table slot is precisely the shape CONTRACT-P5 §7 spends the 41 call sites -// killing. +// The in-process arm of the call routing: adapters that unpack a generated table row's +// parameters and call the matching MGPipeApply* entry point. An adapter is a parameter shuffle, +// with no cache, logging or logic of its own, so the in-process arm stays the reference the split +// arm is measured against. Three kinds of row do one thing more: rows whose call site waits for +// an answer post a reply, the create_shader_state escape builds the record's archive, and the +// retired set_residual_value_state row aborts. The tables are installed before main by +// detail::gMonolithTablesInstalled in PipeRoute.h, not by a static initialiser here (a +// static-archive link can drop this object), so no MGP_* thunk needs a null check. #include "PipeRoute.h" diff --git a/MobileGL/MG_Pipe/PipeRoute.h b/MobileGL/MG_Pipe/PipeRoute.h index fd90ba28..75e407a4 100644 --- a/MobileGL/MG_Pipe/PipeRoute.h +++ b/MobileGL/MG_Pipe/PipeRoute.h @@ -6,80 +6,14 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// THE CLIENT -> WIRE ROUTING OF THE 38 MGPipeApply* ENTRY POINTS (P5, integrator ruling R-17; -// 38 since P5c rv added set_context_values, CONTRACT-P5C.md section 5.3). -// Owner: package c1. -// -// WHAT WAS MISSING. `gMGPipeWireRecordApply` is the DECODE hook and it has existed since w1: -// a record arrives, `PipeWireCodec` picks it apart and calls `MGPipeApply`. There was no -// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply` DIRECTLY, so under -// `MOBILEGL_TRANSPORT=inproc` every resource, CSO, texture and program record still executed -// synchronously on the GL thread against a context v1 is moving to the apply thread - where -// all sixteen `IsBackendContextCurrentOnThisThread()` and sixteen `CanTouchGLNow()` guards -// answer false. `ClearThenReadPixels` might have survived that; `TriangleScenario` needs a VBO -// and a program and could not. -// -// The two generated tables (`gMGPipeScreen`, `gMGPipeContext`, `generated/PipeTables.inc`) are -// the boundary this repository already had, and they were never installed - every entry null, -// which `MGPipe.h` correctly calls "precisely the pre-migration state". R-17 installs them. -// This header is the frontend-facing half: the names `MG_Impl/Pipe` calls, and the mechanism -// that gets a kReplySlot row's answer back to its call site. -// -// --------------------------------------------------------------------------------------- -// THE SIZE OF R-17, MEASURED RATHER THAN ESTIMATED -// --------------------------------------------------------------------------------------- -// -// R-17 costed this as "37 thin client emitters over EmitAndWait". The emitters are thin. The -// ROUTING was not, and the reason is that the generated table signature is the P2-era monolith -// interface: `void (*Name)(const Payload* payload, ...)`, async-with-handle, no return value, -// and NO COMPANION POINTER. Of the 37 entry points, as the tables stood: -// -// 24 the row could carry every argument the call site passes today -// 13 it could not -// -// The thirteen split three ways, and only one of the three is a generator question: -// -// (a) NINE carry a blob companion - `chunkBytes`, `blobBytes`, `parameters`, `bytes`, -// `initialBytes` - which is CONTRACT-P5 table 1's "companion pointer today" column. A -// kVarTail row already gains `const void* varTail, Uint32 varTailCount`; a kHasBlob row -// gained nothing. THE GENERATOR CHANGE IS THAT ONE ASYMMETRY, and nothing else: five -// lines in `Call.Signature` giving a kHasBlob row `const void* blobBytes, Uint64 -// blobByteCount`. Ten rows gained the pair (the nine plus `GetCaps`, which has no applier -// entry point). `--check` up to date, `--self-test` 9/9 trips. -// -// (b) FOUR return a value the row cannot: `ResourceCreate`, `ResourceRespecify`, -// `ResourceSubData` and `SetTextureParams` return Bool and `MapPersistent` returns void*. -// These are exactly CONTRACT-P5 §2's reply-slot rows, and R-5 forbids re-deriving any of -// them on the client. The answer therefore has to come back THROUGH the reply slot, which -// is what `MGPipeTakeReply*` below is, and it is the part of R-17 that no amount of -// "thin emitter" covers: `MGPReplySlot` is `{Uint64 Id;}` - an IDENTIFIER, not a value - -// so a reader had to exist for the identifier to be worth minting. -// -// (c) FOUR CANNOT GO THROUGH A GENERATED ROW AT ALL, and each one is a contract ruling -// rather than an oversight. They are the ESCAPES below. Saying "four" out loud is the -// honest answer to "tell me if it is bigger than it looks": it is, by four rows and one -// reply-reading mechanism. -// -// --------------------------------------------------------------------------------------- -// THE REPLY MAILBOX, AND WHY ONE ENTRY IS THE RIGHT DEPTH -// --------------------------------------------------------------------------------------- -// -// `MGPReplySlot` carries an id; the answer lands beside it. The mailbox is ONE ENTRY DEEP and -// thread-local, and that is not a simplification - it is R-1's verb barrier stated as a data -// structure. While the barrier holds, the in-flight depth is exactly one (`ReplySlot.h`: -// "under R-1's verb barrier the in-flight depth is one"), so a second posting before the first -// is taken is not a capacity problem, it is a barrier that has stopped working. It Fatals. -// -// THE MAILBOX HAS NO DEFAULT ANSWER AND THIS IS THE WHOLE POINT. `MGPipeTakeReplyBool` on a -// slot nothing posted to is `Fatal{ReplyMissing, ""}`, not `false` and emphatically not -// `true`. "Always accept" is ID-39's 66 lost DirectVulkan uploads with a wire in between, and -// "always refuse" is an emitter that re-sends for ever. A row that forgets to answer is a bug -// that must be impossible to ship, so it is impossible to READ. -// -// WHEN THE BARRIER RETIRES (R-1 opens family by family) this becomes a real slot pool keyed on -// seq, which is what `Transport::ReplySlot` already is on the wire side. The mailbox is the -// frontend-side handle onto it and nothing else; it is deliberately NOT a second id space, -// because the split arm stamps the record's own seq into `MGPReplySlot::Id` (R-3). +// Client-side routing of the MGPipe calls. MG_Impl/Pipe emitters call MGPipeRoute, never +// MGPipeApply, so every record goes through the installed tables (gMGPipeScreen, +// gMGPipeContext, gMGPipeRouteEscapes): the monolith adapters or the client wire emitters. +// A kReplySlot answer comes back through a thread-local mailbox one entry deep: each route takes +// its answer before returning, so a second post before the take is Fatal{ReplyOverrun}. Taking +// an answer nothing posted is Fatal{ReplyMissing}, never a default: assuming acceptance loses +// uploads, assuming refusal makes the emitter resend for ever. Under split the slot id is the +// record's own sequence number, so the mailbox adds no second id space. #pragma once #include @@ -121,45 +55,13 @@ namespace MobileGL::MG_Pipe { Uint64 MGPipeRepliesDeclined(); // --------------------------------------------------------------------------------- - // The escapes: the four rows no generated signature can express + // The escapes: the rows no generated signature can express // --------------------------------------------------------------------------------- - // - // A hand-written third table, installed and uninstalled by the same two functions as the - // generated pair, so there is ONE mechanism and not two. Each row's signature is the - // applier's own, because that is the shape the monolith arm has to reproduce byte for - // byte (G2), and each row is here for a ruling that is written down: - // - // ResourceRespecify `initialBytes` has NO kHasBlob and R-13.3 forbids giving it one - // ("initialBytes is always nullptr under split; initial content - // arrives as ResourceSubData records immediately after this one"). - // So the row is correct to omit it AND monolith must still pass it. - // ResourceFlushRange `bytes` likewise, by R-13.2, and for a sharper reason: a blobref - // here would be "a second, forgeable way to say the same thing". - // MapPersistent `size` and `seedBytes` have no carrier at all - `MGPHandleOnly` is - // {Handle, Kind} - and the call returns void*. R-6 makes the split - // answer a constant DECLINE, so the carrier is not needed; the - // monolith arm needs both arguments. - // CreateShaderState SEVEN blobrefs (`Spirv[6]` + `Reflection`) and TWO typed frontend - // pointers. One `blobBytes` pair cannot express seven runs, and - // serialising under monolith to un-serialise in the adapter would put - // `EncodeProgramArtifacts` on the monolith path, which `PipeApply.h` - // explicitly promises it is not ("zero serialisation cost on the - // monolith path"). - // SetProgramBindings THREE tails in three index spaces plus a PARALLEL NAME ARRAY - // (P5e, docs/archive/contracts/CONTRACT-P5E.md §1/§5.5). One `varTail` + - // `varTailCount` pair cannot express three runs at all, and the - // fourth argument is not a run: the storage-override key is a - // `MGHostSpan` on the wire and a plain `const char*` under monolith, - // so the applier takes the resolved names beside the tail rather - // than resolving segments itself (rule C - a span retires with the - // record that named it, and the applier copies the bytes it keeps). - // The generated `gMGPipeContext.SetProgramBindings` row therefore - // STAYS NULL, which is what PipeCatalogueTest already pins. - // - // OVERTURN CONDITIONS, one per row: give `MGPResourceDesc` and `MGPFlushRange` a blobref - // (overturns R-13.2/R-13.3, and c0 owns `MGPipeTypes.h`); give `MGPHandleOnly` a size - // (same owner) and P6 a real remote map; measure that a per-stage SPIR-V run beats one - // archive, at which point `CreateShaderState` needs a multi-blob row rather than this one. + // A hand-written third table, installed with the generated pair. Each row keeps the applier's + // signature because the monolith arm passes what the wire record does not carry: + // ResourceRespecify's `initialBytes` and ResourceFlushRange's `bytes` (sent as resource_subdata + // records instead), MapPersistent's `size`, `seedBytes` and void* result, CreateShaderState's + // seven SPIR-V/reflection blobs (monolith never serialises them), SetProgramBindings' tails. struct MGPipeRouteEscapes { Bool (*ResourceRespecify)(const MGPResourceDesc* desc, const void* initialBytes, const MGPRespecifiedLevel* level); @@ -207,24 +109,11 @@ namespace MobileGL::MG_Pipe { const MGPipeRouteEscapes& MGPipeMonolithEscapes(); namespace detail { - // THE INSTALL THAT CANNOT BE LINKED AWAY, and the first version of this WAS. - // - // It began as a static initialiser inside PipeRoute.cpp, on the reasoning that - // `gMGPipeScreen` and `gMGPipeContext` are inline variables with CONSTANT (zero) - // initialisation - sequenced before every dynamic initialiser - so an installer in - // dynamic init necessarily beats any GL entry point. That reasoning is correct and the - // mechanism still failed, because it assumed PipeRoute.o would be in the link at all: - // every MG_Test target is its OWN binary over a static archive, `CsoCache.h` reaches - // the table through an inline thunk and names no symbol from PipeRoute.o, so the - // linker dropped the object, the initialiser never ran, and five CsoCacheTest cases - // took a null function pointer. The shared library linked it and was fine, which is - // exactly the shape that ships. - // - // AN INLINE VARIABLE FIXES BOTH HALVES AT ONCE. Its initialiser is emitted as a COMDAT - // in every translation unit that includes this header, so any binary that can call a - // thunk has one copy of it; and naming `MGPipeInstallMonolithTables` from a header the - // call sites already include is an undefined reference that forces the archive member - // into the link. There is still exactly one install, and it still happens before main. + // Installs the monolith tables before main in every binary that includes this header. + // A static initialiser in PipeRoute.cpp is not enough: a test binary linking the static + // archive may drop PipeRoute.o (CsoCache.h reaches the tables only through an inline + // thunk), leaving the tables null. This inline variable is emitted in each including + // translation unit, and its call to MGPipeInstallMonolithTables pulls PipeRoute.o in. inline const Bool gMonolithTablesInstalled = (MGPipeInstallMonolithTables(), true); } // namespace detail @@ -240,31 +129,13 @@ namespace MobileGL::MG_Pipe { void MGPipeNoteInstalledArm(MGPipeRouteArm arm); // --------------------------------------------------------------------------------- - // The call-site names: MGPipeRoute for each of the thirty-eight + // The call-site names: MGPipeRoute for each routed MGPipeApply* entry point // --------------------------------------------------------------------------------- - // - // EVERY ONE TAKES THE APPLIER'S OWN SIGNATURE, so converting a call site is a rename and - // nothing else. That is not tidiness, it is the G1 lesson from round 1 stated as a rule: - // rewriting `if (const auto f = TABLE.GL.Slot)` into a Bool-valued macro is semantically - // identical and generates DIFFERENT code (-144 bytes, two symbols resized). A wrapper that - // took `&expr` would force every site holding a temporary - `HandleOnly(handle)`, - // `BufferHandleOnly(handle)` - to grow a named local, which is a second expression shape - // change at forty sites. A reference parameter keeps the call site's text identical but - // for the name. - // - // THREE OF THE THIRTY-SEVEN TAKE ONE ARGUMENT MORE THAN THEIR APPLIER, and the extra is - // always the same thing: A BYTE COUNT THE RECORD DOES NOT DECLARE. CONTRACT-P5 table 1 - // rule A requires every kHasBlob row to declare its length under split; five rows already - // do and the wrapper reads it back off the record, three do not and their call site is the - // only place the number exists: - // SetGlobalConstants declares 0 (ProgramEmit.h); the length is GetUBOSize() - // ResourceSubData buffer half declares real, TEXTURE half declares 0 on the - // grounds that the count is "the server's to compute" - which - // table 1 row 7 says "cannot be a bounds check". The level - // shadow's byte size is in scope at that call site. - // BufferSubDataResident the application's staging store, sized by the caller - // A defaulted argument was rejected: a default would let a site that has the number forget - // to pass it and still compile, and the failure would be a silently short upload. + // Each takes the applier's signature by reference, so converting a call site is a rename and a + // site passing a temporary needs no named local. Three also take a byte count the record does + // not declare and only the call site knows: SetGlobalConstants (GetUBOSize()), the texture half + // of ResourceSubData, and BufferSubDataResident. The count has no default, so a site cannot + // forget it and upload short. // ---- screen ------------------------------------------------------------------------ inline Bool MGPipeRouteResourceCreate(const MGPResourceDesc& desc) { @@ -272,27 +143,12 @@ namespace MobileGL::MG_Pipe { MGP_ResourceCreate(&desc, &reply); return MGPipeTakeReplyBool(reply, "resource_create"); } - // P12: WHICH RESPECIFIES STILL WAIT - one predicate, read by the wire emitter, so the two - // halves of this row cannot disagree about whether an answer is owed. - // - // THE BUFFER HALF NEVER DOES. Both call sites discard its Bool outright - // (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify(desc, nullptr);`), so the wait - // bought a value that was thrown away - on the device run this row was the second largest - // source of round trips in one frame, at ~5 ms each. - // - // THE TEXTURE HALF WAITS EXACTLY UNTIL THE OBJECT IS CONFIRMED, and "confirmed" is - // MGPipeHandleIsPublished: the latch is set only when the object's create was ACCEPTED, and - // the applier keeps texture records across a make-current (PipeFill.cpp:3756-3759), so for a - // published object the acceptance this row asks for is a fact the client already holds. The - // confirmation itself is paid once, at first use: EmitResourceRespecify sends a create when - // the latch is clear (TextureEmit.h:869-879), and that create is blocking. - // - // AND WHEN THE LATCH IS CLEAR THIS RETURNS TRUE, which is the whole fallback. It also covers - // the one place the two ends' consumer gates are worded differently - the client asks - // "any bit of the family" (PipeFill.cpp:1577-1595) while the applier asks "bit 10" - // (PipeApply.cpp:1408-1419) - because a create refused by the narrower server gate leaves the - // latch clear, so every later row for that object keeps taking the real answer. The cost of - // that case is throughput, never silence. + // Whether a resource_respecify record waits for its answer; the wire emitter reads this same + // predicate. Off the client wire arm it always does. The buffer half never waits: its callers + // discard the Bool. The texture half waits until the texture's create is accepted + // (MGPipeHandleIsPublished; a blocking create at first use), after which acceptance is known + // because the applier keeps texture records across make-current. A refused create leaves the + // latch clear, so later records keep waiting for the real answer. inline Bool MGPipeResourceRespecifyWantsItsReply(const MGPResourceDesc& desc) { if (MGPipeInstalledArm() != MGPipeRouteArm::kClientWire) return true; if (static_cast(desc.Target) == MGPipeResourceTarget::Buffer) @@ -316,56 +172,13 @@ namespace MobileGL::MG_Pipe { // ---- resources --------------------------------------------------------------------- - // P5e (ra), CONTRACT-P5E §2.5 / ruling 15 (ID-93): DOES THIS SUB-DATA RECORD WANT ITS - // ANSWER? One predicate, two callers - this route and the wire emit table - because the - // route cannot pass a flag through the thunk (the call table's signature is the - // catalogue's) and two hand-written copies of "is this the buffer half" is exactly the - // drift that would make the client wait for an answer the emitter told the server not to - // bother with, or read a slot that was never posted. - // - // THE BUFFER HALF DOES NOT: its Bool is discarded at the only call site there is - // (MGPipeEmitResourceSubData), and because the row carries kReplySlot the client used to - // wait for it anyway - one full round trip per 64 KB persistent-map block, whose answer - // nobody looked at. - // - // THE TEXTURE HALF DOES NOT EITHER, ON THE WIRE ARM (P12, the handoff's item B), and the - // argument is that its answer is DERIVABLE there - not that anybody stopped reading it. - // DrainTextureSubData still clears the level's dirty flag on an accepted answer (D-D5), and - // it still gets one; what it does not get any more is a ROUND TRIP for it: - // - // * THE EMISSION IS ALREADY GATED ON THE ANSWER'S CONTENT. A texture record is produced - // only where FamilyIsLive(kMGPipeSubsystemTextureResources, ...) holds, and under split - // that gate IS CapsMirrorInstance().ServerConsumes(bit 10) (PipeFill.cpp:1719-1740) - the - // same published fact the applier's own acceptance belt reads (PipeApply.cpp:1408-1419, - // NoP4aConsumer). So the one server-side refusal this acceptance exists to carry - "no - // consumer", the ID-39 / D-D5 / ID-18 M3 case that is WHY the texture half ever asked - - // cannot be the answer to a record this client emitted. - // * WHAT REMAINS IS A BUG CLASS, AND IT IS LOUD ALREADY. A record whose resource target - // names no texture, or that declares no texels or no bytes, is an MGP_TRIP_WIRE_REPORT on - // the server (PipeApply.cpp:1201-1218, :2245-2252); a record for a handle the applier - // does not hold is counted and named (ResolveResourceIn, :721-728), and the client's own - // tracker prevents it by creating before it ever sends sub-data (PipeFill.cpp:1045-1051). - // * AND EVERY ANSWER THE CLIENT CAN STILL GET FOR FREE, IT STILL GETS: the emitter reads - // this route's Bool exactly as before, so a record the CLIENT cancelled (the device-lost - // latch, an exhausted stage) still answers "not accepted" and the level stays dirty and - // is retried - D-D5's safe direction, unchanged. - // - // THE WAIT DOES NOT DISAPPEAR, IT MOVES TO THE STAGE. A record whose run does not fit - // SEG_STAGE reclaims retired bytes and parks on the stage doorbell (PipeWireCodec.cpp - // StageAllocate / ReclaimStagedBytes) - the one wait a run-ahead client owes, and the one - // that is proportional to the WINDOW rather than to the call count. That is the whole of item - // B: on Minecraft 26.2's atlas frame, 55,428 reply waits at ~4.4 ms each (96 MiB of uploads - // through a 32 MiB window) become the handful of stage retirements those same 96 MiB need. - // (Measured on the host by RemoteClientControls.TextureUploadsStopOwningAReplyOnTheWireArm, - // which counts the reply slots the client opens for a batch of uploads: N -> 0.) - // - // MONOLITH IS UNTOUCHED, and the arm test is what says so: there the applier's answer is - // returned by Mono_ResourceSubData on the calling thread and costs nothing, so there is - // nothing to stop waiting for and the predicate keeps its old answer. - // - // The test is MGPSubData::Target == kMGPipeResourceTargetBuffer, whole field, which is the - // invariant MGPipeTypes.h asserts beside the packer and which the applier's own - // SubDataNamesABuffer already reads. + // Whether a resource_subdata record waits for the server's answer (ARCHITECTURE.md §13.1.1). + // The route and the wire emit table both read this predicate, so neither waits for a reply the + // other did not request. On the wire arm no record waits: MGPipeEmitResourceSubData discards + // the buffer half's Bool, and a texture record is emitted only when the server consumes the + // texture family, so acceptance is known; a record the client cancels still answers false. + // Monolith answers for free and keeps the texture half's answer. The test reads the whole + // MGPSubData::Target field, as SubDataNamesABuffer does. inline Bool MGPipeSubDataWantsItsReply(const MGPSubData& record) { // THE WIRE ARM ONLY. Everything above is an argument about a client talking to a server // over SEG_CMD/SEG_STAGE; a monolith run inside a split build (every unit and diff --git a/MobileGL/MG_Pipe/PipeSessionFail.h b/MobileGL/MG_Pipe/PipeSessionFail.h index 35128761..17d377e0 100644 --- a/MobileGL/MG_Pipe/PipeSessionFail.h +++ b/MobileGL/MG_Pipe/PipeSessionFail.h @@ -6,36 +6,14 @@ // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// P7 wave 0 (plan section 1.3's P7-marked refusal row, item (0); Ph slice (2)). HOW A BACKEND DEATH REACHES -// Session::Fail WITHOUT THE BACKEND KNOWING MG_Remote EXISTS. +// How a backend death reaches Session::Fail without the backend naming MG_Remote. // -// The three Magma wire funnels - MagmaWireFatal (Renderer/WireFramebuffer.inc), -// WireDescriptorFatal (Renderer/UniformManager.cpp) and WireBufferLegacyFatal -// (Renderer/WireDraw.inc) - each logged their line and raised their OWN std::abort(). Fifteen -// P7-marked refusals (the grep key is the at-sign form, not spelled here) and two more sites die through them, and every one of those deaths: -// -// * published NO SessionFault frame, so the peer read a bare EOF and could not name what -// ended the session (CONTRACT-P6 5.2's whole point); -// * bumped NO SessionFaultCount(), so exit gate S8's "zero faults over a good run" could -// neither confirm nor falsify them; -// * carried a HAND-TYPED family word with no path to FatalFamilies.def's projection, so -// nothing checked that `UnmigratedVerb` in the string still meant UnmigratedVerb. -// -// Calling SessionFail() from the .inc files directly would be the obvious fix and is the wrong -// one: MG_Backend's renderer would then name an MG_Remote symbol on its own account rather than -// through the two staging headers it already borrows under `#if MOBILEGL_BUILD_DISAGGREGATED`, -// and the layering that P13's module boundaries have to establish would be one more edge worse. -// -// So the shape is MGP_TRIP_WIRE_REPORT's (PipeApply.cpp:66): MG_Pipe owns the entry point, the -// DEFAULT behaviour is exactly what the site did before (same log line, same abort), and the -// richer behaviour is a function pointer that a layer above installs. MG_Remote installs -// InstallPipeSessionFailHook() at server-role init (MG_Backend/Init.cpp's InitServerRoleCommon, -// which both the inproc server role and the spawn child run), and from then on these deaths go -// through SessionFail like every other one. -// -// THE MESSAGE STRING IS PASSED VERBATIM, family word and all - the same rule FatalFunnel.h -// states. The lines these three funnels write are already counted by name in the retrace refusal -// census and in every recorded P7-refusal measurement, so they are byte-identical before and after. +// The backend's fatal funnels (Magma's wire refusals, Espryt's slot-table refusals) call +// MGPipeSessionFail, which by default logs the line and aborts. MG_Remote installs a hook at +// server-role init (MG_Backend/Init.cpp's InitServerRoleCommon, run by both server roles), so the +// death publishes a SessionFault frame, counts in SessionFaultCount() and carries a checked +// family word. The message passes verbatim, as FatalFunnel.h requires, so refusal censuses read +// the same line either way. #pragma once diff --git a/MobileGL/MG_Pipe/PipeVerbSink.h b/MobileGL/MG_Pipe/PipeVerbSink.h index 9c3ef867..794a54c1 100644 --- a/MobileGL/MG_Pipe/PipeVerbSink.h +++ b/MobileGL/MG_Pipe/PipeVerbSink.h @@ -42,25 +42,14 @@ namespace MobileGL::MG_Pipe { virtual void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) = 0; }; - // ---- the verb sink: the five rows with no MGPipeApply* to delegate to --------------- + // ---- the verb sink: the rows with no MGPipeApply* entry point to delegate to ------------ // - // The decoder owns NO semantics, so every arm ends in an existing MGPipeApply* free - // function - except five, and they are exactly contract §7's class B: Clear (57), Blit - // (56), ReadPixels (58), DrawVbo (59) and Present (67). Those are GLFunctionsTable VERBS. - // MG_Pipe has no applier for any of them (the 37 MGPipeApply* entry points are the object - // and state families), so for these five P5 writes the first consumer as well as the first - // producer - and the consumer is the SERVER'S backend call, which is v1's, not the codec's. - // - // So the codec does what it can prove and stops there: it bounds-checks, cross-checks the - // tail, resolves the segments and hands over a DECODED, VALIDATED argument list. With no - // sink installed those five arms return false ("this build does not implement it"), which - // is the same answer the other unimplemented rows give. - // - // SetShaderBuffers (38) and SetStreamOutputTargets (39) also have no applier entry point, - // and they deliberately get NO sink method: both are off P5's reduced path (BRIEF §4's - // exclusion list), so inventing a consumer for them would be building a semantics nobody - // can test this phase. Their arms validate both tails - which is the part a later phase - // must not have to re-derive - and return false. + // The decoder owns no semantics. A row with an applier entry point ends in its MGPipeApply* + // function; the GLFunctionsTable verbs, fences, queries and control records are bounds-checked, + // tail-checked and segment-resolved, then handed to the sink below as a validated argument + // list. With no sink installed those arms return false ("this build does not implement it"), + // as does every row with neither an entry point nor a sink method, once validated + // (SetStreamOutputTargets checks both its tails first). struct MGPipeQueryResultReply { Uint64 Value = 0; Uint32 Produced = 0; @@ -139,22 +128,11 @@ namespace MobileGL::MG_Pipe { return false; } - // ---- P5b (docs/archive/contracts/CONTRACT-P5B.md): the rows the four migration packages consume. + // ---- the remaining verbs with no MGPipeApply* entry point: dispatch, barriers, copies, + // image and storage-block binds, transform feedback, patch parameter and mipmaps. // - // Every one below is a GLFunctionsTable verb with NO MGPipeApply* entry point - the - // census's correction - so, exactly like the five above, the codec validates and hands - // over and the SERVER'S sink (Server/PipeApplier.cpp's ServerVerbSink) makes the - // backend call. The default bodies return false ("this build does not implement it"); - // ServerVerbSink's stubs die Fatal{UnmigratedVerb, ""} by name until the owning - // package lands the real body, so a client that flips a slot ahead of its server half - // aborts with the same line the census greps rather than rendering nothing. - // - // i1 OnLaunchGrid, OnMemoryBarrier, OnResourceCopyRegion, OnBindShaderImage, - // OnSetStorageBlockBinding - // t2 OnBeginStreamOutput, OnEndStreamOutput, OnPauseStreamOutput, - // OnResumeStreamOutput, OnBindStreamOutput, OnPatchParameter - // f1 OnGenerateMipmap, OnCopyFramebufferToTexture (and OnClear's non-Whole kinds) - // d1 OnDrawVbo's indirect tail and user-index span (above) + // Same hand-over as the rows above: the codec validates the record, and the backend call is + // made by MG_Record::RecordVerbSink, the base of the server's ServerVerbSink. virtual Bool OnLaunchGrid(const MGPGridInfo& grid) { (void)grid; return false; diff --git a/MobileGL/MG_Pipe/SubsystemDeps.def b/MobileGL/MG_Pipe/SubsystemDeps.def index 691236f4..9f9715de 100644 --- a/MobileGL/MG_Pipe/SubsystemDeps.def +++ b/MobileGL/MG_Pipe/SubsystemDeps.def @@ -5,67 +5,14 @@ // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header -// -// D-K2's SUBSYSTEM DEPENDENCY RULE, IN ONE PLACE (P3b/P4b R-5). -// -// WHAT WAS WRONG. The rule "bit X requires bit Y" was stated in SIX places, and the audit found -// two of them WRONG in the same direction: -// -// * MG_Pipe/MGPipe.h's comment said "THREE OF THEM HAVE A DEPENDENCY" (there are six rows) and -// "The mirror pairs (10 without 11, 10 without 9, 7 without 10) are all fine" - but 10 -// without 11 is exactly D-K2's FOURTH row, refused by the server at -// Managers.cpp ResolveTextureResourceSubsystemArm and withheld by the client at -// PipeFill.cpp's table. PipeFill.cpp says so out loud: "The brief's original -// 'bit 10 without 11 is fine' is WITHDRAWN for P4a as built."; -// * MG_Backend/DirectGLES/Managers.h's header comment repeats both defects, in the header of -// the file that IMPLEMENTS the refusal. -// -// Neither wrong comment could fail anything, which is the whole argument for a table: the two -// EXECUTABLE statements of the rule are in different link units with no shared header, and the -// four PROSE statements drifted because nothing compared them to anything. -// -// WHAT THIS FILE IS. The rule, once, as rows. It is a preprocessor-only table like -// MG_Remote/FatalFamilies.def - no generator reads it - and it carries pure bit constants, so -// including it from MGPipe.h keeps check_include_closure.py's value-header rule satisfied. -// -// WHO READS IT. MGPipe.h turns it into kMGPipeSubsystemDependencies[], -// MGPipeSubsystemRequires(), MGPipeSubsystemDependenciesAreSet() and -// MGPipeSubsystemDependencyWhy(). The two executable statements of the rule read those, and as -// of wave 2-D package D3 they read NOTHING ELSE - every hand-written copy is gone: -// -// CLIENT dependency check MG_Impl/Pipe/PipeFill.cpp's P4aFamilyDependencyBits / -// P4aFamilyDependenciesAreSet (bits 9-12) and P5eFamilyIsLive -// (bit 13). The local kMGPipeP4aFamilyDependencies[] is deleted; -// what stays is the P4a-specific COVERAGE static_assert, because -// that gate answers for four of these six families and a fifth P4a -// family without a row here must still be a compile error. -// SERVER consumer gate MG_Backend/DirectGLES/Managers.cpp's four -// ResolveSubsystemArm() over PipeSubsystemDependencyMissing, -// which now takes the FAMILY rather than a literal bit (bits 9-12), -// plus ResolveVertexInputSubsystemArm (bit 8) and DirectGLES.cpp's -// ResolveBufferBindingSubsystemArm (bit 13). The texture family's -// two guarded calls collapse into one, and the refusal names -// whichever required bit is actually missing instead of always the -// first, because the missing set is computed from the row. -// -// The rows moved here one commit before the readers did (R-5 landed with PipeFill.cpp and -// Managers.cpp belonging to packages in flight), and the anti-drift property never waited for -// that: MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp drives BOTH readers at every -// interesting mask and compares them against these rows - a change to either side alone reds. -// It is now a comparison of two READERS rather than of two copies, which is strictly weaker as -// a drift detector and strictly stronger as a guarantee; it is kept because the two sides still -// fold in reasons of their own (the server's own family bit, D-C3's wire-caps refusal, the -// client's consumer and wired conjuncts) and those can still disagree. -// -// THE ROWS ARE NOT TRANSITIVE, and that is load-bearing rather than an oversight: each reader -// tests the required BITS against MOBILEGL_PIPE_PUSH alone, never the other family's liveness, -// because Espryt's resolvers classify their arms from that mask alone. Making the rows -// transitive here would make the two sides disagree at some mask, which is the failure the -// table exists to prevent. PipeFill.cpp:1576-1581 argues this at length and -// TextureEmitTest.cpp:1278-1283 pins it. -// -// THE MIRROR PAIRS THAT STAY FINE are stated in MGPipe.h beside the table rather than here, -// because an absence is not a row. +// The subsystem dependency rule ("bit X requires bit Y"), stated once as rows. Preprocessor +// only (no generator reads it) and pure bit constants, so MGPipe.h may include it under +// check_include_closure.py's value-header rule. MGPipe.h builds kMGPipeSubsystemDependencies[] +// and the MGPipeSubsystem{Requires,DependenciesAreSet,DependencyWhy} helpers from it; the client +// gate (MG_Impl/Pipe/PipeFill.cpp) and Espryt's ResolveSubsystemArm read only those, and +// MG_Test/Backend/DirectGLES/SubsystemDepsTest.cpp checks both against the rows. Rows are not +// transitive: each reader tests the required bits against MOBILEGL_PIPE_PUSH alone, as Espryt's +// resolvers do. The bit pairs that are safe without a row are listed in MGPipe.h. // clang-format off diff --git a/MobileGL/MG_Pipe/VerbRecordOps.h b/MobileGL/MG_Pipe/VerbRecordOps.h index b89da5c6..f8afca81 100644 --- a/MobileGL/MG_Pipe/VerbRecordOps.h +++ b/MobileGL/MG_Pipe/VerbRecordOps.h @@ -12,24 +12,12 @@ #include -// THE VERB -> RECORD TABLE (P15). Every MGPipeVerb names the record op its emitter publishes, and -// the client's barrier decision at the validate point is that op's wait class (PipeCalls.def) and -// nothing else. Commands are recorded and replayed asynchronously by default; a verb syncs only -// because the record it emits has a wait class that says so. -// -// IT IS THE FORWARD DIRECTION. MGP_VERB_OP_LIST (FieldOwnership.def) is op -> verb, the -// direction the SERVER needs to stamp a verb boundary, and it is many-to-one (one draw_vbo row -// for twenty draw verbs, one blit row for two blit verbs). Inverting it verb-first answered -// "no row" for every verb but the representative, and the old fallback turned "no row" into -// "barriered" - a forced applier quiesce per verb (FCL: one per frame from the blit, 1.65 ms of -// GL-thread wait). Here there is no fallback in either direction: a verb with no row, or with two, -// is a compile error (the static_assert below). -// -// A verb whose emitter can publish more than one op names the one with the STRONGEST wait class -// (the fill decision is made before the record exists, so it must cover the worst record the verb -// may emit). kMGPipeNoRecord names a verb that publishes nothing: the client answers it from the -// caps mirror, so no apply can read a fill for it. -// +// Verb -> record table: each MGPipeVerb names the record op its emitter publishes, and the +// client's barrier decision at the validate point is that op's wait class (PipeCalls.def) alone. +// It is the forward direction of MGP_VERB_OP_LIST (FieldOwnership.def), which is many-to-one and +// cannot be inverted. A verb that may publish several ops names the one with the strongest wait +// class, since the fill is decided before the record exists; kMGPipeNoRecord marks a verb +// answered from the caps mirror. A missing or duplicate row is a compile error, never a default. // X(Verb, Op) // clang-format off #define MGP_VERB_RECORD_OP_LIST(X) \ diff --git a/docs/Disaggregated/design/02-catalogue-and-records.md b/docs/Disaggregated/design/02-catalogue-and-records.md index e0db7f4d..fcbaed8e 100644 --- a/docs/Disaggregated/design/02-catalogue-and-records.md +++ b/docs/Disaggregated/design/02-catalogue-and-records.md @@ -28,6 +28,17 @@ - 20 个 draw 入口塌成 `DrawVbo` 一条;`Clear` 一条判别式;sampler 集合**没有 stage 维度**(192 单元合并空间);`SetTextureParams` 按资源寻址、与 sampler view 分开(D10);`SetIndexBuffer` 独立于 VAO 配置(D5)。 - 显式不移植:`GetIntegeri_v` / `GetInteger64i_v` / `GetProgramiv`、`set_pixel_unpack_state`(前端已解析)、压缩格式概念、`pipe_transfer`。 +#### 3.2.1 flag 与 WaitClass 的含义 + +- `kHasBlob` 严格表示 payload 拥有一个 `MGPBlobRef` 成员,而不是"这条调用带字节":解码器信任它就必须有成员可读。`ResourceRespecify` 的初始内容与 `ResourceFlushRange` 的字节不走 blobref,而以相邻的 `resource_subdata` 记录过线。 +- `kNeedsAck` 只表示这条调用的记录**可能**要 ack,由逐记录谓词决定(`MGPipeResourceRespecifyNeedsAck`,§8.3)。 +- `kReplySlot`:applier 返回 client 会据以动作的答案(清 level 脏标、闩参数)。flag 只导出一份 `kMGPipeCallFlags[]`(`generated/PipeWire.inc`),消费者不另抄。 +- 第五列 `WaitClass` 是值不是 opcode,生成为 `MGPipeWaitClassFor(op)`,是 `MGPipeBarriered`(§11.6)的静态一项;client 只在 run-ahead 武装(server 发布 `kCapRunAheadApply`)时据它决定等不等,否则走 lockstep: + - `kWaitReply`:答案不可推导且 client 要用,阻塞在 reply slot。与 `kReplySlot` 同在同缺,`gen_pipe.py` 拒绝不一致的行:不等的 reply 行会让 client 读一个没人填的 slot,没有 slot 的等待行会等一个没人发的答案。逐记录 route 谓词可再放宽(如 `MGPipeSubDataWantsItsReply`)。 + - `kWaitPresent`:节奏由 encode 之前取的 present credit 决定,publish 之后不再等。 + - `kWaitApplied`:client 停到记录 apply 完(今天只有 `applier_reset`)。 + - `kWaitNone`:apply 不读 client 内存,发布即走。 + ### 3.3 能力位(`MGPCapBit`) `CallMask` 取代"槽位是否为 null"这一隐式能力探测;`MGPCaps` = `DynamicBackendParameters` + `CallMask` + 格式能力表与 renderer 字符串两个 blob,握手后快照、`MakeCurrent` / `InitCapabilities` 后按 generation 重发布。位包括 viewport array、fp64 顶点、`kCapResidentSubData`、XFB / query 家族、`kCapNeedsHostIndexBytes`、`kCapNeedsHostUboBytes`、`kCapBackendOwnsXfbCapture`、`kCapRunAheadApply`(`MGPipeRunAheadCapBitsFor`:Espryt 自 P5e、Magma 自 P5f 之后的 Magma run-ahead 起发布,后者契约 `docs/archive/contracts/CONTRACT-MAGMA-RUNAHEAD.md`)。**不存在"归属开关"(D-B7)**:multi-draw 分档与 restart 重写永远由 server 拥有,client 只在 caps 要求时提供索引字节。 diff --git a/docs/Disaggregated/design/03-frontend.md b/docs/Disaggregated/design/03-frontend.md index ff829c60..d342fa75 100644 --- a/docs/Disaggregated/design/03-frontend.md +++ b/docs/Disaggregated/design/03-frontend.md @@ -14,6 +14,17 @@ per-setter 推送会把 Blaze3D 每个 batch 的冗余 `glEnable/glDisable` 变 - 对象类组(vertex buffers、framebuffer、sampler views / samplers / images、const / shader buffers、SO targets)靠 5 个**聚合世代**把快门降成一次 `Uint64` 比较——没有聚合就回答不了"有没有哪张已绑定纹理动了"。 - 完整性由 `scripts/gen_pipe_dirty_surface.py` 保证(每个 mutator → 必须 bump 的世代,`--check` + `--self-test` 进 CI)。**快门看不见自己的主体**是踩过三次的坑:新增字段必须带"记录字段 → setter → 快门"一行,优先混入已有世代(新计数器会撑大 pull 对象,G1 不允许)。 +#### 5.2.1 dirty-surface 映射的答案(`DirtySurface.def`) + +`DirtySurface.def` 为每个后端可观察的前端 mutator 写明"谁发布这次修改"。漏发布不报错、只会静默渲染陈旧状态,所以清单由 `scripts/gen_pipe_dirty_surface.py` 机械枚举:扫描 `MG_Impl/GLImpl` 与 `MG_State/GLState` 里按 `MUTATOR_PREFIXES` 匹配的 `pGLContext->` 调用和 `MGP_NOTE_MUTATION` 站点(后者带字段名,行写在外层函数上),`--check` 双向核对行集。 + +答案只列在**每条发生修改的路径**上都触发的发布者(值未变而提前返回不算修改);只在部分路径触发的位若单列,按表构造的快门会欠触发,而欠触发是危险方向。 +- `NEW_*`:`Tracker.h` 的 dirty 位,下一个 verb 发布;`kImmediate`:同一函数体直达后端;`kReverseChannel`:后端写回前端;`kNoBackendRead`:没有后端读点。 +- `kExplicitDestroy`:最后一个引用释放时,client 的 `MGPipeEmit*DestroyAndFree` 依次做 wire 删除(该类有删除行时)、后端通知、释放 slot;`kUnpublishedDestroy`:program pipeline 与 shader 没有 wire 句柄,无从删除。 +- `kPulledEveryVerb`:没有位移动,字段在其 verb 类的 may-read 表里,残余填充每个 verb 都拷贝;`kPulledPartialShutter|NEW_*`:同样靠拉取兜底,所附的位只在部分修改路径上触发。 + +位答案由脚本推导:RenderState 族与 `RenderState.cpp` 双向比对(`BumpVersions()` 动两个计数器,裸 `++m_version` 只动 `NEW_RENDER_STATE`);其余位把快门读的成员 / 字段与 mutator 传递写入的成员 / 字段求交,只能查出"该位的快门在任何路径上都不动"这一种错答,证明不了每条路径都动。写入根定位不了的函数被污染,依赖它的答案为 UNDECIDED,须列入 `MGP_DIRTY_SURFACE_UNDECIDED_LIST`,列了却已可判定同样报红。散文答案不经推导(`kPulledPartialShutter` 所附的位除外);扫描器按文本匹配函数体,lambda 里的 mutator 记到外层函数上,经 helper 发布的修改读作延迟发布,语义证明仍是 `MOBILEGL_PIPE_VERIFY`。 + ### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集(D-B1) ``` @@ -56,6 +67,16 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与 - **dirty 归属反转**:client 维护发射游标、发射后清自己的标志;清 dirty 要等 applier 的 acceptance(被拒的 level 留在脏表里);server 自己的重新变脏是 server 的事。 - `TextureUploadShapeScenario` 把上传形状录成金标(P3b/P4b D2 起是门;Mali 帧时增量无设备,按 Adreno 记录)。 +### 6.1 server 侧待上传集合 + +- client 在 applier 接受某 level 的 `ResourceSubData` 时就清掉自己的 dirty 标志;后端的上传循环有提前返回的臂(Espryt:纹理不完备、multisample 目标只刷新),所以已接受的形状由 applier 按 `(uploadTarget, level)` 累积在 `MGPipeResourceRecord::PendingUploads`,只在后端真正上传处消费并清除,任意次 bail 都不丢纹素。 +- 累积沿用 client 的 rect 模型:union box 取并、rect 列表拼接;任一侧只有 box,或列表将超过 `kMGPipeMaxPendingUploadRegions`,该项退化为只有 box(仍覆盖全部纹素,只多传空隙)。 +- respecify 只丢弃它替换掉的那份存储上的待上传(盒子在那份存储的坐标系里,跨缩小保留会越界上传): + - level 为空 = 整资源重定义(`glBufferData` / `glBufferStorage`、`glTexStorage*`、纹理视图),全部清空; + - 具名 level = 一次 `glTexImage*D`,只丢该 `(uploadTarget, level)`,与描述符是否变化无关。整体清空会丢掉别的 level 已被接受、尚未上传的纹素:`glTexImage2D(0)` → draw(纹理不完备,后端 bail)→ `glTexImage2D(1)`; + - 只换元数据(`RespecifyRedefinesNoStorage`:sticky 的 `BindMask` / `ImageBindableHint` 变化时重发的描述符)不丢任何项。 +- buffer 没有待上传项,也永远不算只换元数据。 + ## 7. Shader state = SPIR-V + 反射归档 - `CreateShaderState` 过线的是逐 stage SPIR-V + 反射归档,**不是源码**:glslang 全在 client、SPIRV-Cross 全在 server,文件级切割;server 侧无编译池。归档序列化器 `ProgramArtifactsCodec`,monolith 只在 verify 构建里调它(D-H3)。 diff --git a/docs/Disaggregated/design/05-backend-and-server.md b/docs/Disaggregated/design/05-backend-and-server.md index 99332492..32c0ae7e 100644 --- a/docs/Disaggregated/design/05-backend-and-server.md +++ b/docs/Disaggregated/design/05-backend-and-server.md @@ -13,6 +13,15 @@ Espryt 的 persistent ring、buffer pool、fallback-repack、scratch FBO、驱 ```cpp // MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,访问器类型与后端原来读到的完全一致 #if MOBILEGL_PIPE_PUSH + +#### 9.2.1 server 侧的 verb 戳与读检查 + +client 的残余填充在自己的 verb 边界给全部字段盖戳,server 读 `PipeInputs` 前必须换成自己的戳:与 client 共用一块时,不清零则每次读都“新鲜”、`rsp` 恒为 0,门形同虚设;两块分开时,没有戳则第一次读就中止。 + +- 盖戳点:`PipeApplier::StampVerbBoundary` 只在 `MGPipeVerbForWireOp`(`FieldOwnership.def` 的 `MGP_VERB_OP_LIST`)映到 verb 的 op 上调用 `MGPipeServerStampVerbBoundary`:递增 `CurrentVerbSerial`、记下 verb,给该 verb 类可读掩码(`FillPoints.def`)内的 `RECORD_SUPPLIED` / `APPLIER_DERIVED` 字段盖新戳,其余字段戳清零(“从未填充”先于 sticky 判定,所以清零也取消 sticky 豁免)。 +- 非边界记录(如两次 draw 之间的 `set_dynamic_state`)不盖戳,沿用前一边界的序号、掩码与 verb 名,但盖戳标志已在上一条记录后清除,读检查按盖戳之外处理;20 个 draw 入口都盖成 `DrawArrays`,掩码正确,Fatal 里的名字是泛称。 +- 读检查只在 server 盖戳的 verb 内按归属类(§3.1 的 G8)分流:`RECORD_SUPPLIED` / `APPLIER_DERIVED` 读到旧值是 `Fatal{RoleViolation}`,`FATAL` 直接中止,`BARRIER_PULLED`(仅供生成器对照,没有生产行)在该 verb 类内计入 `rsp`,由 `CountBarrierPull` 决定何时改为中止(记录未 barrier、两角色用不同块、strict 下未准入)。盖戳之外一律是普通 poison `Fatal{UnmigratedPipeInput}`。 +- 盖戳标志由 `PipeApplier::LeaveApplier` 在每条记录之后清除;server 进程里没有 client 半边替它清,漏清会让之后的读都按上一个 verb 的掩码判定。 # define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs) #else # define MGB_CTX (::MG_State::pGLContext) diff --git a/docs/Disaggregated/design/06-transport.md b/docs/Disaggregated/design/06-transport.md index c2eeb7db..bcbf667a 100644 --- a/docs/Disaggregated/design/06-transport.md +++ b/docs/Disaggregated/design/06-transport.md @@ -41,6 +41,16 @@ Phase 1(今天):GL 调用时刻把字节拷进 stage slot,slot 到 apply - **等待规则由 `WaitClass` 列决定(P5e)**:`kWaitReply` / `kWaitPresent` / `kWaitApplied` / `kWaitNone`;run-ahead 武装时 `kWaitNone` 发布即返回。barriered 谓词(`MGPipeBarriered`)client 与 server 用同一个函数、同一份数据算。 - 强制等待点:`MapBuffer(READ)` / `GetBufferSubData` 源走 `SyncGpuWrites`;`glFinish` = 等 `appliedSeq` 追上再排空反向通道;每个 `Server*` EGL forwarder 之前等一次。`glGetError` 最多晚一个 present credit。 +#### 11.6.1 barriered 谓词的条款 + +`MGPipeBarriered(op, payload, applierState)`(`MG_Pipe/PipeApply.h`)由 server 的 `ApplyOne` 计算,决定这条记录能否读 client 内存;client 不调用它,而是在发布前对同样的条款求值(`ClientSession` 的等待规则读 `WaitClass` 列,`PipeFill.cpp` 的 `ClientVerbIsBarriered` 另判 XFB)。两侧答案一旦不同,失败是静默的(client 继续跑,server 读到已经变了的值),所以条款只读线上可见的数据:静态 `WaitClass` 列、记录自身的 payload、ring 上更早的记录写进 applier 的状态。条款只有三条: + +1. `WaitClass != kWaitNone`; +2. 打开的 XFB span 内的 `kCtxVerb`(XFB 保持 lockstep); +3. 带 `kDrawClientArrays` 的 `draw_vbo`:run-ahead 下 client 在发布前就拒绝它,此条只为 lockstep 臂而在,使谓词完备。 + +新条款必须在同一次改动里同时加进这个函数与 client 侧的对应判断,不能只在一侧加。评判候选条款要问**哪条臂为它付代价**,而不是它让哪条车道变绿:曾为让 Espryt 间接 multi-draw 档读 `GL_DRAW_INDIRECT_BUFFER` 绑定合法化,把所有普通 multi-draw 升级为 barriered,于是默认档的每次 `glMultiDraw*` 都多一次会合,而这次读只发生在两个可选档里;最终改由做绑定的一侧按句柄回答(`MultiDraw.cpp` 的 `BoundDrawIndirectBufferId`),撤回了该条款。 + ### 11.7 事件回传与溢出 client 在 `glGetError`、query / sync 查询、`eglSwapBuffers`、buffer 回读与每轮等待循环中排空 `SEG_EVENT`。日志有损;语义事件无损:环满时 server 置 `eventRingFull`、在记录边界停 apply、敲 client,client 排空后清标志并敲回(两半都在,才不死锁)。run-ahead 下 producer 阻塞等排空,整笔预算 `MOBILEGL_IPC_EVENT_WAIT_MS`;client 不排空 / 已走 / 会话停止 → `ReverseChannelForfeit{…}` 弃投并闩住(Ph PH-6),而不是 `Fatal` 打掉 server。不发布 `kCapRunAheadApply` 的 server(lockstep 臂)保留 P5c 的 Fatal。 diff --git a/docs/Disaggregated/design/07-memory-readback-verification.md b/docs/Disaggregated/design/07-memory-readback-verification.md index 95d77a11..26dc5a44 100644 --- a/docs/Disaggregated/design/07-memory-readback-verification.md +++ b/docs/Disaggregated/design/07-memory-readback-verification.md @@ -22,6 +22,13 @@ T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `has - 不可避免(罕见):握手;surface 生命周期;`glReadPixels` 到客户内存;GPU-write pending 的 buffer 首次 CPU 读;带超时的 sync / query 结果;`glBufferStorage` 的 ack;纹理拉取;ring / stage 耗尽与 present credit。 - 跨机上每条 `kWaitReply`(`ResourceCreate`、`SetTextureParams`、纹理 `ResourceSubData`)都是一个 RTT:逐帧次数是 P6.5 必测数,减少它是 P9 / P10 的活。 +#### 13.1.1 kWaitReply 行的逐记录豁免 + +`kWaitReply` 是行级属性;一条记录是否真的阻塞由逐记录谓词决定,route 与 wire emit 表读同一个函数,避免一边等另一边没要的回包。因此上面第三条所列的纹理 `ResourceSubData` 在 wire 臂上已不逐条付 RTT。 +- `resource_subdata`(`MGPipeSubDataWantsItsReply`):buffer 那半的 Bool 无人读,不等。wire 臂上纹理那半也不等:纹理记录只在 server 消费纹理族时才发(caps 镜像里的 consumer 位,与 applier 的接受检查读同一事实),所以 applier 唯一的合法拒绝"无 consumer"落不到这条记录上;其余拒绝(如目标不是纹理、无纹素或无字节、未知句柄)都是 bug,server 侧会 trip wire 报告或计数具名。client 自己取消的记录(device lost、stage 耗尽)仍答"未接受",level 留脏重试。等待由此从每条记录移到 `SEG_STAGE` 窗口回收,与窗口成正比而不是与调用数成正比(实测见 `MEASUREMENTS.md`「纹理上传的等待粒度」一条)。 +- `resource_respecify`(`MGPipeResourceRespecifyWantsItsReply`):wire 臂上 buffer 那半不等;纹理那半等到对象的 create 被接受(`MGPipeHandleIsPublished`),之后 acceptance 已是 client 知道的事实,首次使用时付一次阻塞的 create;create 被拒则闩不置位,后续照常等真答案,代价是吞吐而不是静默。 +- monolith 下答案在调用线程上直接返回、没有代价,谓词保持原答案(纹理那半照收)。 + ### 13.2 五部分验证门(取代 monolith 的字节一致门) 1. **接口纯度**:A 门 include 图(`scripts/check_include_closure.py`)、B 门 server 镜像不引用 `MG_State::GLState` 与 glslang(P6 的 a6 实测 server 仍链 `MG_Impl`,由 `scripts/link_ratchet.py` 的下行棘轮追踪;P13 把 172 条降到 27 条具名债务、`# P13` 注解清零并由脚本拒绝回潮,W7 的 `MobileGL_server_linkcheck` 把它变成真链接)、C 门 `MG_Backend` 里零 `pGLContext`;外加"每个后端 memo 键都是 `{slot, gen}`"(`HandleRecycleScenario`)。 diff --git a/docs/Disaggregated/design/08-runtime-and-platform.md b/docs/Disaggregated/design/08-runtime-and-platform.md index 208ef58d..f2259446 100644 --- a/docs/Disaggregated/design/08-runtime-and-platform.md +++ b/docs/Disaggregated/design/08-runtime-and-platform.md @@ -31,3 +31,29 @@ - Linux / WSL / CI 永不开窗(`EGL_PLATFORM=surfaceless`);Wayland 不支持。Windows / 桌面 client 在 TCP 终局下是真实形态(Windows 是第二批,开放问题 22);macOS 不拆分;Windows 机器不是正确性门。 - server 死:client 读到挂断 → **device-lost 闩**(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`、reset status `GL_UNKNOWN_CONTEXT_RESET`);`Session::Fail` 漏斗向对端发 `SessionFault` 帧命名家族(`FatalFamilies.def`)。闩之前创建的 context 永久丢失(`eglMakeCurrent` / `eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`,`glGetError` 报一次 `GL_CONTEXT_LOST`,销毁时不往新 session 发任何记录)。**进程可恢复**(`MOBILEGL_IPC_RECOVER`,默认 1):应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 拆掉丢失的 session、重新拨号得到新 session,client 侧描述旧 server 的缓存全部清空(发布闩、CSO/sampler 缓存、emitter 闩、create window、applier-reset 序号、persistent map),wire epoch 前进使旧 GLContext 一律视为丢失;surface 保留并在下次 make-current 时于新 session 重建(EGL 对 power-management 事件的语义),Wayland 窗口的 shared image 在新 session 重新分配。in-process server 与 `fd:` 对不可重拨,仍是永久丢失。`MOBILEGL_IPC_RESPAWN`(server 重启后全量重推)仍是具名拒绝。client 死:server 读到 EOF 立即销毁 context 并退出(supervisor 继续服务下一连接)。 + +#### 15.3.1 Fatal 家族与 wire FatalCode + +内部 Fatal 家族(`MG_Pipe/FatalFamilies.def`)远多于 wire 上 `FatalCode` 的 7 个值,所以家族是内部枚举:对端只收到粗粒度的码,细粒度的词留在本地日志与 `SessionFault` 帧的 `family` 字符串里。码只回答「这是哪一类结束」: + +- `ProtocolCorruption`:无法正确执行的东西——畸形记录、未实现的 verb / surface、两个角色必须守住的不变式、不合契约的回复。对端读到的是「双方对协议的理解不一致」。 +- `RingOverrun`:共享环或事件通道溢出,是传输容量问题。 +- `SegmentMismatch`:共享段的尺寸放不下某个角色必须放进去的东西(尺寸错,不是一时满)。 +- `AbiMismatch`:两端的布局或协议版本不一致。 +- `ServerCrashed`:等待方归咎于对端的活性结束:排空未完成、apply 线程不在、server 拿不到必需的原生资源(`ResourceUnavailable`)、server 窗口被销毁(`ServerWindowLost`)、GPU 设备丢失(`BackendDeviceLost`,只闩住该 session)。拿不到资源不报 `SegmentMismatch`,那会把错归到共享段尺寸上;wire 上也没有「拿不到资源」的值,新增会改 `wireFingerprint`。 +- `DeviceLost`:只由 device-lost 闩设置,没有家族投影到它。 + +### 15.4 共享图像(`shared_image`) + +server 分配并以 dma-buf 描述符导出的颜色缓冲,让一个 client 的帧不离开 GPU 内存就进入另一个 client 的纹理。一行记录(`MGPSharedImageOp`),每个操作都经 reply slot 以 `MGPSharedImageReply` 回答;server 交给 client 的描述符走 aux socket,先于 reply 到达;`ImageId` 由 server 铸造,server 生命期内不复用。`Damage` 是 GL 窗口坐标、左下原点的矩形,0 个 = 整个表面。 + +| 操作 | 语义 | +|---|---| +| `Allocate` / `Release` | 新建 `Width×Height` 的 `Format` 图像,reply 给出 id,描述符经 aux socket 送达 / 会话放掉对 `ImageId` 的引用 | +| `Import` | 认领 client 在 aux socket 上排队的描述符(`MGPSharedImageFdOffer` 带本记录的 seq);须是本 server 导出的图像,尺寸与格式一致 | +| `Present` | 当前 context 的默认帧缓冲按顶行在先拷进 `ImageId`(窗口帧成为 wl_buffer 的内容),只拷 `Damage`:client 按图像记录它自上次写入后缺的区域;GPU 拷完才回 reply | +| `Attach` | 纹理 `Texture` 的存储变成 `ImageId`(`glEGLImageTargetTexture2DOES`),直到它下一次定义存储的 respecify | +| `QueryBufferAge` | 在流中回答当前 draw surface 的 `EGL_BUFFER_AGE_EXT`(0 = 未知);应用先声明绘制区域(`EGL_KHR_partial_update`)时 `Format = kMGPBufferAgeDamageRegionFollows` | +| `SetDamageRegion` | `eglSetDamageRegionKHR`:本帧将画的区域 | +| `Flush` | 往图像里渲染的会话(X server 的 glamor)的 `glFlush` / `glFinish`:自上个边界用过的图像按 flush 的 fence 发布为已写、已读(隐式同步),发布后回答 | +| `NativeFence` | `EGL_ANDROID_native_fence_sync`:会话已提交工作完成时 signal 的 sync_file(写图像的会话同时做 `Flush` 的发布),经 aux socket 先于 reply 送达,`Format = kMGPNativeFenceFdFollows`;`Format = 0` 表示 server 给不出描述符,reply 发出时工作已完成 |