mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Docs] (docs/Disaggregated, MG_Backend/MGPipe, MG_IntegrationTest/Harness, MG_Pipe): shorten long comments and move their design arguments into docs
This commit is contained in:
@@ -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.
|
||||
//
|
||||
// `<field>@<verb>` and the `[BARRIER-PULLED, …, retires in <phase>]` 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 `<field>@<verb>` 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<SizeT>(field);
|
||||
const SizeT verbIndex = static_cast<SizeT>(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
|
||||
|
||||
@@ -51,26 +51,12 @@ namespace MobileGL::MG_Pipe {
|
||||
Optional<MGPipeInputField> MGPipeFindInputField(const char* name);
|
||||
Optional<MGPipeVerb> 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, "<field>@<verb>"} (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 "<none>";
|
||||
// 2. stamp every RECORD-SUPPLIED and APPLIER-DERIVED field with the new serial - those
|
||||
// are exactly the fields the records this verb carried can answer;
|
||||
// 3. ZERO every BARRIER-PULLED and FATAL field's stamp.
|
||||
//
|
||||
// (3) is what makes the instrumentation real. The client's residual fill stamps ALL 63
|
||||
// fields at its own verb boundary (PipeFill.cpp step 4), so without the zeroing every
|
||||
// field would read fresh on the server, `rsp` would be identically 0, and the gate would
|
||||
// be decoration - the precise "an inproc implementation proves nothing" failure R-2
|
||||
// exists to prevent. Zeroing also cancels the sticky exemption for free: generated/
|
||||
// PipeFilled.inc tests "never filled" BEFORE it tests sticky, so gen == 0 wins.
|
||||
//
|
||||
// The value a BARRIER-PULLED read then gets is still the client's residual fill's, and it
|
||||
// is still CORRECT - because the verb barrier (R-1) leaves exactly one of the two threads
|
||||
// runnable. That is the debt, not a bug; `rsp` is its size.
|
||||
//
|
||||
// v1 calls this from Server/PipeApplier::StampVerbBoundary. MGPipeVerbForWireOp maps the
|
||||
// record's op onto a verb and answers kVerbCount for an op that is not verb-shaped, which
|
||||
// is the case the applier must NOT stamp on: a set_dynamic_state between two draws is not
|
||||
// a new verb, and stamping there would retire the previous verb's answers early.
|
||||
//
|
||||
// TWO THINGS THE CALLER INHERITS AND SHOULD NOT REDISCOVER:
|
||||
//
|
||||
// (a) The records BETWEEN two boundaries apply under the earlier boundary's stamp - its
|
||||
// serial, its class mask and its verb NAME. That is correct today because every
|
||||
// MGPipeApply* entry point touches gPipeInputs through MGPipeApplyAccess, which
|
||||
// carries no MGP_INPUT_CHECK; the day one of them calls back into the backend, its
|
||||
// reads will be judged against a verb they do not belong to.
|
||||
// (b) The verb a draw record stamps is MGPipeVerb::DrawArrays for ALL TWENTY draw verbs.
|
||||
// The class mask is right (FillPoints.def puts all twenty in kDraw) and the NAME in a
|
||||
// Fatal is not: a glDrawElements that aborts will say "@DrawArrays". draw_vbo carries
|
||||
// no verb id, so fixing it means either a field on the record or a second argument
|
||||
// here; it is cosmetic for the verdict and misleading for the reader, and it belongs
|
||||
// with whatever phase widens draw_vbo to the multi-draw family (P8).
|
||||
// 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
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
+23
-146
@@ -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<MG_State...>, 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<MG_State...>
|
||||
// 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<FramebufferObject>, an ImageTextureBinding, a TextureUnit or a
|
||||
// SharedPtr<ProgramObject> - 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<Array<BindingSlotRange1D, 84>, 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) \
|
||||
|
||||
@@ -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:<member> plus FIELD:<member>.<leaf>,
|
||||
// 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) \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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),
|
||||
|
||||
+10
-45
@@ -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 Resolve<Family>SubsystemArm 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <MG_Pipe/MGPipe.h>
|
||||
#include <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 {
|
||||
|
||||
+141
-469
@@ -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 <MG_Backend/BackendObject.h>
|
||||
#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<Uint16>(MobileGL::TextureUploadTarget)
|
||||
// P4a, ID-12 / esprytobj DV-5: WAS Pad0, and the size did not move - the two bytes
|
||||
// were already here. static_cast<Uint16>(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<Uint16>(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,
|
||||
|
||||
+147
-471
@@ -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<Uint32>(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 <class Entry, class ArrayT>
|
||||
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<const MGPDrawInfo*>(payload);
|
||||
if ((draw.Flags & static_cast<Uint8>(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
|
||||
|
||||
+163
-565
@@ -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<const MG_State::GLState::ProgramArchive> 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<MGPipeShaderCsoRecord> 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<MGPipeFramebufferRecord> FramebufferRecords;
|
||||
};
|
||||
|
||||
@@ -654,24 +528,13 @@ namespace MobileGL::MG_Pipe {
|
||||
Vector<MGPipeRenderStateCsoRecord> 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,
|
||||
|
||||
+16
-169
@@ -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).
|
||||
|
||||
+40
-144
@@ -9,23 +9,13 @@
|
||||
#pragma once
|
||||
#ifndef MOBILEGL_MG_PIPE_MUTATION_H // belt and braces: reachable as <MG_Pipe/..> 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 <MG_Pipe/MGPipe.h>
|
||||
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/<Family>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/<Family>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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+38
-225
@@ -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<Name>`. There was no
|
||||
// encode twin. Every `MG_Impl/Pipe` emitter called `MGPipeApply<Name>` 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, "<row>"}`, 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<Name>, never
|
||||
// MGPipeApply<Name>, 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 <Includes.h>
|
||||
@@ -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<Name> for each of the thirty-eight
|
||||
// The call-site names: MGPipeRoute<Name> 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<MGPipeResourceTarget>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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, "<GL slot>"} 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;
|
||||
|
||||
@@ -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
|
||||
// Resolve<Family>SubsystemArm() 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 Resolve<Family>SubsystemArm 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
|
||||
|
||||
|
||||
@@ -12,24 +12,12 @@
|
||||
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
|
||||
// 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) \
|
||||
|
||||
@@ -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 要求时提供索引字节。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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`)。
|
||||
|
||||
@@ -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 发出时工作已完成 |
|
||||
|
||||
Reference in New Issue
Block a user