// MobileGL - MobileGL/MG_Pipe/PipeApply.h // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header #pragma once #include #include "MGPipeRenderStateSpans.h" #include "MGPipeTypes.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. namespace MobileGL::MG_State::GLState { struct LinkArtifacts; struct SpirvArtifacts; } // namespace MobileGL::MG_State::GLState namespace MobileGL::MG_Pipe { struct PipeInputs; // --------------------------------------------------------------------------------- // The CSO store // --------------------------------------------------------------------------------- // One record per live render-state CSO, indexed by MGPipeHandle::Slot. It keeps the 396 // pipeline bytes because an incremental create_render_state names only the chunks that // moved against a BaseCso - the rest has to come from somewhere, and that somewhere is // the record the client is naming. struct MGPipeRenderStateCsoRecord { Uint32 Gen = 0; Bool Live = false; Array PipelineBytes{}; }; // --------------------------------------------------------------------------------- // 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. struct MGPipeResourceOps { void (*Create)(MGPipeHandle res, const MGPResourceDesc& desc); void (*Respecify)(MGPipeHandle res, const MGPResourceDesc& desc, const void* initialBytes); void (*SubData)(MGPipeHandle res, const MGPSubData& record, const void* bytes); // kOptional: may be null. `bytes` is the application's staging store and is valid for // the duration of the call only. void (*SubDataResident)(MGPipeHandle res, const MGPSubData& record, const void* bytes); void (*FlushRange)(MGPipeHandle res, const MGPFlushRange& record, const void* bytes); void (*Readback)(MGPipeHandle res, const MGPReadback& record); void (*Destroy)(MGPipeHandle res); void* (*MapPersistent)(MGPipeHandle res, Uint64 size, const void* seedBytes); void (*UnmapPersistent)(MGPipeHandle res); }; // Install / read the table. A null argument uninstalls, which is what a backend does at // context teardown and what every build that has not migrated the family sits at. void MGPipeSetResourceOps(const MGPipeResourceOps* ops); const MGPipeResourceOps* MGPipeGetResourceOps(); // --------------------------------------------------------------------------------- // 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. inline constexpr Uint32 kMGPipeMaxResourceSlots = 1u << 20; inline constexpr Uint32 kMGPipeMaxVertexElementsSlots = 1u << 16; // P4a's three, and the argument is written out for each because the records differ in // size. None 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 corrupt. // // A sampler CSO record is a 100-byte value plus a handle, and sampler CSOs are // CONTENT-ADDRESSED at capacity 256 on the client, so the live population is bounded by // that cache and not by the application. 1<<16 is far above anything a GL program can hold // and small enough that a corrupt slot is refused rather than allocated. inline constexpr Uint32 kMGPipeMaxSamplerCsoSlots = 1u << 16; // A sampler VIEW is identity-addressed one per ITextureObject (P4a D-F2), so its // population tracks the texture population exactly and it takes the texture bound. inline constexpr Uint32 kMGPipeMaxSamplerViewSlots = 1u << 20; // The shader-CSO bound is the SLOT LIMIT ITSELF, because the composite band lives inside // that space (MGPipeHandles.h): a bound below it would refuse the very slots // AllocateComposite is allowed to hand out. inline constexpr Uint32 kMGPipeMaxShaderCsoSlots = kMGPipeShaderCsoSlotLimit; static_assert(kMGPipeMaxShaderCsoSlots > kMGPipeShaderCsoCompositeSlotBase, "the ShaderCso bound must contain the composite band, or a composite handle " "is refused as out of range on arrival"); // ID-19(b): the framebuffer record is now PER OBJECT and its table is slot-indexed like the // five above, so it takes a bound on the same terms. A framebuffer record is 304 bytes and // an FBO is a CONTAINER object - not shared between contexts, minted a few dozen at a time // by a renderer and a few hundred by a shader pack - so 1<<16 is orders of magnitude above // any live population and still turns a corrupt Uint32 into a refusal rather than a // 4-billion-entry resize. inline constexpr Uint32 kMGPipeMaxFramebufferSlots = 1u << 16; // THE FOURTH set_framebuffer_state TARGET, AND IT IS THE CONTRACT'S TO MINT (c0e: // MGPipeFramebufferTarget::Named = 3). It is declared here as a plain constant because wire // v3 and c0e run in parallel: the applier must ADMIT the value now, and this package may not // write MGPipeTypes.h. When c0e lands, this constant is deleted and every use below becomes // static_cast(MGPipeFramebufferTarget::Named) - wire's verification round retires it, // and the static_assert underneath is what makes forgetting impossible: the day the // enumerator exists, MGPipeFramebufferTarget::Count becomes 4 and this fires. // // ITS MEANING: "this record describes the framebuffer it names; no binding changes." Draw / // Read / Both write the record AND set the bound handle(s); Named writes the record only. // That is what lets the DSA entry points - BlitNamedFramebuffer and the four // ClearNamedFramebuffer* - be handed a record for a framebuffer that is bound to neither // binding, which is the hole esprytobj's C-1 found: the applier used to hold the two BOUND // records only, so a named blit or clear reached a driver FBO that never got its // attachments. inline constexpr Uint8 kMGPipeFramebufferTargetNamed = 3; static_assert(static_cast(MGPipeFramebufferTarget::Count) == kMGPipeFramebufferTargetNamed, "c0e has landed MGPipeFramebufferTarget::Named: delete kMGPipeFramebufferTargetNamed " "and spell the enumerator (wire's verification round, ID-21)"); // The two framebuffer BINDINGS, and there are two rather than three: Both and Named are // things a RECORD says, not bindings a server has. MGPipeApplierState::BoundFramebuffer is // indexed by MGPipeFramebufferTarget::Draw / ::Read, which is what makes package D's // "is this framebuffer the one bound to target t" one array compare (ID-19(d)). inline constexpr Uint32 kMGPipeFramebufferBindingCount = 2; static_assert(static_cast(MGPipeFramebufferTarget::Draw) == 0 && static_cast(MGPipeFramebufferTarget::Read) == 1, "BoundFramebuffer is indexed by the target byte; Draw and Read must be 0 and 1"); // ---- P4a's SHAPE bounds, and they are the same argument the slot bounds above make, one // level down: every number below arrives inside a payload, every one of them decides how // much the applier allocates or how far it indexes, and NONE of them is ever allocated by // being named. A record that names one past its bound is Fatal{ProtocolCorruption} - the // verdict this file reserves for a record that would make the server act outside its own // storage - and never a resize. // A sub-data record's mip level. GL's own bound is log2 of the maximum texture size, which // no device reports above 2^16, so a level index of 32 addresses a texture no // implementation can allocate and is a corrupt record rather than a large one. It is NOT // MGPTextureParams::MaxLevel's bound: GL_TEXTURE_MAX_LEVEL defaults to 1000 and is a // parameter, not a storage level, so nothing here polices it. inline constexpr Uint16 kMGPipeMaxTextureLevels = 32; // The pending-upload set (below) is keyed by (UploadTarget, Level) and both halves come // off the wire. Levels are bounded above; upload targets are not - a cube face, an array // target and a rectangle target are all legal values - so the number of DISTINCT keys one // resource may accumulate is bounded here. Six cube faces times 32 levels is 192; 256 // leaves room for a target space this phase has not enumerated and still refuses the // unbounded growth a corrupt Uint16 would otherwise buy. inline constexpr Uint32 kMGPipeMaxPendingUploads = 256; // The rect list behind one pending entry. The frontend keeps at most MipmapStorage's // kMaxDirtyRects = 96 per level and answers "0 rects" for everything it cannot describe // that way, which is the model this mirrors: an accumulation that would exceed this // collapses to BOX ONLY - the same answer, with the same meaning, and never a dropped // region. 256 is that bound with room for several emissions accumulating behind a bail. inline constexpr Uint32 kMGPipeMaxPendingUploadRegions = 256; // The default uniform block's image, the one allocation P4a adds per program. The size // comes from the program's own MGPProgramDesc::GlobalUboSize, so it is checked ONCE at // create_shader_state and the set_global_constants that follows can only allocate what the // create already declared. 16 MiB is four orders of magnitude above any default uniform // block a real program links and still turns a corrupt Uint32 into a refusal. inline constexpr Uint32 kMGPipeMaxGlobalConstantsBytes = 16u << 20; // One record per live resource, indexed by MGPipeHandle::Slot, kind Buffer; slot 0 is the // reserved null handle and is never live. struct MGPipeResourceRecord { Uint32 Gen = 0; Bool Live = false; // The last create/respecify, verbatim. The backend reads its Width / Usage / // StorageFlags / HasDefinedContent instead of asking the frontend object. MGPResourceDesc Desc{}; // SERVER-OWNED, monotone, and it never crosses the line: an MGGen-class counter, ++ on // every mutation this applier applies (respecify, sub-data, flush range, resident // sub-data). It is what replaces the frontend change serial the backend used to // mirror, and no MGPipe call may require the client to provide or know one. Uint64 Serial = 0; // ALWAYS FALSE IN P3a, AND WRITTEN BY NOBODY. It exists so the phase that pushes // persistent-mapped host writes can set it with zero new record kinds; a verify build // pins that it is false, so that phase cannot land a silent semantic change under it. Bool HasLiveHostWrites = false; // ---- P4a. Only a record of kind Texture ever carries these; a buffer's stay at // their defaults, which is what keeps ONE record type for the discriminated // descriptor rather than a second one that would have to be kept in step with it. // set_texture_params, per texture OBJECT and independent of any binding - which is // the whole point of addressing it by resource: a texture that is only an FBO // attachment, only an image-unit binding or only a glCopyImageSubData endpoint has no // sampler view to hang its parameters on, and today the READ-attachment case reaches // no parameter push at all. ParamsSerial replaces the twin's // m_syncedTextureParamsVersion + m_forceTextureParamsResync pair. // // Params.BuiltinSampler MAY NAME A CSO WHOSE RECORD IS GONE. set_texture_params // deliberately does not resolve it (the sampler CSO is content-addressed and shared, // D-F1, and the ordering between the two families is the emitter's), and // delete_sampler_state does not sweep the textures that name the CSO it drops. So a // consumer that follows this handle must expect SamplerCsos[slot] to be dead or // recycled and treat that as it treats any other stale handle - it is an ordering fact // about the two emitters, not a corrupt record. MGPTextureParams Params{}; Uint64 ParamsSerial = 0; // 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. struct PendingUpload { Uint16 UploadTarget = 0; Uint16 Level = 0; MGPBox UnionBox{}; Vector Regions; }; Vector PendingUploads; }; // --------------------------------------------------------------------------------- // P4a: the three new object-record kinds (D-J1) // --------------------------------------------------------------------------------- // // All three follow MGPipeResourceRecord's shape exactly - Gen, Live, a payload and a // server-owned monotone Serial - because the body-level idioms are the same ones: // a create starts the record OVER rather than editing it (a recycled slot's record must // not contribute one field, and Serial stays 0 because a create is not a mutation, so a // fresh backend twin starting at 0 agrees without either side publishing anything); the // serial moves BEFORE the backend is told; a destroy drops the record whole and keeps the // generation, and the CLIENT frees the slot afterwards. // create_sampler_state / delete_sampler_state. The parameters cross byte for byte // INCLUDING borderColorForm - all three border representations are always numerically // populated, so the value alone cannot say which driver entry point to use - and // MOBILEGL_PIPE_VERIFY compares them FIELD BY FIELD (PipeFields.def's // MGP_FIELDS_SamplerParameters), because the struct has three bytes of trailing padding // and a byte comparison of it is a coin flip rather than a gate. struct MGPipeSamplerCsoRecord { Uint32 Gen = 0; Bool Live = false; SamplerParameters Params{}; Uint64 Serial = 0; }; // create_sampler_view / delete_sampler_view: ONLY the view restrictions. Everything a // glTexParameter writes lives on set_texture_params instead. Re-issuing on the same // handle is how a restriction change travels (Gen moves only on slot reuse); it bumps // Serial and does not rebind anything. struct MGPipeSamplerViewRecord { Uint32 Gen = 0; Bool Live = false; MGPSamplerView View{}; 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. struct MGPipeShaderCsoRecord { Uint32 Gen = 0; Bool Live = false; MGPProgramDesc Desc{}; // GetUBOContentVersion() as last received. ~0u is the backends' "never uploaded" // sentinel and the client must never emit it, so it is also what this starts at. Uint32 GlobalConstantsVersion = ~Uint32{0}; Vector GlobalConstants; Uint64 GlobalConstantsSerial = 0; Uint64 Serial = 0; }; // The vertex-elements CSO as the applier holds it: the unpacked blob, both views, plus // the serial the backend's per-VAO twin compares against instead of a wrapping Uint16 // configuration version plus an identity patch. // // The 32 is GL's MAX_VERTEX_ATTRIBS as MobileGL advertises it (kMGPipeMaxVertexAttribs, // MGPipeTypes.h), which is also the bound the record's two declared counts are checked // against before the blob is unpacked. struct MGPipeVertexElementsRecord { Uint32 Gen = 0; Bool Live = false; Uint32 AttributeCount = 0; Uint32 BindingPointCount = 0; Array Attributes{}; Array BindingPoints{}; // Server-owned MGGen, ++ on every create_vertex_elements applied to this handle - // including a RE-create on the same handle, which is how a configuration change // travels (the handle is minted per frontend VAO and Gen moves only on slot reuse). Uint64 ContentSerial = 0; }; // set_framebuffer_state's record, HELD PER FRAMEBUFFER OBJECT and indexed by the handle's // slot (ID-19(b)). It is the one record kind in this file whose object has NO WIRE LIFETIME: // the catalogue has no framebuffer create and no framebuffer destroy, because a framebuffer // is state and set_framebuffer_state is the only call that names one (D-I2). So there is // nothing to mark dead and nothing to refuse against, and a slot is simply OVERWRITTEN by // its successor's record - which is correct rather than merely tolerable, since the record // that reaches this table is the description of whatever object holds the slot NOW. // // `Live` is therefore NOT a lifetime. It means "a record has been written at this slot", // which is the only question a reader can ask: it separates a table entry that exists // because the vector grew past it from one an emission actually wrote. The GENERATION is // still checked on every lookup (P3a contract-review M2), and a mismatch is a LOUD refusal - // it means an emitter handed a stale handle, or minted a successor without describing it, // which is exactly the seam defect the DSA arm would otherwise turn into a blit into a // driver framebuffer with no attachments. struct MGPipeFramebufferRecord { Uint32 Gen = 0; Bool Live = false; MGPFramebufferState State{}; }; struct MGPipeApplierState { // Indexed by slot; slot 0 is the reserved null handle and is never live // (MGPipeHandles.h kMGPipeFirstAllocatableSlot). Vector RenderStateCsos; // The last bind, so a rebind of the same handle can be answered without a scatter. MGPipeHandle BoundRenderStateCso = kMGPipeNullHandle; // The residual block as last received. Compared against the assembled state on every // set_residual_value_state; a disagreement is the D9 trip wire. ResidualValueBlock Residual{}; Bool HasResidual = false; // 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. Uint32 ScatteredChunkBits = 0; // What the two trip wires last did. A wire nothing can observe is a gate that cannot // go red for the reason it exists (ROADMAP.md), and only a poison or verify build // aborts: the shipped push build counts and logs, so these counters are how a unit // case sees the wire fire in EVERY build rather than in one. Uint32 ResidualCapabilitiesCompared = 0; // of the 35, at the last set_residual_value_state Uint32 ResidualDivergences = 0; // cumulative Uint32 PatchCarrierComparisons = 0; // cumulative, armed set_patch_state calls only Uint32 PatchCarrierDivergences = 0; // cumulative // ---- P3a (D-G4). ---- // // THE TWO HALVES BELOW HAVE DIFFERENT LIVES, and MGPipeApplierReset is where the // difference is spent: the OBJECT RECORDS describe GL objects and outlive a // make-current; the WORKING STATE describes what the next draw fetches with and does // not. Reading the whole block as "per context" is what dropped a shared buffer's // record at every context switch and made the write that followed it disappear. // ---- object records: indexed by MGPipeHandle::Slot of kind Buffer / // VertexElementsCso, and NOT part of the working state. // // A GL object lives in a SHARE GROUP, not in a context: a buffer created before a // make-current is the same buffer, with the same storage, after it, and its record is // the only thing the backend has left to read that storage's extent and mutation // serial out of (D-A4 re-keys IsBufferDrawClean onto exactly those two). Dropping the // records at a make-current would therefore make every subsequent glBufferSubData on a // pre-existing buffer resolve to nothing and be refused - a lost write, in a build // where the refusal's assertion has compiled out. // // They are 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 // MGPipeApplierReleaseObjectRecords when the served context and its applier go away. // Nothing else. Vector Resources; Vector VertexElementsCsos; // ---- P4a's object records. FIVE MORE TABLES, and the two resource ones are separate // Vectors rather than more rows of `Resources` above because the slot space is PER // KIND: a Buffer, a Texture and a Renderbuffer can all hold slot 7 at once, so a // single slot-indexed table would alias three different objects onto one record. The // record TYPE is shared - one discriminated descriptor for buffers, every texture // target and renderbuffers - and the bound is shared; only the table is per kind. // // Like the two above they are share-group state: MGPipeApplierReset does not touch // them, and only the object's own death signal and MGPipeApplierReleaseObjectRecords // clear them. Vector TextureResources; Vector RenderbufferResources; Vector SamplerCsos; Vector SamplerViewCsos; Vector ShaderCsos; // The ShaderCso COMPOSITE band's records, indexed by (slot - the band's base), for the // same reason MGPipeSlotAllocator keeps the band in a table of its own: the band // starts at 983040, so one program-pipeline composite in the slot-indexed vector above // would grow it to ~983k records of ~240 bytes each. THE SERVER STILL NEVER LEARNS IT // IS A COMPOSITE - the split is an indexing detail on this side of the wire, the // handle is an ordinary ShaderCso handle, and create/bind/delete_shader_state name it // exactly as they name any other program. Vector CompositeShaderCsos; // AND THE SIXTH, WHICH IS THE ONE ID-19 ADDED. Keyed by the FRAMEBUFFER HANDLE's slot, // for the reason MGPipeFramebufferRecord states: the two bound-target records the phase // started with could not describe a framebuffer that is bound to neither binding, and // the five DSA entry points (BlitNamedFramebuffer, the four ClearNamedFramebuffer*) hand // Espryt exactly that. // // IT IS AN OBJECT TABLE AND IT LIVES WHERE THE OTHER OBJECT TABLES LIVE, which is also // its make-current rule: MGPipeApplierReset does NOT clear it. An FBO is not shared // between contexts, but its record is addressed by a slot out of one global allocator, // so nothing aliases across a switch - and dropping the table would leave a // DSA-only framebuffer with no record and no event that would ever re-emit one (the // client's suppressor invalidation re-emits the two BOUND records and nothing else). Vector FramebufferRecords; // Every call this applier REFUSED because it named a record this applier does not // have: an unknown slot, a slot that is not live, or a generation that has moved on // under it. The refusal is a defined no-op - nothing stored, nothing dispatched, no // serial moved - for the reason written beside kResourceRefusalNote in PipeApply.cpp, // but A NO-OP NOBODY CAN SEE IS A DROPPED CALL NOBODY CAN SEE: MOBILEGL_ASSERT compiles // out at INFO, which is what all three gate builds and every shipped build are, so // these two are how a unit case - and an operator reading a log - observe it in EVERY // 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. Uint64 RefusedObjectCalls = 0; // ---- working state: what the next draw fetches with. All of it is per context and // all of it is cleared by MGPipeApplierReset, EXCEPT the two serials, which only ever // advance (see there). // The last bind_vertex_elements. Null is legal and means "no VAO bound". MGPipeHandle BoundVertexElements = kMGPipeNullHandle; // The last set_vertex_buffers, as received: the entries, the window they describe, // and the fetch base instance they are valid for. Array VertexBuffers{}; Uint32 VertexBufferStart = 0; Uint32 VertexBufferCount = 0; // The RAW value the client sent (MGPVertexBuffers::BaseInstance). It is NOT a resolved // shift: whether the fetch shift has to be emulated at all is a backend capability - a // device with native base-instance support shifts nothing - and emulation is // server-owned, so the backend arm turns this into a per-attribute byte shift out of // each attribute's own stride and divisor. This header sits below MG_Backend and may // not ask that question. The client never pre-shifts an offset and never learns the // answer. Uint32 VertexFetchBaseInstance = 0; // Server-owned MGGen, ++ on every applied set_vertex_buffers. It is what retires the // backend twin's wrapping-Uint16-plus-identity patches - which means IT MUST NEVER // HAND OUT A VALUE TWICE. A reset ADVANCES it (the cleared window is itself a change // the twin has to hear about) and never returns it to 0: a counter that restarts walks // back through every value it has already stamped into a twin that outlived the // switch, and the identity patch that used to close that hole is exactly what D-G4 // deletes on the twin's side. Uint64 VertexBuffersSerial = 0; // The last set_index_buffer. Independent of the vertex-elements configuration by // design (D5): the index slot is not part of a VAO's configuration version. MGPIndexBuffer IndexBuffer{}; // Advanced, never zeroed, for VertexBuffersSerial's reason. Uint64 IndexBufferSerial = 0; // Every map_persistent EMISSION, i.e. every acquisition attempt - mint OR decline - // because every one of them needs an answer from the resource owner. In monolith the // answer is free; under a transport it is a real round trip. The number is therefore // the same in both modes and is "one per storage definition", which is what makes it // assertable today instead of a counter that can only ever read zero. The counter an // operator greps is PipeStats' map-persistent-roundtrips (mpr); this member is the // applier-side observable a unit case reads without a stats window. Uint64 MapPersistentRoundtrips = 0; // ---- P4a's WORKING state. All of it is per context and all of it is cleared by // MGPipeApplierReset, EXCEPT the serials, which only ever advance - a counter that // restarts walks back through values already stamped into a twin that outlived the // switch, and P4a deletes the identity patches that used to close that hole. // WHICH FRAMEBUFFER IS BOUND TO EACH BINDING, and that is ALL this pair is since // ID-19(b): the record itself lives in FramebufferRecords above, keyed by the handle. // Indexed by MGPipeFramebufferTarget::Draw / ::Read. kMGPipeNullHandle means "nothing // described this binding yet", which is what a make-current leaves behind. // // set_framebuffer_state Draw / Read / Both writes the RECORD at state.Fbo's slot AND // sets the handle(s) here; Named (kMGPipeFramebufferTargetNamed) writes the record and // touches nothing here at all - that is the whole of the fourth target's meaning. Array BoundFramebuffer{}; // ONE SERIAL FOR THE FAMILY, and it moves on EVERY write - a Named record's included, // because a twin memoising "the framebuffer state I have seen" has to hear about a // named framebuffer's attachments exactly as it hears about a bound one's. It is the // number that retires the four g_fboSynced* arrays and the twin's {slot version, object // version, backend id generation} triple. Uint64 FramebufferSerial = 0; // Every FramebufferRecordFor() that found a record at the slot whose GENERATION had // moved on. It is NOT RefusedObjectCalls: this is a READ by the server's own sync path // and not a call this applier refused, and set_framebuffer_state's counter contract // (below) is that no framebuffer call ever moves that one. A non-zero value here is a // seam defect - an emitter minted a successor for a recycled slot and never described // it, or handed out a handle it had already retired - so it is counted AND logged, and // a unit case reads it in every build for the reason the other counters exist. // // `mutable` because the three accessors below are const: package E holds the applier // through a `const auto&` and must keep doing so. mutable Uint64 StaleFramebufferRecordLookups = 0; // The three kVarTail unit sets, as received. NO STAGE DIMENSION: 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. // // THE VAR-TAIL WINDOW IS THE BOUND AND ENTRIES OUTSIDE IT ARE NOT CLEARED - the // record is "the last set as received", exactly as set_vertex_buffers is, and // Start + Count above the bound is Fatal{ProtocolCorruption}. Array BoundSamplerViews{}; Uint32 SamplerViewStart = 0; Uint32 SamplerViewCount = 0; Uint64 SamplerViewsSerial = 0; Array BoundSamplerStates{}; Uint32 SamplerStateStart = 0; Uint32 SamplerStateCount = 0; Uint64 SamplerStatesSerial = 0; Array BoundShaderImages{}; Uint32 ShaderImageStart = 0; Uint32 ShaderImageCount = 0; Uint64 ShaderImagesSerial = 0; // set_draw_program / set_dispatch_program are two calls because the frontend has two // joins and two PipeInputs slots; bind_shader_state is the third, and a null handle is // legal in all three and means "nothing bound". MGPipeHandle DrawProgram = kMGPipeNullHandle; MGPipeHandle DispatchProgram = kMGPipeNullHandle; MGPipeHandle BoundShaderCso = kMGPipeNullHandle; Uint64 ProgramBindingSerial = 0; // ---- 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. const MGPFramebufferState* FramebufferRecordFor(MGPipeHandle fbo) const; const MGPFramebufferState* DrawFramebuffer() const; const MGPFramebufferState* ReadFramebuffer() const; }; // The monolith's single applier. Under split there is one per served context. MGPipeApplierState& MGPipeApplier(); // 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. void MGPipeApplierReset(); // THE OTHER SCOPE: the served context is going away and its applier with it, so the object // records go too. Under split that is one applier per served context and this is its // teardown. In the monolith there is ONE applier behind every context, so this is // deliberately wired to NOTHING: a record is cleared by its object's own death signal // (resource_destroy, delete_vertex_elements) and the process's exit clears the rest. // Calling it on one context's destruction in a monolith would drop every other context's // records, which is the C1 hole in its other direction. void MGPipeApplierReleaseObjectRecords(); // --------------------------------------------------------------------------------- // The seven apply entry points (ARCHITECTURE.md 5.3, ROADMAP.md P2) // --------------------------------------------------------------------------------- // create_render_state. `chunkBytes` is the pipeline chunks named by desc.ChunkMask, // concatenated in ascending chunk order (MGPipeGatherPipelineChunks' output). A // brand-new CSO must name every chunk; an incremental one starts from desc.BaseCso. void MGPipeApplyCreateRenderState(const MGPRenderStateDesc& desc, const void* chunkBytes); // bind_render_state: 12 bytes, no blob, no hashing. Scatters the record's seven pipeline // chunks into the working block and publishes both versions. void MGPipeApplyBindRenderState(const MGPBindRenderState& bind); // delete_render_state: frees the slot. The client's allocator owns the Gen bump on // REUSE; the record only stops being live here. CsoCache's LRU eviction emits this. void MGPipeApplyDeleteRenderState(const MGPHandleOnly& handle); // set_dynamic_state: the dynamic chunks named by dyn.ChunkMask, concatenated ascending. void MGPipeApplySetDynamicState(const MGPDynamicState& dyn, const void* chunkBytes); // set_pixel_pack_state. PACK only, deliberately (MGPipeTypes.h, ARCHITECTURE.md 4.6 D5). void MGPipeApplySetPixelPackState(const MGPPixelPackState& pack); // set_patch_state. The trio also travels in pipeline chunk P0, and the applier asserts // under verify that the two carriers agree - the redundancy is a trip wire, not waste. void MGPipeApplySetPatchState(const MGPPatchState& patch); // set_vertex_attrib_defaults: `tail` is hdr.Count MGPAttribValues for the attributes // named by hdr.Mask, in ascending location order. void MGPipeApplySetVertexAttribDefaults(const MGPVertexAttribDefaults& hdr, const MGPAttribValue* tail); // set_residual_value_state: what has no call of its own. Since P2 that is one Uint64 of // capability bits, and every one of them is ALSO answerable from the assembled working // block - which is the point. A disagreement is Fatal{PipeResidualDiverged, ""}. void MGPipeApplySetResidualValueState(const ResidualValueBlock& block); // --------------------------------------------------------------------------------- // P3a: the nine resource entry points (D-A1, D-A2) // --------------------------------------------------------------------------------- // // These are the ONE exception to push-at-validate: they are applied at the GL call that // causes them, from the same dispatchers that call the old op table today, because that // is already where those hooks run. Nothing about buffers moves to validate time here. // // The `bytes` companion of the three content-carrying calls is the client's shadow base, // never a copy (see MGPipeResourceOps). A null is a real answer wherever the payload says // the content is undefined. // // AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB. The signatures are what the client, // the backend and the gates compile against, and the records above are what they write // into; the bodies land in the two commits that follow this one on the same branch. // The scope of one resource_respecify, and it is an APPLIER-SIDE ARGUMENT and not a wire // record: it is not in PipeFields.def, it crosses no payload, and the transport reads the // scope off the call it is replaying rather than off a field. The two members mirror // MGPipeResourceRecord::PendingUpload's key exactly, which is the only thing the applier // does with them - so UploadTarget is MGPSubData::Target VERBATIM, the whole packed field // (ID-12: low byte = MGPipeResourceTarget, high byte = the cube-face upload target), the // same value the emission of that level put in the record. A per-face respecify therefore // drops the face it redefines and leaves the other five standing, and a caller that packs // the pair differently here than it packs it there simply matches nothing. struct MGPRespecifiedLevel { Uint16 UploadTarget = 0; Uint16 Level = 0; }; // THE THREE ACCEPTANCE RETURNS, AND WHY ALL THREE (ID-18 M3, clientfb review M3). D-D5 // step 1 says the client clears a level's dirty flags "for the levels whose record the // applier ACCEPTED", and the emitter cannot answer that for itself: an `if constexpr` that // discarded the call, a dead or stale handle (a counted no-op) and a corrupt record (a Fatal // that deliberately moves no counter) are all invisible from the call site, so a client that // clears on the strength of having EMITTED drops those texels for good. resource_subdata // returns it, and so must the two calls that DEFINE the storage a subsequent upload lands // in - a create or a respecify the applier refused leaves no record for the upload to // accumulate onto, and B's own bookkeeping (its per-entry descriptor dedupe, its drain list) // must not advance past a call that never landed. // // ALL THREE ARE SOURCE-COMPATIBLE: a Bool return is ignorable, P3a's call sites in // MG_Impl/Pipe/PipeFill.cpp discard it, and gen_pipe.py never parses this header - the wire // path calls no MGPipeApply* at all (wire review W1), so PipeCalls.def and // MobileGL/MG_Pipe/generated do not move. // resource_create: mints the record and marks the slot Live. Emitted from the buffer // object's CONSTRUCTOR, so a resource exists before anything can name it; storage is // defined lazily by the first respecify and a backend tolerates a resource with none. // // Returns true when the record was minted. False for the three refusals: the reserved slot // 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 the PREVENTION half of the texture-remint // stall class. 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. 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. Bool MGPipeApplyResourceSubData(const MGPSubData& record, const void* bytes, const MGPSubRegion* regions = nullptr); // buffer_subdata_resident: same shape; `bytes` is the application's staging store and is // valid for the duration of the call only. The op-table entry may be null. void MGPipeApplyBufferSubDataResident(const MGPSubData& record, const void* bytes); // resource_flush_range: record.AccessFlags are the application's REAL mapping flags, not // a normalised subset - the backend reads them per call to choose its upload shape. void MGPipeApplyResourceFlushRange(const MGPFlushRange& record, const void* bytes); // resource_readback: whole-buffer by contract. The answer travels back through the // reverse channel, and the writeback happens BEFORE the mutation epoch bumps, never // after - the ordering is a correctness rule, not a preference. void MGPipeApplyResourceReadback(const MGPReadback& record); // resource_destroy: clears Live and drops the record, then the backend frees its twin. // The CLIENT frees the slot afterwards, in that order, because the allocator forgets the // lifetime id on free and a notice resolved twice finds nothing the second time. void MGPipeApplyResourceDestroy(const MGPHandleOnly& handle); // map_persistent: bumps MapPersistentRoundtrips and asks the backend. Returns the // coherent host pointer the resource owner donated, or null for a DECLINE - which is a // real answer and the reason the call is kOptional as well as kReplySlot. `seedBytes` is // the shadow, still live at this point, for the backends that seed the new store from it. void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes); // unmap_persistent: the donation ends. Never emitted by P3a's own paths; the call exists // so the pair is complete and the transport has both halves. void MGPipeApplyUnmapPersistent(const MGPHandleOnly& handle); // --------------------------------------------------------------------------------- // P3a: the five vertex-input entry points (D-G, D-H, D-I) // --------------------------------------------------------------------------------- // create_vertex_elements. `blobBytes` is MGPVertexAttribWire[desc.AttributeCount] // immediately followed by MGPVertexBindingPointWire[desc.BindingPointCount], both in // ascending index order. The applier REFUSES a record whose declared counts do not // describe its own blob, and both counts are bounded by kMGPipeMaxVertexAttribs. // Re-issuing on the same handle is how a configuration change travels; it bumps // ContentSerial and does not rebind. void MGPipeApplyCreateVertexElements(const MGPVertexElements& desc, const void* blobBytes); // bind_vertex_elements. The null handle is legal and means "no VAO bound". void MGPipeApplyBindVertexElements(const MGPHandleOnly& handle); // delete_vertex_elements: emitted from ONE place, the frontend object's death notice. void MGPipeApplyDeleteVertexElements(const MGPHandleOnly& handle); // set_vertex_buffers: `tail` is hdr.Count MGPVertexBuffer entries starting at hdr.Start. // hdr.BaseInstance is the DRAW's raw base instance and is stored, unresolved, in // VertexFetchBaseInstance - the decision whether to emulate the fetch shift is the // backend's, for the reason written beside that member. Bumps VertexBuffersSerial. void MGPipeApplySetVertexBuffers(const MGPVertexBuffers& hdr, const MGPVertexBuffer* tail); // set_index_buffer: an independent call, NOT a subset of the vertex-elements // configuration. Bumps IndexBufferSerial. void MGPipeApplySetIndexBuffer(const MGPIndexBuffer& record); // --------------------------------------------------------------------------------- // P4a: the fifteen object and working-state entry points (D-A1, D-B1, D-J1) // --------------------------------------------------------------------------------- // // 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. // // AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB, exactly as P3a's nine were: the // signatures are what the client, the backend and the gates compile against and the // records above are what they write into; the bodies land in the three commits that // follow this one on the same branch. // 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. // kMGPipeFramebufferTargetNamed 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). void MGPipeApplySetFramebufferState(const MGPFramebufferState& state); // create_sampler_state. `parameters` is the client's canonical SamplerParameters copy, // beside the record for the one Blob rule's reason; the applier stores it by value. void MGPipeApplyCreateSamplerState(const MGPSamplerDesc& desc, const SamplerParameters* parameters); // delete_sampler_state: emitted by the CSO cache's LRU eviction and by the frontend // sampler object's death helper. Clears Live and drops the record; the client frees the // slot afterwards. void MGPipeApplyDeleteSamplerState(const MGPHandleOnly& handle); // create_sampler_view. Re-issued on the SAME handle whenever the view restrictions move, // which is legal because Gen increments only on slot reuse and never on a respecify. void MGPipeApplyCreateSamplerView(const MGPSamplerView& view); void MGPipeApplyDeleteSamplerView(const MGPHandleOnly& handle); // set_texture_params: addressed by RESOURCE and independent of any binding, which is what // lets a texture that is only an attachment, only an image-unit binding or only a // glCopyImageSubData endpoint carry its parameters at all. params.BuiltinSampler may never // be the null handle - every ITextureObject owns a sampler object - so a null is // Fatal{ProtocolCorruption} rather than "no sampler". void MGPipeApplySetTextureParams(const MGPTextureParams& params); // set_sampler_views / bind_sampler_states / set_shader_images: `tail` is hdr.Count entries // starting at hdr.Start, and hdr.Start + hdr.Count above the unit bound is // Fatal{ProtocolCorruption}. Entries outside the declared window are NOT cleared. void MGPipeApplySetSamplerViews(const MGPSamplerViews& hdr, const MGPBoundView* tail); void MGPipeApplyBindSamplerStates(const MGPSamplerStates& hdr, const MGPipeHandle* tail); void MGPipeApplySetShaderImages(const MGPShaderImages& hdr, const MGPImageView* tail); // 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. void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc, const MG_State::GLState::LinkArtifacts* link, const MG_State::GLState::SpirvArtifacts* spirv); void MGPipeApplyBindShaderState(const MGPHandleOnly& handle); void MGPipeApplyDeleteShaderState(const MGPHandleOnly& handle); void MGPipeApplySetDrawProgram(const MGPHandleOnly& handle); void MGPipeApplySetDispatchProgram(const MGPHandleOnly& handle); // set_global_constants: the DEFAULT UNIFORM BLOCK only. Keyed (ShaderCso, Version) and // emitted at most once per program per frame; `bytes` is MapUBO()'s image, GetUBOSize() // long, handed over as a companion pointer with Blob.Size 0. record.Version is // GetUBOContentVersion() and may never be ~0u, which is the backends' "never uploaded" // sentinel. void MGPipeApplySetGlobalConstants(const MGPGlobalConstants& record, const void* bytes); // --------------------------------------------------------------------------------- // P4a: the named, greppable unmigrated emulations (D-M) // --------------------------------------------------------------------------------- // // ROADMAP.md's P4a row ends "emulation 在 split 下显式 Fatal 直到 P8". In monolith the // code paths keep running exactly as today - the Fatal is a SPLIT-only arm - so this costs // P4a a named call site per unmigrated emulation and nothing else. P5/P8 give it teeth: a // split server that reaches one of these has no client address space to read and must // abort loudly rather than degrade silently. // // Monolith body: (void)name;. The list of names is pinned by // PipeCatalogueTest.EveryUnmigratedEmulationIsNamedOnce and the call count is grepped by // the purity gate, so a site that quietly disappears is a red gate rather than a surprise // at P8. void MGPipeUnmigratedEmulation(const char* name); // --------------------------------------------------------------------------------- // The derivation step (ARCHITECTURE.md 5.3, P2 brief D5) // --------------------------------------------------------------------------------- // Recomputes every PipeInputs field that is a pure function of the working // RenderStateParameters, instead of pulling it out of GLContext a second time. // // The oracle is the one P1 built: MOBILEGL_PIPE_VERIFY's compare-at-read re-reads each of // these from the live context at every backend read, so a transcription error is caught // on the first draw that reads it - on the retrace and integration-verify LANES, which is // where the comparator arms (MG_Config::Features.PipeVerify). A unit-test process never // runs the config loader, so the unit oracle is a different one: // RenderStateSpansTest.DerivationMatchesTheFrontendGetters walks every setter and // compares all 29 derived values against the frontend getters they were transcribed from. void MGPipeDeriveRenderStateFields(PipeInputs& inputs); // The same derivation, SCOPED to the chunks a scatter actually moved (bit i is global // chunk i - MGPipeGlobalChunkBitsOf{Pipeline,Dynamic}Mask widens a wire mask to it). This // is what the applier calls, and it is why a per-frame glViewport - the D8 case whose // whole point is that it sends dynamic chunk D0 alone - does not pay for the 8-wide blend // loop, the 16-wide depth-range loop or the 35-arm capability switch. Every guard's chunk // set is computed from the boundary table with MGPipeRenderStateChunkBitsCovering, so a // boundary move cannot leave one stale, and // RenderStateSpansTest.IncrementalChunksKeepEveryDerivedFieldInStep drives the scoped // path against the frontend getters family by family. void MGPipeDeriveRenderStateFieldsForChunks(PipeInputs& inputs, Uint32 globalChunkBits); } // namespace MobileGL::MG_Pipe