Files
MobileGL/MobileGL/MG_Pipe/PipeApply.h
T

1070 lines
73 KiB
C++

// 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 <Includes.h>
#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<Uint8, kMGPipePipelineChunkBytes> 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<Uint8>(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<Uint8>(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<Uint8>(MGPipeFramebufferTarget::Draw) == 0 &&
static_cast<Uint8>(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<MGPSubRegion> Regions;
};
Vector<PendingUpload> 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<Uint8> 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<MGPVertexAttribWire, kMGPipeMaxVertexAttribs> Attributes{};
Array<MGPVertexBindingPointWire, kMGPipeMaxVertexAttribs> 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<MGPipeRenderStateCsoRecord> 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<MGPipeResourceRecord> Resources;
Vector<MGPipeVertexElementsRecord> 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<MGPipeResourceRecord> TextureResources;
Vector<MGPipeResourceRecord> RenderbufferResources;
Vector<MGPipeSamplerCsoRecord> SamplerCsos;
Vector<MGPipeSamplerViewRecord> SamplerViewCsos;
Vector<MGPipeShaderCsoRecord> 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<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).
Vector<MGPipeFramebufferRecord> 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<MGPVertexBuffer, kMGPipeMaxVertexAttribs> 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<MGPipeHandle, kMGPipeFramebufferBindingCount> 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<MGPBoundView, kMGPipeMaxTextureUnits> BoundSamplerViews{};
Uint32 SamplerViewStart = 0;
Uint32 SamplerViewCount = 0;
Uint64 SamplerViewsSerial = 0;
Array<MGPipeHandle, kMGPipeMaxTextureUnits> BoundSamplerStates{};
Uint32 SamplerStateStart = 0;
Uint32 SamplerStateCount = 0;
Uint64 SamplerStatesSerial = 0;
Array<MGPImageView, kMGPipeMaxImageUnits> 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, "<Cap>"}.
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