mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-15 14:03:23 +09:00
kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp) and kMGPipeP5eClientWaitRuleLanded (MG_Pipe/PipeApply.h) both to true. They are ONE switch with two spellings and the integration commit throws both. THIS BRANCH IS NOT MERGEABLE AS IT STANDS and is parked so the defect it exposes can be worked on against a reproducible head. Measured on this commit: build rc=0 · unit 2254/2254 · gens rc=0 · flavours rc=0 (pull 915 TUs, push 929 TUs) integration-split 112 / 181 (69 failed) integration-gpu 1290 / 1359 (69 failed) and the failures are FLAKY: 71 failed at -j 4, 68 at -j 1, and a single entry run alone passes and then aborts on a repeat. A census over all 181 entries under each entry's own command and ENVIRONMENT gives 70 red on 17 distinct <field>@<verb> pairs, over just FOUR fields: 27 GetRenderStateParametersVersion 25 GetTextureContextId 16 GetBufferBindingSlot 2 IsCapabilityEnabled spread across eleven verbs - Clear, DrawArrays, DrawElements, DrawElementsBaseVertex, DrawElementsIndirect, DrawElementsInstancedBaseVertex, EndTransformFeedback, GetIntegeri_v, GetSyncStatus, MultiDrawElementsBaseVertex, ReadPixels. Several of those verbs are BARRIERED, which is what makes this interesting: 16 of the 70 abort on the UNBARRIERED arm (why=UNBARRIERED) and the other 54 are the plain unfresh-read abort with no [BARRIER-PULLED] bracket at all, i.e. a RECORD-SUPPLIED field that the freshness stamp says was never filled. One field at one verb, consistently, would be a missing migration. Four fields scattered over eleven verbs, flaky run to run, including barriered ones, is a RACE - and the obvious candidate is that gPipeInputs is shared mutable state whose mutual exclusion WAS the lockstep. The client still fills it for barriered verbs while the apply thread is draining earlier unbarriered records, so the client's fill and the server's verb-boundary stamp can now overlap in time. That is consistent with P5d's own research conclusion, recorded in the roadmap, that retiring this barrier needs gPipeInputs VERSIONING or double-buffering, deferred to P11 after P3b/P4b's handle-keyed twin tables. The strict lane being hard green (181/181, zero Fatal) is necessary and NOT sufficient, and this commit is the proof: strict runs under lockstep, where ApplyOne stamps every record barriered, so it cannot see anything that only happens when the client stops waiting. The lane measures remaining DEBT; it does not measure whether run-ahead is correct.
1468 lines
101 KiB
C++
1468 lines
101 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"
|
|
// P5e (MG_Remote/CONTRACT-P5E.md §2.1): MGPipeBarriered takes an MGPWireOp and reads the
|
|
// generated wait-class and call-class tables, all of which live in MGPipe.h. It is a sibling
|
|
// header in the same layer - MG_Pipe includes MG_Pipe - so the closure does not move.
|
|
#include "MGPipe.h"
|
|
|
|
// The in-process applier: the SERVER half of the calls P2 emits. Under split this file is
|
|
// MG_Remote/Server/PipeApplier (ARCHITECTURE.md 8.3); in the monolith it writes
|
|
// MG_Backend/MGPipe/PipeInputs' gPipeInputs directly, so a call and its effect are one
|
|
// function call apart and nothing is serialised.
|
|
//
|
|
// THE SERVER'S PER-CONTEXT WORKING BLOCK *IS* PipeInputs::m_renderState. bind_render_state
|
|
// and set_dynamic_state scatter their chunks straight into it, which is why DirectGLES'
|
|
// SyncRenderState is not one line changed (ROADMAP.md P2, G5): the block Espryt binds by
|
|
// const reference is the assembled block. It is also what makes the MOBILEGL_PIPE_VERIFY
|
|
// comparator a real oracle instead of a tautology - the compare-at-read now proves
|
|
// "assembled == live", field by field, at every backend read.
|
|
//
|
|
// This header FORWARD-DECLARES PipeInputs rather than including it: the applier's callers
|
|
// (MG_Impl/Pipe) already have it, and MG_Pipe sits below MG_Backend.
|
|
//
|
|
// Compiled only under MOBILEGL_PIPE_PUSH (CMakeLists.txt), so the pull build gains no symbol.
|
|
// P4a: create_shader_state carries the reflection ARCHIVE, and in monolith the archive does
|
|
// not travel - the two structs ride beside the record through the entry point's companion
|
|
// pointers, exactly as P3a's `const void* initialBytes` does (D-H3, the one Blob rule). So
|
|
// this header needs their NAMES and never their definitions; the forward declaration is the
|
|
// whole coupling and the closure gate is what keeps it one. The verify build is the only
|
|
// place the codec runs, and it runs from PipeApply.cpp.
|
|
// P5e (pg) ADDS A THIRD NAME AND KEEPS THE RULE: ProgramArchive is what a SPLIT create carries
|
|
// - the record's OWN copy of both structs plus the stage of each module - and the record holds
|
|
// it through a SharedPtr, which needs no definition either (the deleter is captured where the
|
|
// archive is constructed, in the decoder's TU). So the coupling is still three names and no
|
|
// closure change, and MG_Backend, which does need the definition, includes
|
|
// ProgramArtifactsCodec.h for it.
|
|
namespace MobileGL::MG_State::GLState {
|
|
struct LinkArtifacts;
|
|
struct SpirvArtifacts;
|
|
struct ProgramArchive;
|
|
} // namespace MobileGL::MG_State::GLState
|
|
|
|
namespace MobileGL::MG_Pipe {
|
|
struct PipeInputs;
|
|
// P5c (tx): MGPipeResourceOps::TextureRespecify names it; the definition is beside the
|
|
// applier entry point that produces one (below, with MGPipeApplyResourceRespecify).
|
|
struct MGPRespecifiedLevel;
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// 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);
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// P5c (tx): the TEXTURE half of the resource family, appended so every positional
|
|
// initialiser of the nine P3a members keeps its meaning. TextureSubData is called
|
|
// from ApplyTextureUpload AFTER the gate, the accumulation and the serial, while
|
|
// `bytes` still names the staged run (SEG_STAGE retires when the record does, so an
|
|
// adoption anywhere later would read dead bytes - rule C); the backend copies the
|
|
// run into the server's staged-texture store (MG_Remote/Server/StagedTextureStore.h)
|
|
// and does nothing in monolith. TextureRespecify is the defined-ness/drop channel:
|
|
// it rides MGPipeApplyResourceRespecify's own scope rules (a named level redefines
|
|
// that level, a whole-resource respecify drops them all, a metadata update is not
|
|
// delivered). TextureDestroy is resource_destroy's texture arm - the applier hands
|
|
// only a buffer to Destroy, and a store keyed by the handle needs the death to drop
|
|
// its key. All three may be null together: a backend that has not adopted the staged
|
|
// shadow leaves them null and keeps the pre-tx shape.
|
|
void (*TextureSubData)(MGPipeHandle res, const MGPSubData& record, const void* bytes,
|
|
const MGPSubRegion* regions);
|
|
void (*TextureRespecify)(MGPipeHandle res, const MGPResourceDesc& desc,
|
|
const MGPRespecifiedLevel* level);
|
|
void (*TextureDestroy)(MGPipeHandle res);
|
|
#endif
|
|
};
|
|
|
|
// 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 is the contract's MGPipeFramebufferTarget::Named (c0e):
|
|
// "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, which is how
|
|
// the DSA entry points - BlitNamedFramebuffer and the four ClearNamedFramebuffer* - hand the
|
|
// server a record for a framebuffer bound to neither binding (esprytobj review C-1, ID-19).
|
|
|
|
// 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;
|
|
// "DOES THIS RESOURCE HAVE A LIVE HOST WRITER RIGHT NOW?"
|
|
//
|
|
// False and written by nobody through P3a and P4a; P5 (b1) is the phase it was waiting
|
|
// for and gives it its producer, with zero new record kinds exactly as planned: the
|
|
// bit rides MGPSubData::HasLiveHostWrites - a byte out of that payload's existing pad -
|
|
// and ApplyBufferWrite assigns it here. In a MONOLITH build nothing sets it and the
|
|
// MOBILEGL_PIPE_VERIFY wire (PinNoLiveHostWrites) still refuses a producer, so the pin
|
|
// survives the phase it was written for instead of being deleted by it.
|
|
//
|
|
// It is what IsBufferDrawCleanByHandle asks under split INSTEAD of the frontend
|
|
// object's IsMapped(), because a spawned server has no frontend object to ask. Getting
|
|
// that substitution wrong once already cost a silent regression - an emulated
|
|
// persistent map read draw-clean for ever - which MG_Test/SanityTest.cpp's
|
|
// DirectGLESBufferDrawProbe pair now pins from both sides.
|
|
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.
|
|
// P5e (MG_Remote/CONTRACT-P5E.md §1): one entry of set_program_bindings' third tail as the
|
|
// RECORD owns it. The wire carries the block name as an MGHostSpan into SEG_STAGE, which
|
|
// retires with the record that named it (rule C), so the applier copies the bytes here
|
|
// rather than keeping the span: a rebuild happens at the next bind, which may be a frame
|
|
// later, and by then the staged run is somebody else's.
|
|
struct MGPipeProgramStorageOverride {
|
|
String Name;
|
|
Int32 Binding = 0;
|
|
};
|
|
|
|
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;
|
|
|
|
// ---- P5e's server-owned archive (CONTRACT-P5E.md §5.5, gap G-A) --------------------
|
|
//
|
|
// FILLED BY PACKAGE pg, AND ONLY UNDER A TRANSPORT. This is the whole of rule F for
|
|
// this family: before P5e the record stored the DESCRIPTOR and the artefacts rode
|
|
// beside it as two companion pointers into the frontend's own ProgramObject, so every
|
|
// reflection question the program twin asked - forty accessors in SyncToBackend alone -
|
|
// was a read of client memory that `ProgramObject::Link()` REPLACES IN PLACE. Under
|
|
// run-ahead the client is already several records past the create, so those reads are
|
|
// torn or stale by construction and no amount of care at the read site can fix it.
|
|
//
|
|
// The client encodes the archive once per link (ProgramEmit.h -> SEG_STAGE), the
|
|
// decoder frames and deserialises it, and the record ADOPTS it here - a SharedPtr and
|
|
// not a Vector, because a program-pipeline composite and its stage programs are three
|
|
// records over one link's artefacts, and because the twin holds the pointer across the
|
|
// build it is running. It retires with the record, which is rule C: the staged run the
|
|
// bytes arrived in belongs to somebody else by the next frame.
|
|
//
|
|
// NULL UNDER MONOLITH, and that is the arm selection and not an omission (ruling 1):
|
|
// the push-monolith build keeps its frontend arms token for token, so its twin reads
|
|
// the frontend's archive as it always has and this pointer is never consulted.
|
|
SharedPtr<const MG_State::GLState::ProgramArchive> Archive;
|
|
|
|
// ---- P5e's set_program_bindings tails (CONTRACT-P5E.md §1, §5.5) -------------------
|
|
//
|
|
// DECLARED BY c0e, FILLED BY PACKAGE pg. THE THREE POST-LINK MUTABLE REFLECTION FIELDS
|
|
// the archive cannot answer: glUniformBlockBinding, glUniform1i on a sampler uniform and
|
|
// glShaderStorageBlockBinding all move a binding INSIDE the frontend's LinkArtifacts
|
|
// after the link, so a twin that answered a draw from the archive alone would bind the
|
|
// uniform blocks the program was LINKED with rather than the ones it is BOUND with.
|
|
//
|
|
// A RE-ISSUED create_shader_state CLEARS ALL OF THEM, for the reason it clears
|
|
// GlobalConstants: a relink replaces the archive these index into, so a surviving tail
|
|
// would name block indices and uniform locations of a program that no longer exists.
|
|
// The client re-emits set_program_bindings before the create that describes the relink.
|
|
Vector<Int32> BlockBindings; // dense, GL uniform-block index order
|
|
Vector<MGPProgramSamplerUnit> SamplerUnits; // sparse, ascending Location
|
|
Vector<MGPipeProgramStorageOverride> StorageOverrides; // name-keyed by design
|
|
// The client's commutative hash of the override MAP, so the twin's "must I rebuild the
|
|
// storage-block bindings" clause is one Uint64 compare and the name tail is touched only
|
|
// on a rebuild. 0 is "no overrides", which is what an untouched program has.
|
|
Uint64 Signature = 0;
|
|
// Server-owned MGGen, ++ on every applied set_program_bindings for this record. It is
|
|
// what the program twin's clean condition and both texture-unit memos key on (ruling 6),
|
|
// so it obeys VertexBuffersSerial's rule: ADVANCED, never returned to a value it has
|
|
// already handed out.
|
|
Uint64 BindingsSerial = 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;
|
|
|
|
// P4a's BELT (ID-39): every call in one of the four families P4a migrates that this
|
|
// applier declined because NO BACKEND HAS REGISTERED MGPipeResourceOps - i.e. because
|
|
// nothing in this process consumes what the record publishes.
|
|
//
|
|
// WHY THE APPLIER ASKS A QUESTION ABOUT THE BACKEND AT ALL, when it is otherwise
|
|
// backend-neutral: acceptance is a CONTRACT WITH THE CLIENT since ID-18 M3. The
|
|
// emitters clear a texture level's dirty flags, advance their descriptor mirrors and
|
|
// latch their suppressors on the answer this applier returns, so an applier that
|
|
// accepts a record nothing will ever read makes the client forget work the legacy pull
|
|
// path still owed - which is exactly how 66 texture-upload-shaped DirectVulkan cases
|
|
// went red on the push build (ID-39). The client's own gate
|
|
// (MG_Impl/Pipe/PipeFill.cpp's FamilyIsLive) stops the emission upstream; this is the
|
|
// belt under it, so a record that reaches here by any other route - GL_Framebuffer.cpp's
|
|
// PipePublishFramebufferByName calls its emitter directly, without passing PipeFill -
|
|
// is declined rather than accepted.
|
|
//
|
|
// IT IS NOT A DEFECT COUNTER, WHICH IS WHY IT IS SILENT. RefusedResourceCalls,
|
|
// RefusedVertexInputCalls and RefusedObjectCalls each mean "a record named something
|
|
// this applier should have had"; a non-zero value there is a seam defect. A non-zero
|
|
// value HERE is the designed steady state of a backend with no P4a twins, so logging it
|
|
// would put an ERROR line in every ordinary Magma run. The number is the observable.
|
|
//
|
|
// THE DEATH PATHS ARE DELIBERATELY NOT ON THIS LIST. resource_destroy,
|
|
// delete_sampler_state, delete_sampler_view and delete_shader_state are idempotent
|
|
// cleanup that must keep working whatever the registration did, and with no consumer
|
|
// there is no record for them to find anyway (they count their own refusal). Per
|
|
// context and cleared by MGPipeApplierReset, like the three above it.
|
|
Uint64 RefusedNoConsumer = 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 (MGPipeFramebufferTarget::Named) 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;
|
|
|
|
// ---- P5e: THE INDEXED BUFFER BINDING POINTS (CONTRACT-P5E.md §5.6, rulings 10/11) ---
|
|
//
|
|
// DECLARED BY c0e, FILLED BY PACKAGE sb. One window per CLASS, because the record is
|
|
// per class - a set_shader_buffers names Uniform, ShaderStorage or AtomicCounter and
|
|
// describes THAT target's array. XFB does NOT ride this state: its targets are span-
|
|
// scoped and carry a Generation this payload has no field for, so set_stream_output_
|
|
// targets keeps its own row and XFB stays lockstep (§5.7).
|
|
//
|
|
// THE VAR-TAIL WINDOW IS THE BOUND, exactly as it is for the three unit sets above:
|
|
// Start is 0 by contract, Count is the touched high-water mark, and a binding at or
|
|
// above Count means "nothing bound" - which is what the frontend array's default says
|
|
// too, so the server does not have to clear entries outside the window to be right.
|
|
Array<MGPBufferRange, kMGPipeMaxBufferBindingPoints>
|
|
BoundShaderBuffers[kMGPipeShaderBufferClassCount]{};
|
|
Uint32 ShaderBufferStart[kMGPipeShaderBufferClassCount] = {0, 0, 0};
|
|
Uint32 ShaderBufferCount[kMGPipeShaderBufferClassCount] = {0, 0, 0};
|
|
// Which entries of classes 1 and 2 the shader may WRITE through. It is what replaces
|
|
// the backend's own GPU-write marking walk (DirectGLES.cpp's three MarkBufferGpuWritten
|
|
// sites), which sb deletes under a transport because the client already owns the
|
|
// GPU-write set and the server's walk is over client memory.
|
|
//
|
|
// THREE WORDS PER CLASS, NOT ONE (P5e, ID-104): the window is 84 points and c0e's
|
|
// single Uint32 could describe only the first 32 of them, so a storage buffer bound at
|
|
// point 32 or above read as read-only here while the record said nothing was wrong.
|
|
// Always through MGPipeShaderBufferMaskHas / ...Set (MGPipeTypes.h), which is the same
|
|
// arithmetic the payload's own mask goes through - one table, both sides.
|
|
Uint32 ShaderBufferWritableMask[kMGPipeShaderBufferClassCount]
|
|
[kMGPipeShaderBufferWritableMaskWords] = {};
|
|
// ONE serial for all three classes, ++ on every applied record and ADVANCED (never
|
|
// zeroed) by MGPipeApplierReset, for VertexBuffersSerial's reason - and the emitter's
|
|
// latch resets with it, or the first emission after a make-current is suppressed as
|
|
// unchanged.
|
|
Uint64 ShaderBuffersSerial = 0;
|
|
|
|
// ---- P5e: the applied value the barriered predicate's XFB escalation reads ---------
|
|
//
|
|
// MGPContextValues::IsTransformFeedbackActive as set_context_values last delivered it.
|
|
// It is mirrored HERE, beside the applier's other working state, and not only written
|
|
// into gPipeInputs, because MGPipeBarriered must be computable on the SERVER from
|
|
// applier state alone: the record that carries it precedes the verb on the ring, so
|
|
// both roles evaluate the same predicate over the same value (§2.1, escalation (i)).
|
|
Bool IsTransformFeedbackActive = false;
|
|
|
|
// ---- P5c (rv, CONTRACT-P5C.md §5.3): the server-side answer for the three texture
|
|
// SHUTTERS. FieldOwnership.def moves GetSamplingResolutionGeneration /
|
|
// GetTextureBindGeneration / GetTextureContextId to APPLIER_DERIVED - "a shutter, not
|
|
// a value: the server answers from its own Serial", which their rows have said since
|
|
// P5 - and THESE are the serials, read by the PipeInputs accessors under a
|
|
// server-stamped verb instead of the client's residual-fill copy.
|
|
//
|
|
// TextureShutterSerial answers the two GENERATIONS. It is an MGGen like its seven
|
|
// siblings above: server-owned, monotone, ++ on every applied record that can move
|
|
// what the frontend's bind / sampling-resolution generations guard - the three unit
|
|
// sets, set_texture_params, a respecify or destroy of any resource, a bind_shader_image
|
|
// verb (the sink bumps it, PipeApplier.cpp) - and ADVANCED, never zeroed, by
|
|
// MGPipeApplierReset / MGPipeApplierReleaseObjectRecords. Over-firing is the safe
|
|
// direction for a memo key: a moved serial costs a re-sync, a stuck one renders stale.
|
|
//
|
|
// ContextSerial answers GetTextureContextId: stable within the served context, moved
|
|
// by every MGPipeApplierReset (a make-current is a fresh server, §5.1), which is all
|
|
// the backends' per-context memo keys need.
|
|
Uint64 TextureShutterSerial = 0;
|
|
Uint64 ContextSerial = 0;
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
// ---- P5c (hd, CONTRACT-P5C §3.2): THE CURRENT VERB'S OWN HANDLES. ----------------
|
|
//
|
|
// Server state, written by ServerVerbSink at the top of a verb's dispatch and read by
|
|
// the backend DURING THAT SAME VERB, so the apply thread resolves the verb's objects
|
|
// from the handles the record carried instead of probing the client's slot allocator
|
|
// for a frontend object's lifetime id (T2). It is per-verb WORKING state, not object
|
|
// state: every writer overwrites the whole set for its verb (null included), so a
|
|
// value is only ever read between its verb's write and that verb's return, and there
|
|
// is nothing to clear at a reset.
|
|
//
|
|
// Blit: both null = the bound-form blit (the bindings answer, as before). A non-null
|
|
// pair names the read/draw framebuffers of a NAMED blit (MGPBlit::ReadFbo/DrawFbo);
|
|
// the backend must then run its named-blit arm and raise VerbBlitNamedConsumed - the
|
|
// sink reads that flag back so a backend with no named arm is a loud decline rather
|
|
// than a silent blit of whatever is bound.
|
|
MGPipeHandle VerbBlitReadFbo = kMGPipeNullHandle;
|
|
MGPipeHandle VerbBlitDrawFbo = kMGPipeNullHandle;
|
|
Bool VerbBlitNamedConsumed = false;
|
|
// copy_framebuffer_to_texture's destination texture (MGPCopyFromFramebuffer::Dst).
|
|
MGPipeHandle VerbCopyTexDst = kMGPipeNullHandle;
|
|
// generate_mipmap's texture (MGPMipPlan::Res).
|
|
MGPipeHandle VerbMipRes = kMGPipeNullHandle;
|
|
// The current indirect draw's command buffer and (for the *Count forms) parameter
|
|
// buffer (MGPDrawIndirect::Buffer / ParameterBuffer).
|
|
MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle;
|
|
MGPipeHandle VerbIndirectParameterBuffer = kMGPipeNullHandle;
|
|
// dispatch_indirect's command buffer (MGPGridInfo::IndirectBuffer).
|
|
MGPipeHandle VerbDispatchIndirectBuffer = kMGPipeNullHandle;
|
|
|
|
// P5e: "is the record being applied barriered?" is NOT a member of this struct. It is
|
|
// thread_local beside MGPipeApplierCurrentRecordIsBarriered() in PipeApply.cpp, because
|
|
// it describes which record THIS THREAD is inside rather than per-context state the
|
|
// applier owns - and because a reader (the allocator guard) that had to find the right
|
|
// context's applier first would be asking a harder question than the one it needs
|
|
// answered. The integrator resolved the two packages' duplicate here (ID-103).
|
|
|
|
|
|
// Every verb's writer calls this FIRST and then sets its own fields, so no field ever
|
|
// outlives the verb that wrote it and a reader can treat non-null as "this verb's".
|
|
void ClearVerbHandles() {
|
|
VerbBlitReadFbo = kMGPipeNullHandle;
|
|
VerbBlitDrawFbo = kMGPipeNullHandle;
|
|
VerbBlitNamedConsumed = false;
|
|
VerbCopyTexDst = kMGPipeNullHandle;
|
|
VerbMipRes = kMGPipeNullHandle;
|
|
VerbIndirectBuffer = kMGPipeNullHandle;
|
|
VerbIndirectParameterBuffer = kMGPipeNullHandle;
|
|
VerbDispatchIndirectBuffer = kMGPipeNullHandle;
|
|
}
|
|
#endif
|
|
|
|
// ---- 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();
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// P5e: THE BARRIERED PREDICATE (MG_Remote/CONTRACT-P5E.md §2.1, ruling 3)
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// ONE FUNCTION, TWO CALLERS, AND THAT IS THE WHOLE POINT. The client calls it at the emit
|
|
// table to decide whether to wait after publishing; the sink calls it in ApplyOne to decide
|
|
// whether the record it is about to apply may read client memory. If the two answers ever
|
|
// differ the failure is silent: the client runs on while the server reads a value that has
|
|
// already moved. So it is a PURE FUNCTION OF WIRE-VISIBLE DATA - the static wait-class
|
|
// column, the record's own payload, and applier state that a record earlier on the ring put
|
|
// there - and both roles evaluate the same three clauses:
|
|
//
|
|
// 1. MGPipeWaitClassFor(op) != kWaitNone the static column (§2.2)
|
|
// 2. kCtxVerb with an open transform-feedback span escalation (i): XFB stays lockstep
|
|
// 3. draw_vbo carrying kDrawClientArrays escalation (ii): client vertex arrays
|
|
//
|
|
// NO OTHER RUNTIME ESCALATION EXISTS. Adding one is an integrator ruling and a row in the
|
|
// contract, not a condition somebody adds at a call site - because a fourth clause that only
|
|
// one side computes is the same silent failure one level down.
|
|
//
|
|
// Escalation (ii) is a REFUSAL under run-ahead (§5.1), so on a run-ahead server it never
|
|
// reaches the sink at all; it is here so the predicate is TOTAL and so the lockstep arm,
|
|
// where such a draw is legal, gets the barrier it needs.
|
|
//
|
|
// `payload` may be null, which answers "the fixed payload is not available here" and makes
|
|
// clause 3 fall through - a caller that has an opcode but no bytes (the emit table before it
|
|
// has built the record) must not be told a draw is unbarriered on that account, so DrawVbo
|
|
// with a null payload is treated as carrying no client arrays and the emit site passes the
|
|
// real MGPDrawInfo it is about to publish.
|
|
Bool MGPipeBarriered(MGPWireOp op, const void* payload, const MGPipeApplierState& st);
|
|
|
|
// Whether the record the apply thread is INSIDE is a barriered one. Server-private: written
|
|
// by the sink's ApplyOne from MGPipeBarriered before it dispatches, read by
|
|
// MGPipeRefuseAllocatorFromApplyThread (§4.4) to decide whether a frontend-keyed registry
|
|
// probe is a named, accounted debt or a role violation.
|
|
//
|
|
// IT ANSWERS `true` BY DEFAULT AND THAT IS THE LANDED LOCKSTEP ANSWER, not a stub: on a
|
|
// server that does not publish kCapRunAheadApply - Magma always, and Espryt until the
|
|
// integration commit - EVERY record is barriered, the client is parked in its own wait for
|
|
// each of them, and P5C's semantics hold unchanged. An asserting body here would abort the
|
|
// monolith and the lockstep split alike, which is why this one seam is implemented rather
|
|
// than declared: the safe answer and the correct answer are the same answer.
|
|
Bool MGPipeApplierCurrentRecordIsBarriered();
|
|
// The one writer, called by the sink at the top of every record's dispatch. Package id owns
|
|
// the call site (it is the line ra rebases onto); c0e owns the storage so both compile.
|
|
void MGPipeApplierSetCurrentRecordBarriered(Bool barriered);
|
|
|
|
// ---- P5e (gl), ID-128: WAS THIS RECORD BARRIERED BY ESCALATION? ----------------------
|
|
//
|
|
// MGPipeBarriered is the static wait class PLUS two payload-derived escalations (ID-83): an
|
|
// open transform-feedback span makes every context verb inside it barriered, and a draw
|
|
// carrying client vertex arrays makes that draw barriered. "Escalated" is therefore exactly
|
|
//
|
|
// MGPipeBarriered(op, payload, st) && MGPipeWaitClassFor(op) == kWaitNone
|
|
//
|
|
// and BOTH HALVES ARE ALREADY COMPUTED on every record - ApplyOne computes the predicate
|
|
// unconditionally for ID-103's reason and the static class is a table - so this is a bit
|
|
// carried beside the existing stamp rather than new plumbing.
|
|
//
|
|
// IT IS A SEPARATE FLAG AND NOT A REFINEMENT OF THE ONE ABOVE, because the two answer
|
|
// different questions. The stamp above answers "is the client parked behind this record",
|
|
// which the allocator guard and the frontend-keyed registry guard ask. This one answers "and
|
|
// was it parked for a reason the STATIC table cannot see", which only the strict knob asks -
|
|
// and it asks because the escalations happen exactly for XFB-active and client-array draws,
|
|
// both of which CONTRACT-P5E puts outside this phase (§5.7 and ID-82). A pull on such a
|
|
// record is a debt some later phase owes, not a migration P5e skipped.
|
|
//
|
|
// FALSE IS THE DEFAULT, which is the opposite of the stamp's default and deliberately so:
|
|
// "not escalated" is the answer that admits nothing, so a reader that runs before any writer
|
|
// is strict rather than lax.
|
|
Bool MGPipeApplierCurrentRecordIsBarrieredByEscalation();
|
|
void MGPipeApplierSetCurrentRecordBarrieredByEscalation(Bool escalated);
|
|
|
|
// THE CLIENT'S HALF OF THE WAIT RULE IS NOT LANDED YET, AND THIS CONSTANT IS THAT FACT.
|
|
// MGPipeBarriered above describes what the client WILL do; until package ra changes
|
|
// EmitAndWaitTails the client still blocks after every record, so the answer the server
|
|
// must stamp is `true` whatever the wire says - the two named exemption scopes (§4.4) are
|
|
// legal exactly while the client is parked, and it is parked behind all of them today.
|
|
// IT FLIPS WITH kMGPipeP5eRunAheadReady, IN THE INTEGRATION COMMIT, AND NOT WHEN ra LANDS
|
|
// (ra's amendment to ID-103's wording, which said "the same commit that lands the wait
|
|
// rule"). The argument ID-103 makes is about whether the CLIENT is parked, not about
|
|
// whether ra's code exists: RunAheadArmed() is a conjunction whose third term is the
|
|
// server's caps bit, so between ra landing and the caps bit being published the client
|
|
// still blocks after every record - and stamping `false` there would withdraw §4.4's
|
|
// exemptions from probes that client's own wait still makes safe, which is exactly the
|
|
// "refusal with no defect behind it" ID-103 refused. The two constants are therefore ONE
|
|
// switch with two spellings, and the integration commit throws both.
|
|
//
|
|
// Computing the predicate anyway (PipeApplier::ApplyOne) is deliberate: it keeps the
|
|
// function exercised on every record for the whole phase rather than first run on the day
|
|
// it starts deciding.
|
|
inline constexpr Bool kMGPipeP5eClientWaitRuleLanded = true;
|
|
|
|
// P5c (rv): the two serials the PipeInputs texture-shutter accessors answer with under a
|
|
// server-stamped verb (FieldOwnership.def, APPLIER_DERIVED). Free functions rather than
|
|
// member reads so PipeInputs.h needs this header's DECLARATIONS only... and because the
|
|
// bump rule - advance, never zero - is stated once, beside the state.
|
|
Uint64 MGPipeApplierTextureShutterSerial();
|
|
Uint64 MGPipeApplierContextSerial();
|
|
// The one writer-side helper: every applier entry point that can move what the frontend's
|
|
// texture bind / sampling-resolution generations guard bumps the shutter serial through
|
|
// this, so the bump rule lives in exactly one place.
|
|
void MGPipeApplierNoteTextureStateMoved();
|
|
|
|
|
|
// A MAKE-CURRENT, NOT A TEARDOWN - and the distinction is the whole of this function's
|
|
// contract. It runs on every change of the current GLContext (MGPipeTracker::Update resets
|
|
// the tracker whenever the context pointer moves, and the emitter calls this from the
|
|
// first walk that follows), including a make-current BACK to a context that is still alive
|
|
// and whose objects are all still there.
|
|
//
|
|
// So it drops what a returning context may not inherit - the render-state CSOs (whose
|
|
// client-side cache is dropped on the line above it, so both sides start over together),
|
|
// the residual mirror, and the vertex-input WORKING state - and it ADVANCES the two global
|
|
// vertex-input serials rather than zeroing them. It does NOT drop the resource or
|
|
// vertex-elements records: those describe share-group objects that the switch does not
|
|
// destroy, and dropping them is a dropped write on the far side of it.
|
|
//
|
|
// P4a EXTENDS BOTH HALVES AND THE RULE IS UNCHANGED (D-J4). Cleared: the two framebuffer
|
|
// BINDINGS, the three unit sets, DrawProgram / DispatchProgram / BoundShaderCso - all of it
|
|
// per-context working state - with their serials ADVANCED and never zeroed. Not cleared:
|
|
// texture and renderbuffer resources, sampler CSOs, sampler views, shader CSOs, the
|
|
// framebuffer RECORDS, and the texture params and pending uploads that ride on a resource
|
|
// record, because a texture lives in a share group exactly as a buffer does.
|
|
//
|
|
// ID-19(b) MOVED THE FRAMEBUFFER RECORD ACROSS THAT LINE and the reason is worth stating.
|
|
// Before it, the whole framebuffer state was working state and a make-current took it. Now
|
|
// the RECORD is an object record and only the two BOUND HANDLES are working state, so a
|
|
// switch clears the bindings - after which DrawFramebuffer() / ReadFramebuffer() answer
|
|
// null, exactly as the cleared records used to answer a null Fbo - and leaves the table
|
|
// standing. Dropping the table instead would silently lose the record of every framebuffer
|
|
// that is described by NAME and never bound, because the client's re-emission on a fresh
|
|
// context is driven by MGPipeSetHashSuppressor::InvalidateAll, which re-sends the two bound
|
|
// records and nothing else.
|
|
//
|
|
// AND THEREFORE NO P4a TRACKER NEEDS A RE-PUBLICATION PATH ON FreshlyPrimed, AND NONE MAY
|
|
// HAVE ONE: re-emitting create_sampler_state for a record the applier still holds would
|
|
// move its Serial for nothing. What DOES reset on a fresh context is each emitter's
|
|
// BOUND-HANDLE latch - the framebuffer and unit-set hashes through
|
|
// MGPipeSetHashSuppressor::InvalidateAll, and the program emitter's BoundShaderCso mirror -
|
|
// because those mirror working state this function just cleared.
|
|
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_context_values (P5c rv, CONTRACT-P5C.md §5.3): the residual-value record. Writes
|
|
// every field it carries into gPipeInputs through MGPipeApplyAccess - the server-owned
|
|
// write that retires the eight value-class BARRIER_PULLED rows. Emitted only with an
|
|
// active transport; under monolith there is no producer and the fields keep coming
|
|
// through the residual fill (G1).
|
|
void MGPipeApplySetContextValues(const MGPContextValues& values);
|
|
// 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.
|
|
// A NAMED LEVEL IS DROPPED EVEN WHEN EVERY STORAGE-DEFINING FIELD IS UNCHANGED (P4a final
|
|
// review C-1): the pointer is the caller's statement that it reallocated that level, and
|
|
// a non-base level's extent is not in the descriptor. Only a NULL level with unchanged
|
|
// fields is the metadata update that drops nothing (ID-18 M4); the client's mask republish
|
|
// is the one caller of that shape and passes null on purpose.
|
|
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.
|
|
// MGPipeFramebufferTarget::Named sets NEITHER: it is how a DSA entry point hands Espryt
|
|
// a framebuffer it is about to blit into or clear WITHOUT claiming it is bound.
|
|
//
|
|
// FramebufferSerial advances on every applied record, Named included.
|
|
//
|
|
// Two refusals, both Fatal{ProtocolCorruption} and neither counted (see RefusedObjectCalls:
|
|
// this entry point resolves nothing and can only ever fault): a target above Named, a
|
|
// draw-buffer entry outside the record's own Color[], a slot at or above
|
|
// kMGPipeMaxFramebufferSlots, and the NULL HANDLE - a record that named {0,0} would install
|
|
// itself where "nothing is bound" is read, and every emitter has a handle for every
|
|
// framebuffer it describes (kMGPipeDefaultFramebuffer {0,1} for the default one).
|
|
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".
|
|
//
|
|
// Returns true when the record took the parameters (P4a final review m-1, audit F-7): the
|
|
// emitter's version latch advances on this answer and on nothing else, the way the
|
|
// sub-data and respecify paths latch on theirs, so a refused record - no consumer, no
|
|
// record for the handle, a null sampler - is re-sent at the next call rather than at the
|
|
// next glTexParameter*. Source-compatible for the same reason the three resource returns
|
|
// are: a Bool is ignorable and gen_pipe never parses this header.
|
|
Bool 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);
|
|
|
|
// ---------------------------------------------------------------------------------
|
|
// P5e's two entry points: DECLARED BY c0e, BODIED BY sb AND pg
|
|
// ---------------------------------------------------------------------------------
|
|
//
|
|
// Both are declared here, with bodies in PipeApply.cpp that ABORT BY NAME, so that every
|
|
// P5e package compiles and links against the same signatures from day one and the family
|
|
// that lands one of them changes a body rather than adding a symbol. An abort and not a
|
|
// silent no-op: a record that reached an unimplemented applier and returned quietly is a
|
|
// dropped binding set, which renders wrong rather than stopping - the exact failure mode
|
|
// ID-39's sixty-six lost uploads had. Nothing routes either opcode until its package lands
|
|
// (PipeCatalogueTest pins both table slots null), so the abort is unreachable today.
|
|
|
|
// set_shader_buffers, per Class. `tail` is hdr.Count MGPBufferRanges starting at hdr.Start;
|
|
// Class >= kMGPipeShaderBufferClassCount and Start + Count above the capacity are
|
|
// Fatal{ProtocolCorruption, "SetShaderBuffers.Class"} / "SetShaderBuffers.Count". Entries
|
|
// outside the declared window are NOT cleared - the record is "the last set as received",
|
|
// exactly as set_vertex_buffers is.
|
|
void MGPipeApplySetShaderBuffers(const MGPShaderBuffers& hdr, const MGPBufferRange* tail);
|
|
|
|
// set_program_bindings. The three tails in the order the record declares them, plus the
|
|
// RESOLVED block names for the override tail: the names ride SEG_STAGE as host spans and
|
|
// SEG_STAGE retires with the record that named them, so the applier must copy them into the
|
|
// record before this call returns (rule C) and the decoder hands over pointers that are
|
|
// valid only for the duration of the call. `storageOverrideNames[i]` belongs to
|
|
// `storageOverrides[i]`, NUL-terminated, exactly as set_storage_block_binding's single name
|
|
// is. Any of the four pointers may be null when its count is 0.
|
|
void MGPipeApplySetProgramBindings(const MGPProgramBindings& hdr, const Int32* blockBindings,
|
|
const MGPProgramSamplerUnit* samplerUnits,
|
|
const MGPProgramStorageOverride* storageOverrides,
|
|
const char* const* storageOverrideNames);
|
|
|
|
// create_shader_state. THE ARTEFACTS TRAVEL BESIDE THE RECORD, by pointer: all seven of
|
|
// desc.Spirv[] and desc.Reflection are declared with Size 0 ("this record does not declare
|
|
// its blob"), which is what a monolith emission is, and the codec is NOT called - zero
|
|
// serialisation cost on the monolith path. A verify build serialises, deserialises and
|
|
// field-compares before storing, and a mismatch is Fatal{PipeVerifyDiffer, "program-archive"}.
|
|
// Splitting this record for a transport whose ring caps one record at half its capacity is
|
|
// P5's problem, not this entry point's.
|
|
//
|
|
// P5e (pg) ADDS THE FOURTH ARGUMENT AND DID NOT DEFAULT IT, for the reason PipeRoute.h
|
|
// gives about the three byte counts it also refused to default: a caller that HAS the
|
|
// archive and forgets to pass it would compile, and the failure would be a program twin
|
|
// reading the frontend's artefacts on the apply thread - silent, and exactly what this
|
|
// package exists to end. `archive` is null under monolith (the two companion pointers are
|
|
// the frontend's own and the handle arm is not taken there) and non-null under a
|
|
// transport, where `link` and `spirv` point INTO it: the record adopts it, so every later
|
|
// reflection read is of memory the server owns. A non-null archive whose Link/Spirv are
|
|
// not the two pointers passed beside it is a caller bug this entry point asserts on.
|
|
void MGPipeApplyCreateShaderState(const MGPProgramDesc& desc,
|
|
const MG_State::GLState::LinkArtifacts* link,
|
|
const MG_State::GLState::SpirvArtifacts* spirv,
|
|
SharedPtr<const MG_State::GLState::ProgramArchive> archive);
|
|
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
|