Merge branch 'p5e/c0e' into feat/disaggregated (P5e c0e: the contract, the wait-class column, opcode 80, the knobs and the cross-package seams)

This commit is contained in:
2026-09-18 07:00:07 -04:00
35 changed files with 2275 additions and 194 deletions
+32 -3
View File
@@ -322,9 +322,9 @@ namespace MobileGL::MG_Config {
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP4a), so MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-pull control and 0x1ff (kMGPipeSubsystemsMigratedAtP3a) is
// the "everything before P4a" control P4a's A/B is run against - each phase's
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
// is the "everything before P5e" control P5e's A/B is run against - each phase's
// constant survives as the next phase's control, which is why none of them is ever
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
// bits are listed here (MG_Pipe/MGPipe.h owns them):
@@ -342,6 +342,9 @@ namespace MobileGL::MG_Config {
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
// bind_sampler_states / set_shader_images) - requires 0x400
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
// classes and the dirty bits 15/16/17) - requires 0x80
// (every MGPBufferRange::Res names a Buffer handle; P5e)
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
// family runs its legacy arm; it is never half-run.
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
@@ -502,6 +505,32 @@ namespace MobileGL::MG_Config {
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
// saying so.
//
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
// every record are identical on both arms, and the only difference is whether the
// client waits. That is what makes "is the picture the same" a question about the wait
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
// the first barriered row's stale pull.
//
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
// arm that stops filling it.
Uint32 RunAhead = 1;
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
// have in flight before it waits for a swap to come back. 1 = the client publishes
// frame N+1's records while the server applies and swaps frame N - one frame of
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
// client that is already CPU-bound and costs a frame of latency, which is why the
// default is 1 and not "as deep as the ring".
Uint32 PresentCredit = 1;
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
// build, the seven sticky forwards that are otherwise exempt - from "count it in
// rsp" to Fatal (R-7.3).
+17 -4
View File
@@ -8,7 +8,7 @@
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP4a, the push build's PipePush default (the P2 and P3a
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
@@ -263,7 +263,7 @@ namespace MobileGL::MG_ConfigLoader {
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
// subsystems off, everything P3a landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP4a);
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
@@ -379,6 +379,17 @@ namespace MobileGL::MG_ConfigLoader {
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
// run-ahead's whole point is that the client stops filling gPipeInputs for an
// unbarriered record, and the comparator has nothing left to compare when it does.
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
@@ -387,9 +398,11 @@ namespace MobileGL::MG_ConfigLoader {
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB spin=%uus persistent-block=%uKiB "
"adopt-tier=%u verb-barrier=%u strict=%d audit=%d affinity='%s'",
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u strict=%d "
"audit=%d affinity='%s'",
ipc.RingMb, ipc.StageMb, ipc.SpinUs, ipc.PersistentBlockKb, ipc.AdoptTier,
ipc.VerbBarrier, static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
ipc.ServerAffinity.c_str());
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
@@ -13697,4 +13697,88 @@ namespace MobileGL::MG_Backend::DirectGLES {
TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
#if MOBILEGL_PIPE_PUSH
// =====================================================================================
// P5e SEAMS: DECLARED AND REFUSED HERE, BODIED BY THE FAMILY PACKAGES
// (MG_Remote/CONTRACT-P5E.md §4.2; BRIEF-P5E §1's "each such seam is a declared signature
// in c0e so both sides compile from day one")
// =====================================================================================
//
// WHY THEY ARE ALL IN ONE BLOCK AND WHY THE BODIES ABORT. P5e lands as eight packages in
// eight parallel worktrees, and five of them are on opposite sides of these calls: tx2
// writes SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle while fb's
// attachment sync and image sweep CALL them; fb writes the two SyncToBackendByHandle
// overloads while tx2's detach walk needs the reverse index behind them; id rekeys the
// registries the three resolvers read. A package that had to add its own declaration would
// collide with the package that added the other half, which is the merge trap P4a's
// contract package was written to avoid.
//
// So the signature is fixed HERE, once, and each family's commit replaces a body. The body
// ABORTS BY NAME rather than returning null or doing nothing: a resolver that answered null
// would render a blank draw and a sync that did nothing would render stale pixels, and both
// would be green lanes. Nothing calls any of them at this commit - every call site is still
// on the frontend overload beside it - so the abort is a link-time seam, not a runtime one.
//
// A package REPLACES the body in place and deletes the matching comment; it does not add a
// second definition elsewhere, or the linker's answer depends on link order.
namespace {
[[noreturn]] void MGPipeP5eSeamNotLanded(const char* name, const char* owner,
MG_Pipe::MGPipeHandle handle) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"} - the P5e by-handle seam is declared by "
"package c0e and bodied by package %s; it was called for handle {%u, %u} "
"before that package landed",
name, owner, handle.Slot, handle.Gen);
std::abort();
}
} // namespace
namespace VertexArrayImpl {
BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle vertexElements) {
MGPipeP5eSeamNotLanded("VertexArrayImpl::ResolveVaoTwin", "id/vi", vertexElements);
}
} // namespace VertexArrayImpl
namespace TextureImpl {
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture,
Bool imageBindableStorageRequired) {
(void)imageBindableStorageRequired;
MGPipeP5eSeamNotLanded("TextureImpl::SyncTextureToBackendByHandle", "tx2", texture);
}
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle texture) {
MGPipeP5eSeamNotLanded("TextureImpl::ResolveTextureTwin", "id/tx2", texture);
}
void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) {
MGPipeP5eSeamNotLanded("BackendTextureObject::SyncMipmapsToBackendByHandle", "tx2",
texture);
}
} // namespace TextureImpl
namespace FramebufferImpl {
void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo,
FramebufferTarget asTarget) {
(void)asTarget;
MGPipeP5eSeamNotLanded("BackendFramebufferObject::SyncToBackendByHandle", "fb", fbo);
}
} // namespace FramebufferImpl
namespace PrgramImpl {
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso) {
MGPipeP5eSeamNotLanded("PrgramImpl::ResolveProgramTwin", "id/pg", cso);
}
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
MGPipeP5eSeamNotLanded("BackendProgramObjectImpl::SyncToBackendByHandle", "pg", cso);
}
} // namespace PrgramImpl
namespace RenderbufferImpl {
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
MGPipeP5eSeamNotLanded("BackendRenderbufferObject::SyncToBackendByHandle", "fb",
renderbuffer);
}
} // namespace RenderbufferImpl
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectGLES
+85
View File
@@ -1466,6 +1466,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the attribute-offset emulation must stay out of the way. Applied to whatever the
// applier stored, so the answer is the same whichever side resolved it first.
Bool BackendUsesNativeBaseInstance();
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2; declared by c0e, bodied by id/vi) ----
//
// THE VAO TWIN, RESOLVED FROM THE HANDLE THE RECORD CARRIED - MGPipeApplierState::
// BoundVertexElements - instead of from the frontend VertexArrayObject the draw's
// BARRIER_PULLED row hands over. The frontend overload above it stays as the
// monolith-glue half, in the shape ResolveSamplerCsoTwin already established for the
// sampler CSO family: two overloads, not an #if inside one body, so which arm a caller
// is on is visible at the call site.
//
// It is declared HERE, with a body that aborts by name, because the packages that fill
// it in land in parallel worktrees: id rekeys the registry under it, vi moves
// PrepareForDraw's call onto it, and neither may edit the other's file. A missing
// declaration would make that a merge conflict; a declaration with a quiet body would
// make it a null twin and a blank draw.
BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle vertexElements);
#endif
} // namespace VertexArrayImpl
@@ -1590,6 +1606,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendTextureObject(const BackendTextureObject&) = delete;
BackendTextureObject& operator=(const BackendTextureObject&) = delete;
void SyncMipmapsToBackend(const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject);
#if MOBILEGL_PIPE_PUSH
// P5e SEAM (declared by c0e, bodied by tx2): the same storage sync keyed on the
// texture HANDLE, reading the applier's resource record and the server's staged
// store instead of the frontend object's levels and pending uploads. fb's
// attachment sync and the image sweep both call it, which is why it is declared
// once here rather than twice in two packages' worktrees.
void SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture);
#endif
// The storage half of the sync for a texture created by glTextureView. Instead of
// allocating storage and replaying uploads, it makes this object's ES name BE a view
// of the storage texture's ES name (EXT/OES_texture_view), which is what gives the
@@ -1820,6 +1844,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired = false);
#if MOBILEGL_PIPE_PUSH
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// id/tx2) --------------------------------------------------------------------------
//
// EVERY DRAW-PATH TEXTURE ENTRY, BY HANDLE. The caller holds a Texture handle - a
// sampler view's Texture, an image unit's Res, a framebuffer surface's Res, VerbMipRes,
// VerbCopyTexDst, a copy-image endpoint - and the sync resolves the RECORD first and the
// twin by GetOrCreateByHandle, so nothing on the apply thread dereferences a frontend
// ITextureObject. The frontend overload above stays as the monolith-glue half.
//
// ResolveTextureTwin is the lookup without the sync, for the callers that need the twin
// to answer a shape question (an image bind's target and storage format come off the
// twin, not off a new wire field) after tx2's sync has already run this frame.
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(
MG_Pipe::MGPipeHandle texture, Bool imageBindableStorageRequired = false);
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle texture);
#endif
// Brings every texture the next draw reads - the touched units' bindings and the draw
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
// documented at their definitions. Declared here so tests can drive those memos directly.
@@ -1844,6 +1885,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendFramebufferObject& operator=(const BackendFramebufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject,
FramebufferTarget asTarget);
#if MOBILEGL_PIPE_PUSH
// P5e SEAM (MG_Remote/CONTRACT-P5E.md §5.4; declared by c0e, bodied by fb): the same
// sync keyed on the framebuffer HANDLE. The record's eleven surfaces ARE the point
// set - the emitter refuses a point at or above the wire width - so the attachment
// walk needs no frontend FramebufferObject and no m_pushedSyncHandle handshake: the
// handle is the argument. An OVERLOAD rather than a changed signature, so the
// monolith arm and the pull build see no token move.
void SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget asTarget);
#endif
// Apply only this FBO's read buffer (glReadBuffer) to the backend. Split out so it can
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
@@ -2315,6 +2365,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendProgramObjectImpl();
~BackendProgramObjectImpl();
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
#if MOBILEGL_PIPE_PUSH
// P5e SEAM (MG_Remote/CONTRACT-P5E.md §5.5; declared by c0e, bodied by pg): the same
// sync keyed on the ShaderCso HANDLE and answered from the record - the archive the
// create carries and the three binding tails set_program_bindings carries. It is an
// OVERLOAD beside the frontend one, which stays as the monolith-glue half, so the
// pull build's mangled names do not move.
void SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso);
#endif
void Use();
void SetBaseInstance(Uint32 baseInstance) const;
void SetBaseInstanceWordIndex(Int32 wordIndex) const;
@@ -2609,6 +2667,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
};
ImageFormatBakeInputs CollectImageFormatBakeInputs(
const MG_State::GLState::ProgramObject& stateProgramObject);
#if MOBILEGL_PIPE_PUSH
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.5; declared by c0e, bodied by
// id/pg) ---------------------------------------------------------------------------
//
// THE PROGRAM TWIN, RESOLVED FROM MGPipeApplierState::DrawProgram / DispatchProgram /
// BoundShaderCso instead of from GetProgramForDraw()'s frontend SharedPtr - the second
// unconditional pointer read of every draw, and one of the two rows whose retirement is
// what P5e is for. Composite pipeline programs resolve through the same call: their
// slots come out of the allocator's composite band, which the server's slot table gains
// a band for so the ordinary table does not grow to a million entries.
//
// Null, loudly, when the handle names no record or cannot be adopted - the shape
// ResolveSamplerCsoTwin set - and null silently for the null handle, which is the legal
// "nothing bound".
BackendProgramObjectImpl* ResolveProgramTwin(MG_Pipe::MGPipeHandle cso);
#endif
} // namespace PrgramImpl
namespace SamplerImpl {
@@ -2789,6 +2864,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendRenderbufferObject(const BackendRenderbufferObject&) = delete;
BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete;
void SyncToBackend(const SharedPtr<MG_State::GLState::RenderbufferObject>& stateRBOObject);
#if MOBILEGL_PIPE_PUSH
// P5e SEAM (declared by c0e, bodied by fb): the renderbuffer twin of the
// framebuffer overload above. MGPSurface::Res names the renderbuffer and the
// resource record already carries its format, extent and sample count (P4a), so the
// attachment sync needs no frontend RenderbufferObject - which is what deletes the
// one live identity probe left in the attachment path (Managers.cpp's renderbuffer
// cross-check, the sibling that never got the Transport == Monolith gate its
// texture counterpart has).
void SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer);
#endif
Uint GetBackendRenderbufferId() const { return m_backendRBOId; }
void Bind() const;
+31
View File
@@ -81,6 +81,22 @@ namespace MobileGL::MG_Backend {
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
// the same fact ObjectSubsystemControlScenario already pins from the client side.
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
//
// kCapRunAheadApply says "this server applies an unbarriered record without reading
// client memory". That is only true once EVERY per-draw family reads records instead
// of the frontend - vi, sb, pg, tx2 and fb all land before it is - so the caps arm
// below is gated on this constant, which the P5e INTEGRATION COMMIT flips to true
// after the last of them. Until then every P5e package lands with the wait rule, the
// static WaitClass column and the barriered predicate compiled and INERT: the client
// never latches RunAheadArmed, so it runs today's lockstep path byte for byte.
//
// It is a constant and not a knob on purpose. An operator cannot turn a half-migrated
// server into a run-ahead one, because the failure mode is not a slow frame - it is
// the apply thread reading client memory that has already moved, which renders wrong
// rather than aborting.
constexpr Bool kMGPipeP5eRunAheadReady = false;
Uint64 ConsumedSubsystemsFor(BackendType type) {
switch (type) {
case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP4a;
@@ -166,6 +182,21 @@ namespace MobileGL::MG_Backend {
serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) {
capBits |= MG_Pipe::kCapBackendOwnsXfbCapture;
}
// P5e (CONTRACT-P5E.md §1, §6): kCapRunAheadApply, THE DIRECTGLES ARM AND ONLY IT.
//
// Magma is deliberately absent and is not an omission: it keeps the lockstep for
// the whole of P5e, its four apply-thread allocator sites are real debt P7 retires,
// and MGPipeApplierCurrentRecordIsBarriered() answers true for every record on a
// server that does not publish this bit - which is exactly what keeps those probes
// and its BARRIER_PULLED reads inside P5C's semantics and its rsp accounting
// honest. Publishing the bit here for DirectVulkan would turn accounted pulls into
// torn ones, so MagmaPipeIdentityTest pins its absence.
//
// The Espryt arm is gated on kMGPipeP5eRunAheadReady, which is false until the
// integration commit: the bit is what ARMS the client, so every package before it
// lands inert.
capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType,
kMGPipeP5eRunAheadReady);
session.SetCapabilityBits(capBits);
session.SetBackend(loop.Backend());
+25 -18
View File
@@ -48,6 +48,31 @@
namespace MobileGL::MG_Pipe {
// GL_READ_ONLY / GL_WRITE_ONLY / GL_READ_WRITE folded into the one byte the wire carries.
// A value the enum does not name would otherwise truncate silently into a Uint8, which is
// the class of bug the descriptors exist to close.
//
// P5e (CONTRACT-P5E.md §1, ruling 16) MOVED THE NUMBERS OUT FROM UNDER THIS FUNCTION and
// left the GLenum half here, where the GLenum is. The three values are
// MGPipeValueTypes.h's MGPipeImageAccess, which the server decodes through, so the encode
// and the decode are one table instead of two literals that happened to agree. It is also
// why the function is a free function now rather than a private static: the unit case that
// pins the three constants has to reach BOTH halves (MG_Test/Pipe/ImageEmitTest.cpp).
inline Uint32 MGPipeEncodeImageAccess(GLenum access) {
switch (access) {
case GL_READ_ONLY:
return kMGPipeImageAccessReadOnly;
case GL_WRITE_ONLY:
return kMGPipeImageAccessWriteOnly;
case GL_READ_WRITE:
return kMGPipeImageAccessReadWrite;
default:
MOBILEGL_ASSERT(false, "glBindImageTexture access 0x%x is not one of the three GL names",
static_cast<Uint>(access));
return kMGPipeImageAccessReadOnly;
}
}
// D-G3. Over the tail with Start and Count mixed in, the same shape the two sampler sets
// use - and it covers InternalFormat and Access because those are live glBindImageTexture
// state that the format-less image bake keys on, not decoration.
@@ -157,24 +182,6 @@ namespace MobileGL::MG_Pipe {
Uint32 Window() const { return m_window; }
private:
// GL_READ_ONLY / GL_WRITE_ONLY / GL_READ_WRITE folded into the one byte the wire
// carries. A value the enum does not name would otherwise truncate silently into a
// Uint8, which is the class of bug the descriptors exist to close.
static Uint32 MGPipeEncodeImageAccess(GLenum access) {
switch (access) {
case GL_READ_ONLY:
return 0;
case GL_WRITE_ONLY:
return 1;
case GL_READ_WRITE:
return 2;
default:
MOBILEGL_ASSERT(false, "glBindImageTexture access 0x%x is not one of the three GL names",
static_cast<Uint>(access));
return 0;
}
}
Array<MGPImageView, kMGPipeMaxImageUnits> m_entries{};
MGPShaderImages m_lastImages{};
Uint32 m_window = 0;
+42
View File
@@ -2023,6 +2023,15 @@ namespace MobileGL::MG_Pipe {
// emission gate is the subsystem bit plus the whole-record hash and nothing else.
case MGPipeFieldEmitter::SetContextValues:
return kMGPipeSubsystemResidualValues;
// P5e's one emitted row (MG_Remote/CONTRACT-P5E.md §5.6): the indexed buffer
// binding points. Written HERE at the contract commit, with the Coverage.def row
// and EmittedCallSuppliesTheWholeField's arm beside it, for the reason
// kMGPipeWiredSubsystems' block states one paragraph down - this file belongs to
// the contract package for the whole phase, and an emitter enumerator whose
// dispatch arm lived in the family's own worktree is the merge trap that block
// exists to close. It is inert until that family's wired constant leaves 0.
case MGPipeFieldEmitter::SetShaderBuffers:
return kMGPipeSubsystemBufferBindings;
case MGPipeFieldEmitter::kNone:
break;
}
@@ -2091,6 +2100,15 @@ namespace MobileGL::MG_Pipe {
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
"the MGPipe vertex-attribute capacity and the frontend's have drifted");
// P5e (MG_Remote/CONTRACT-P5E.md §1, ruling 10), pinned here for the same reason and in
// the same place: this translation unit sees the frontend constant and the MG_Pipe one,
// and nothing else does. 84 is what the emitter may describe and what the wire carries;
// the BACKEND clamps to the device's real GL_MAX_UNIFORM_BUFFER_BINDINGS, as it does
// today, because a client-side clamp would read a device capability from the wrong side.
static_assert(kMGPipeMaxBufferBindingPoints ==
MG_State::GLState::BufferBindingPointCount,
"the MGPipe buffer-binding-point capacity and the frontend's have drifted");
// ---- P4a's SEVEN pairings, and EVERY ONE OF THEM COMPARES AGAINST
// SubsystemForEmitter RATHER THAN AGAINST A CONSTANT. That is the lesson written out
// twenty lines above and it is not a style preference: naming the subsystem constant
@@ -2128,6 +2146,23 @@ namespace MobileGL::MG_Pipe {
SubsystemForEmitter(MGPipeFieldEmitter::SetDispatchProgram),
"set_draw_program and set_dispatch_program are one family and one A/B");
// P5e's pairing, in the same shape and for the same reason as P4a's seven: the two maps
// answer different questions - one takes a field's EMITTER, the other a dirty BIT - and
// the emission is gated on one while the residual-fill skip is gated on the other, so a
// divergence would push a call whose field is still pulled, or skip a field whose call
// was never emitted. Compared against SubsystemForEmitter rather than against the
// constant, because pinning the two MAPS to each other is the statement that cannot
// drift.
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers) ==
SubsystemForEmitter(MGPipeFieldEmitter::SetShaderBuffers),
"set_shader_buffers and NEW_SHADER_BUFFERS must name one subsystem");
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewConstBuffers) ==
MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers),
"the three binding-point bits are one family and one A/B");
static_assert(MGPipeSubsystemForDirty(MGPipeDirty::NewSoTargets) ==
MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers),
"the three binding-point bits are one family and one A/B");
// THE TEXTURE-RESOURCE SUBSYSTEM HAS NO DIRTY BIT, and that has to be asserted rather
// than left as an absence: its calls are dispatched from the GL entry points that
// cause them, so a bit that started naming it would gate the emission twice - once at
@@ -2283,6 +2318,13 @@ namespace MobileGL::MG_Pipe {
case MGPipeInputField::GetTextureUnitObject:
case MGPipeInputField::GetProgramForDraw:
case MGPipeInputField::GetProgramForDispatch:
// P5e (CONTRACT-P5E.md §5.6): the same answer for the same reason. The field is
// four raw bases into the frontend's binding-point table and set_shader_buffers
// carries resolved {handle, offset, size} ranges; the applier cannot produce a
// pointer, so skipping the pull would leave the mirror null on every draw of every
// push build. What retires it is the four Espryt consumers reading the applier's
// BoundShaderBuffers, not this row.
case MGPipeInputField::GetBufferBindingPoint:
return false;
default:
return true;
+16 -1
View File
@@ -62,8 +62,23 @@ namespace MobileGL::MG_Pipe {
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
SetShaderBuffers, // P4b
// P5e (MG_Remote/CONTRACT-P5E.md §1, ruling 11): THREE SLOTS, ONE PER CLASS, not one
// for the call. set_shader_buffers is emitted per Class (Uniform / ShaderStorage /
// AtomicCounter) because the record's own Class field says which binding-point array
// it describes - so a single slot would make every emission of one class cancel the
// previous emission of another, and the shader-storage set would be suppressed as
// "unchanged" by a uniform set that happened to hash the same way. Three slots also
// keep the per-family fire tally and the A/B meaningful, which one keyed on
// (slot, Class) would not.
SetShaderBuffersUniform, // P5e (sb)
SetShaderBuffersShaderStorage, // P5e (sb)
SetShaderBuffersAtomicCounter, // P5e (sb)
SetStreamOutputTargets, // P4b
// P5e (CONTRACT-P5E.md §1): set_program_bindings, the post-link binding record. Not
// kVarTail-only - it has three tails - but the same rule applies: the emitter latches
// on (Cso, backendStateVersion, blockBindingVersion) and the whole-record hash is what
// says "nothing moved".
SetProgramBindings, // P5e (pg)
SetVertexAttribDefaults, // P2 - the one consumer that is wired
SetFramebufferState, // P4a - wired
// P5c rv (CONTRACT-P5C.md §5.3). NOT kVarTail either - the same shape as
+31 -2
View File
@@ -136,6 +136,19 @@ namespace MobileGL::MG_Pipe {
MGPipeDirtyBit(MGPipeDirty::NewFramebuffer) | MGPipeDirtyBit(MGPipeDirty::NewSamplerViews) |
MGPipeDirtyBit(MGPipeDirty::NewSamplers) | MGPipeDirtyBit(MGPipeDirty::NewShaderImages);
// The THREE P5e adds, all one subsystem (kMGPipeSubsystemBufferBindings): the indexed
// buffer binding points, whose three bits have been computed and counted since P2 and have
// named no subsystem since. Added rather than edited in, for the reason above.
//
// NewSoTargets rides the same bit even though set_stream_output_targets stays UNEMITTED
// for the whole of P5e (XFB is lockstep by escalation, CONTRACT-P5E.md §5.7): the bit is
// "which A/B switch owns this family's legacy arm", and an operator clearing bit 13 has to
// get the whole binding-point family's frontend walk back rather than two thirds of it -
// the rule P3a's three and P4a's two triples already state.
inline constexpr Uint32 kMGPipeDirtyEmittedAtP5e =
kMGPipeDirtyEmittedAtP4a | MGPipeDirtyBit(MGPipeDirty::NewConstBuffers) |
MGPipeDirtyBit(MGPipeDirty::NewShaderBuffers) | MGPipeDirtyBit(MGPipeDirty::NewSoTargets);
inline constexpr const char* kMGPipeDirtyNames[kMGPipeDirtyCount] = {
"NEW_RENDER_STATE",
"NEW_PIPELINE_STATE",
@@ -198,6 +211,22 @@ namespace MobileGL::MG_Pipe {
case MGPipeDirty::NewSamplers:
case MGPipeDirty::NewShaderImages:
return kMGPipeSubsystemSamplers;
// P5e's three, one subsystem (MG_Remote/CONTRACT-P5E.md §1): the indexed buffer
// binding points. Bits 15/16/17 have been computed and counted since P2 and have named
// no subsystem since - "the remaining bits have no call of their own until P4b", which
// the default arm below used to say for them. set_shader_buffers is the call, and
// set_stream_output_targets stays unemitted while riding the same A/B bit, for the
// reason kMGPipeDirtyEmittedAtP5e states.
//
// NAMING THE SUBSYSTEM IS NOT THE SAME AS EMITTING. The emission gate is five
// conjuncts (PipeFill.cpp's `wants()`), one of which is kMGPipeWiredSubsystems - and
// the buffer-binding family's wired constant is 0 until the package that gives the
// emitter its body sets it, exactly as P4a's four families were. So this map moves
// here, inert, and nothing is emitted and no field is skipped on its account yet.
case MGPipeDirty::NewConstBuffers:
case MGPipeDirty::NewShaderBuffers:
case MGPipeDirty::NewSoTargets:
return kMGPipeSubsystemBufferBindings;
// NO BIT NAMES kMGPipeSubsystemTextureResources, and that is deliberate rather than an
// omission: the texture and renderbuffer resource_* calls and set_texture_params are
// dispatched from the GL entry points that cause them - a constructor, a storage
@@ -205,8 +234,8 @@ namespace MobileGL::MG_Pipe {
// is. Bit 10 gates those dispatch sites; there is no dirty bit to map onto it and
// there must not be one, or the emission would be gated twice and disagree with itself.
default:
// The remaining bits have no call of their own until P4b, so there is no
// subsystem to switch and the residual fill keeps supplying their fields.
// Every dirty bit now names a subsystem; the arm stays because the switch is over
// a value cast from an index and a future bit must not fall off the end.
return 0;
}
}
+18
View File
@@ -236,6 +236,23 @@
// And GetTextureObject / GetProgramObject are STICKY (see MGP_COVERAGE_STICKY_LIST): they are
// keyed by GL name, they are object lookups rather than verb state, and a forwarded field has
// no storage for an emitted call to supply.
//
// P5e ADDS ONE ROW, GetBufferBindingPoint -> SetShaderBuffers (MG_Remote/CONTRACT-P5E.md §5.6),
// and it is SHAPE-ONLY like P3a's and P4a's six: the field's storage is four raw bases into the
// frontend's Array<Array<BindingSlotRange1D, 84>, 4>, and set_shader_buffers carries RESOLVED
// {handle, offset, size} ranges per class. The applier has no way to produce a pointer and P5e
// deliberately does not give it one, so the row lands in PipeFill.cpp's
// EmittedCallSuppliesTheWholeField FALSE arm and the decision is taken THERE. What retires the
// pull is not a better applier, it is the four Espryt consumers reading the applier's
// BoundShaderBuffers instead of the frontend's binding points - at which point the
// FieldOwnership.def row flips to RECORD_SUPPLIED.
//
// THE ROW IS INERT AT THIS COMMIT for GetBoundVertexArray's reason one phase on: the emission
// and the fill skip both test kMGPipeWiredSubsystems, and the buffer-binding family's wired
// constant is 0 until the package that gives MG_Impl/Pipe/ShaderBufferEmit.h its body sets it.
// It lands here, with its two PipeFill.cpp switch arms, because this file and that one belong
// to the contract package for the whole phase - a family package that had to add an enumerator
// AND the arm that dispatches on it would be editing the one file the merge trap lives in.
#define MGP_COVERAGE_EMITTED_LIST(X) \
X(GetActiveTextureUnit, SetContextValues) \
X(GetBlendColor, SetDynamicState) \
@@ -243,6 +260,7 @@
X(GetBlendFuncIndexed, CreateRenderState) \
X(GetBoundTransformFeedbackLifetimeId, SetContextValues) \
X(GetBoundVertexArray, BindVertexElements) \
X(GetBufferBindingPoint, SetShaderBuffers) \
X(GetClampReadColor, SetDynamicState) \
X(GetClearColor, SetDynamicState) \
X(GetClearDepth, SetDynamicState) \
+7 -7
View File
@@ -69,7 +69,7 @@
/* no record fallback, and PipeFill.cpp:1902-1905 says outright that what retires the pull */ \
/* is P8, not a better applier: the storage is a SharedPtr<VertexArrayObject> and */ \
/* bind_vertex_elements carries an eight-byte {slot, gen}. */ \
X(GetBoundVertexArray, BARRIER_PULLED, "P8", \
X(GetBoundVertexArray, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend heap reference; the record carries a handle, the mirror is a pointer") \
/* 18 Espryt sites + 11 Magma. Coverage.def:37-70 splits the 15 BufferTargets across three */ \
/* calls and leaves SEVEN with no carrier at all (CopyRead, CopyWrite, PixelPack, */ \
@@ -77,19 +77,19 @@
/* though eight targets are covered. */ \
X(GetBufferBindingSlot, BARRIER_PULLED, "P8 (indirect), P9 (readback), P13 (transfer)", \
"7 of 15 BufferTargets have no call; the field is one array over all 15") \
X(GetBufferBindingPoint, BARRIER_PULLED, "P3b/P4b, P7", \
X(GetBufferBindingPoint, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend BindingSlotRange1D pointer") \
/* 8 Espryt sites through GetFramebufferBindingSlotChecked + 13 Magma. SyncCurrentFBO */ \
/* (:2995) is SELF-DECLARED monolith glue (DirectGLES.cpp:2961-2965) while BindCurrentFBO */ \
/* (:4303-4353) is already split-clean - the bind is migrated, the sync is not. */ \
X(GetFramebufferBindingSlot, BARRIER_PULLED, "P3b/P4b (Espryt), P7 (Magma)", \
X(GetFramebufferBindingSlot, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend BindingSlot<FramebufferObject> pointer") \
X(GetImageTextureBinding, BARRIER_PULLED, "P3b/P4b, P7", \
X(GetImageTextureBinding, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend ImageTextureBinding base pointer") \
X(GetTextureUnitObject, BARRIER_PULLED, "P3b/P4b, P7", \
X(GetTextureUnitObject, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend TextureUnit base pointer; 13 Espryt + 8 Magma sites") \
/* DirectGLES.cpp:4497, PrepareForDraw, the second unconditional pointer read of every draw. */ \
X(GetProgramForDraw, BARRIER_PULLED, "P8 (Espryt), P7 (Magma)", \
X(GetProgramForDraw, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
/* P5b package i1 (CONTRACT-P5B.md §6.9, the one row the contract grants a package). This */ \
/* was FATAL "reachable only from kDispatch; there is no compute on the reduced path" - and */ \
@@ -97,7 +97,7 @@
/* PrepareForCompute pulls this inside it (DirectGLES.cpp:5779, VulkanRenderer.cpp:7327). */ \
/* It is GetProgramForDraw's twin in every respect, so it takes its class and its retiring */ \
/* phases. Overturned by: nothing in P5b; P7/P8 retire both rows together. */ \
X(GetProgramForDispatch, BARRIER_PULLED, "P7 (Magma), P8 (Espryt)", \
X(GetProgramForDispatch, BARRIER_PULLED, "P5e (Espryt unbarriered), P7 (Magma)", \
"frontend SharedPtr<ProgramObject>; the record carries a handle") \
/* The XFB family's one OBJECT row. XFB itself is off the reduced path, but kDraw's */ \
/* may-read mask carries it and the draw walk reads it regardless - which is exactly the */ \
+42 -1
View File
@@ -54,6 +54,30 @@ namespace MobileGL::MG_Pipe {
kOptional = 1u << 5,
};
// P5e (MG_Remote/CONTRACT-P5E.md §2.2). THE FIFTH COLUMN OF PipeCalls.def: what the client
// does after publishing a record of this call, on a server that publishes
// kCapRunAheadApply. Unscoped for MGPipeCallClass's reason - PipeCalls.def spells these as
// bare tokens so the same file can be read by the C++ preprocessor and by
// scripts/gen_pipe.py, which generates MGPipeWaitClassFor(op) from it.
//
// IT IS A VALUE, NOT AN OPCODE AND NOT A FLAG. The wire carries the opcode and both roles
// recover the class from it, exactly as they recover the flags - which is the whole reason
// MGPipeBarriered(op, payload, applierState) can be one function computed identically by
// the emit table and by the sink instead of two copies that agree until they do not.
//
// kWaitNone is 0 so that an uninitialised read is the SAFE direction in exactly one sense
// and the dangerous one in the other, which is why nothing reads this table without an
// opcode that came out of the catalogue: MGPipeWaitClassFor answers kWaitClassCount for an
// opcode it does not know, and a caller holding bytes off a stream must reach its own
// Fatal{ProtocolCorruption} rather than a wait decision.
enum MGPipeWaitClass : Uint8 {
kWaitNone = 0,
kWaitApplied,
kWaitReply,
kWaitPresent,
kWaitClassCount,
};
// The pipeline/dynamic split of RenderStateParameters, defined exactly once (section
// 4.5.2): MG_Pipe/MGPipeRenderStateSpans.{h,cpp}, which landed with P2 and computes
// every chunk boundary with offsetof. Include that header to use it; what stays here
@@ -106,7 +130,17 @@ namespace MobileGL::MG_Pipe {
// the three unit sets
inline constexpr Uint64 kMGPipeSubsystemPrograms = 1ull << 12; // shader CSO, draw/dispatch
// program, global constants
// bits 13..62 reserved for the later phases, allocated in ROADMAP order.
// P5e's one (MG_Remote/CONTRACT-P5E.md §1). The INDEXED BUFFER BINDING POINTS: the three
// dirty bits NewConstBuffers / NewShaderBuffers / NewSoTargets, the set_shader_buffers
// emitter, and the backend's four binding-point walks. It is its own bit and not part of
// bit 7's resource family for the reason every other split here has: a UBO/SSBO binding
// path that regressed and a buffer path that regressed are different findings, and
// clearing one must not disarm the other.
//
// BIT 13 REQUIRES BIT 7, and for bit 11's reason: every MGPBufferRange::Res names a Buffer
// handle, and only bit 7 puts one in the slot table.
inline constexpr Uint64 kMGPipeSubsystemBufferBindings = 1ull << 13;
// bits 14..62 reserved for the later phases, allocated in ROADMAP order.
// NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of CSOs, so
// every pipeline-version change mints a fresh CSO and the map is never probed. This is
// the negative control the whole CSO design is measured against (ROADMAP.md P2).
@@ -124,6 +158,13 @@ namespace MobileGL::MG_Pipe {
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers |
kMGPipeSubsystemPrograms),
"the P4a phase constant and P4a's four subsystem bits have drifted");
// P5e's, and P4a's is NOT edited: 0x1fff stays P5e's "everything P4a had and nothing of
// mine" control, exactly as 0x1ff was P4a's and 0x7f was P3a's. The push default becomes
// 0x3fff, so the A/B that reproduces today's picture is one bit cleared.
inline constexpr Uint64 kMGPipeSubsystemsMigratedAtP5e = 0x3fffull; // bits 0..13
static_assert(kMGPipeSubsystemsMigratedAtP5e ==
(kMGPipeSubsystemsMigratedAtP4a | kMGPipeSubsystemBufferBindings),
"the P5e phase constant and P5e's subsystem bit have drifted");
// The catalogue itself. Only macros, so it is safe to expand inside the namespace, and
// consumers (the unit test, later the transport) get MGP_CALL_LIST from this header.
+140 -2
View File
@@ -133,8 +133,38 @@ namespace MobileGL::MG_Pipe {
// buffer. So the answer is the SERVER's table, published as a bit and read through
// MGL_BACKEND_SLOT_CAP. The first of the "XFB span family" bits SlotCaps.h predicted.
kCapBackendOwnsXfbCapture = 1ull << 9,
// P5e (MG_Remote/CONTRACT-P5E.md §1, §6). THE SERVER APPLIES AN UNBARRIERED RECORD
// WITHOUT READING CLIENT MEMORY: every object it needs is resolved from a handle the
// record or the applier state carries, so the client may publish and move on instead
// of waiting for appliedSeq at every verb (rule F).
//
// It is set in the DirectGLES arm of the server's CallMask and NEVER in the
// DirectVulkan one - Magma keeps the lockstep for the whole of P5e (§6), its
// apply-thread allocator debt is real, and a bit published there would turn that debt
// into torn reads rather than into the accounted pulls it is today. On Espryt the arm
// is gated on kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp), false until the P5e
// integration commit, so every package lands with the wait rule inert.
//
// The client latches RunAheadArmed() = m_barrierArmed && Ipc.RunAhead &&
// Caps().HasCap(kCapRunAheadApply) at the first caps adoption after Start; a later
// snapshot may only turn it OFF, because a caps re-run must not be able to make a
// lockstep server run-ahead in the middle of a frame.
kCapRunAheadApply = 1ull << 10,
};
// THE RUN-AHEAD ARM, AS A PURE FUNCTION, so that "Magma never" is something a unit case can
// hold rather than a line inside MG_Backend/Init.cpp's InitSplitRoles that only a live
// split session reaches. `ready` is Init.cpp's kMGPipeP5eRunAheadReady - false until the
// P5e integration commit - and the BACKEND TYPE is the half that must not depend on it:
// whatever `ready` says, a DirectVulkan server publishes no bit 10, because Magma keeps the
// lockstep for the whole of P5e (CONTRACT-P5E.md §6) and its apply thread really does read
// client memory inside MagmaP7AllocatorDebtScope. MG_Test/Pipe/MagmaPipeIdentityTest.cpp
// pins both halves; reverting the type test there is the phase's named red.
inline constexpr Uint64 MGPipeRunAheadCapBitsFor(BackendType type, Bool ready) {
return (ready && type == BackendType::DirectGLES) ? static_cast<Uint64>(kCapRunAheadApply)
: static_cast<Uint64>(kCapNone);
}
struct MGPCaps {
// The ~90 flat scalars the backends already publish, by inclusion rather than by
// restatement: a caps field added there must not need a second edit here. This is
@@ -591,10 +621,26 @@ namespace MobileGL::MG_Pipe {
Uint8 NativeFloat64;
Uint8 PointSizeDemoted;
Uint8 EnableSpirvValidation;
// P5e (CONTRACT-P5E.md §1). 1 = the link this record describes SUCCEEDED. Today
// "linked" is only implied by the create existing, which is not the same statement:
// create_shader_state is re-issued on the same handle at every link that moves
// GetLinkVersion, and a FAILED relink of a bound program moves it too. The server's
// clean condition needs the difference spelled, because a draw on a program the
// frontend reports unlinked DECLINES (the frontend raises INVALID_OPERATION) and a
// record that cannot say so leaves the twin rebuilding from empty artefacts.
//
// THE DESCRIPTOR GREW 192 -> 200 AND THE DRAFT'S "in the descriptor's existing pad"
// WAS WRONG: the four bytes above end at offset 24 and MGPBlobRef is 8-aligned, so
// there is no spare byte and a fifth one costs the alignment slack below. It is paid
// once, on a record that fires at most once per link, and it is confined to the push
// build (this payload is only ever instantiated under MOBILEGL_PIPE_PUSH), which is
// why G1 does not see it. PipeCatalogueTest pins the new size.
Uint8 LinkStatus;
Uint8 Pad0[7];
MGPBlobRef Spirv[6]; // per stage
MGPBlobRef Reflection;
};
MGP_ASSERT_POD(MGPProgramDesc, 192);
MGP_ASSERT_POD(MGPProgramDesc, 200);
// ---------------------------------------------------------------------------------
// set_*
@@ -802,6 +848,17 @@ namespace MobileGL::MG_Pipe {
inline constexpr Uint32 kMGPipeMaxTextureUnits = 192;
inline constexpr Uint32 kMGPipeMaxImageUnits = 192;
// P5e (CONTRACT-P5E.md §1, ruling 10). How many INDEXED BUFFER BINDING POINTS one
// set_shader_buffers record may describe per class. 84 is the frontend's capacity
// (MG_State/GLState/BufferState/BufferState.h's BufferBindingPointCount, itself the GL 4.5
// core minimum for GL_MAX_UNIFORM_BUFFER_BINDINGS) and it is what travels: the BACKEND
// clamps to the device's real ceiling, as it does today, because a client-side clamp would
// read a device capability from the wrong side of the wire.
//
// Pinned against the frontend constant in MG_Impl/Pipe/PipeFill.cpp, the one translation
// unit that sees both, exactly as the two above are.
inline constexpr Uint32 kMGPipeMaxBufferBindingPoints = 84;
struct MGPVertexBuffer {
MGPipeHandle Res;
Uint64 Offset;
@@ -907,8 +964,23 @@ namespace MobileGL::MG_Pipe {
// Var-tail header: MGPBufferRange[Count], then MGHostSpan[HostSpanCount]. HostSpanCount is
// 0, or Count for the Uniform class under kCapNeedsHostUboBytes (a range with nothing to
// ship carries an empty span, so the two arrays stay index-aligned).
// MGPShaderBuffers::Class's three values, and the ONLY spelling of them (P5e,
// MG_Remote/CONTRACT-P5E.md §1). The payload's comment has named them since P4a and
// nothing numbered them, so the emitter and the applier were one literal each away from
// disagreeing. Uniform is 0 so a zeroed record describes the uniform binding points, which
// is the class every workload has.
//
// XFB IS NOT ONE OF THEM: set_stream_output_targets carries a Generation this payload has
// no field for and its capture points are span-scoped state latched at Begin, so the
// catalogue's split between the two rows stays (§5.7).
inline constexpr Uint32 kMGPipeShaderBufferClassUniform = 0;
inline constexpr Uint32 kMGPipeShaderBufferClassShaderStorage = 1;
inline constexpr Uint32 kMGPipeShaderBufferClassAtomicCounter = 2;
inline constexpr Uint32 kMGPipeShaderBufferClassCount = 3;
struct MGPShaderBuffers {
Uint32 Class; // Uniform | ShaderStorage | AtomicCounter
Uint32 Class; // MGPipeTypes.h's kMGPipeShaderBufferClass* - Uniform | ShaderStorage |
// AtomicCounter
Uint32 Start;
Uint32 Count;
Uint32 WritableMask;
@@ -927,6 +999,60 @@ namespace MobileGL::MG_Pipe {
};
MGP_ASSERT_POD(MGPStreamOutputTargets, 24);
// ---- set_program_bindings (P5e, opcode 80; CONTRACT-P5E.md §1 and §5.5) --------------
//
// THE THREE POST-LINK MUTABLE REFLECTION FIELDS. create_shader_state carries the archive
// as LINKED; glUniformBlockBinding, glUniform1i on a sampler and glShaderStorageBlockBinding
// all move a binding AFTER the link, inside the frontend's LinkArtifacts, so an archive
// snapshot alone is wrong and the server would answer a draw with the bindings the program
// was linked with rather than the ones it is bound with.
//
// Three tails, in this order, because the three live in three different index spaces:
// Int32 BlockBindings[BlockBindingCount] dense, GL uniform-block index
// MGPProgramSamplerUnit SamplerUnits[SamplerUnitCount] sparse, ascending Location
// MGPProgramStorageOverride StorageOverrides[StorageOverrideCount]
// The storage-override map is NAME-keyed BY DESIGN (ProgramObject.h: three index spaces,
// one coordinate), so its key travels as a host span staged whole, exactly as
// set_storage_block_binding's single name does.
//
// Emitted BEFORE create_shader_state at the same validate point: a rebuild inside the verb
// must already see the bindings. A re-issued create CLEARS all three tails, for the reason
// GlobalConstants is cleared - a relink replaces the archive the tails index into.
struct MGPProgramSamplerUnit {
Uint32 Location; // the frontend uniform location
Int32 Unit; // the texture image unit glUniform1i put there
};
MGP_ASSERT_POD(MGPProgramSamplerUnit, 8);
struct MGPProgramStorageOverride {
MGHostSpan Name; // the block name, NUL included, staged whole
Int32 Binding;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPProgramStorageOverride, 40);
// The three declared bounds. A program that exceeds one is a COUNTED REFUSAL and never a
// truncation: a sampler array larger than 256 locations is a program this record cannot
// describe, and silently describing 256 of them is the class of bug the descriptors exist
// to close. The codec raises Fatal{ProtocolCorruption, "SetProgramBindings.<field>"}.
inline constexpr Uint32 kMGPipeMaxProgramBlockBindings = 64;
inline constexpr Uint32 kMGPipeMaxProgramSamplerUnits = 256;
inline constexpr Uint32 kMGPipeMaxProgramStorageOverrides = 64;
struct MGPProgramBindings {
MGPipeHandle Cso;
// The client's commutative hash of the override MAP, the same function the backend
// computes today (Managers.cpp's ComputeShaderStorageBlockBindingSignature), so the
// server's "do I have to rebuild" clause is one Uint64 compare and the name tail is
// touched only on a rebuild.
Uint64 Signature;
Uint32 BlockBindingCount;
Uint32 SamplerUnitCount;
Uint32 StorageOverrideCount;
Uint32 Pad0;
};
MGP_ASSERT_POD(MGPProgramBindings, 32);
// Covers the DEFAULT UNIFORM BLOCK only (D6).
struct MGPGlobalConstants {
MGPipeHandle ShaderCso;
@@ -1413,6 +1539,18 @@ namespace MobileGL::MG_Pipe {
// server never reads the indirect buffer to learn a count and the client never sends
// ranges it does not have.
kDrawIsIndirect = 1u << 5,
// P5e (CONTRACT-P5E.md §1, §5.1, ruling 2). AT LEAST ONE ENABLED ATTRIBUTE OF THE
// BOUND VAO HAS NO BUFFER: the draw's vertex bytes live in client memory and the
// server uploads them today by dereferencing the raw client pointer the record's
// Offset carries (Managers.cpp's SyncClientSideAttributesForDrawArrays). That is
// exactly the read rule F forbids, and it is NOT refused under split at this head -
// only client INDEX arrays are. So the flag exists to make the case VISIBLE on the
// wire: the barriered predicate escalates on it, and under run-ahead the client
// refuses the draw by name (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) rather
// than publishing bytes that may move before they are read. The staged form - a
// per-attribute {BindingIndex, MGHostSpan} tail, shaped like kDrawHasUserIndices - is
// P8's; the lockstep arm is unchanged.
kDrawClientArrays = 1u << 6,
};
// = pipe_draw_info. Today's twenty draw entry points collapse onto this one call, with
+49
View File
@@ -599,6 +599,55 @@ namespace MobileGL {
Uint32 Divisor; // 12
};
// ---- MGPImageView::Access, P5e (CONTRACT-P5E.md §1, ruling 16) -----------------------
//
// ONE TABLE, BOTH ROLES. The CLIENT has encoded this byte since P4a
// (MG_Impl/Pipe/ImageEmit.h's MGPipeEncodeImageAccess, which folds the three GL names into
// 0/1/2 and asserts on anything else); the SERVER open-coded the reverse mapping at its one
// reader and carried a comment saying the encoding "does not exist", which was stale. The
// numbers now live HERE, in the value header both sides already include, so the encode and
// the decode cannot drift - the client emitter names these enumerators instead of literals
// and the backend decodes through the function below.
//
// The enum is NOT the GLenum: this header is pure (no MG_State, MG_Impl, MG_Backend or
// MG_Remote - scripts/check_include_closure.py probe "value-header") and a GL token is a
// frontend spelling. The GLenum <-> enumerator half stays at the one client site that has
// a GLenum in its hand.
enum class MGPipeImageAccess : Uint8 {
ReadOnly = 0, // GL_READ_ONLY
WriteOnly = 1, // GL_WRITE_ONLY
ReadWrite = 2, // GL_READ_WRITE
Count = 3,
};
// 0 IS READ-ONLY AND THAT IS WHY THE ORDER IS THIS ONE, not the low byte of the GL tokens:
// a zeroed MGPImageView must decode to the most restrictive access, so a record that forgot
// to set the field can only ever lose a write, never invent one.
inline constexpr Uint8 kMGPipeImageAccessReadOnly = static_cast<Uint8>(MGPipeImageAccess::ReadOnly);
inline constexpr Uint8 kMGPipeImageAccessWriteOnly = static_cast<Uint8>(MGPipeImageAccess::WriteOnly);
inline constexpr Uint8 kMGPipeImageAccessReadWrite = static_cast<Uint8>(MGPipeImageAccess::ReadWrite);
// The decoder's own precondition. Any other value is Fatal{ProtocolCorruption,
// "ImageView.Access"} at the READER - this header may not log, so it answers the question
// and the caller owns the refusal.
inline constexpr Bool MGPipeImageAccessIsValid(Uint8 encoded) {
return encoded < static_cast<Uint8>(MGPipeImageAccess::Count);
}
inline constexpr MGPipeImageAccess MGPipeDecodeImageAccess(Uint8 encoded) {
return static_cast<MGPipeImageAccess>(encoded);
}
// "May the shader read / write through this binding." Stated here rather than at each
// reader because the image sweep's writable set and the backend's barrier plan ask the
// same question of the same three values.
inline constexpr Bool MGPipeImageAccessReads(MGPipeImageAccess access) {
return access == MGPipeImageAccess::ReadOnly || access == MGPipeImageAccess::ReadWrite;
}
inline constexpr Bool MGPipeImageAccessWrites(MGPipeImageAccess access) {
return access == MGPipeImageAccess::WriteOnly || access == MGPipeImageAccess::ReadWrite;
}
// ---- trip wires (P0.5). Sizes are what every ABI MobileGL ships on produces: every
// member is a fixed-width scalar, an enum of one, or an array of those - no pointer, no
// SizeT - except the vertex types, which carry SharedPtr<BufferObject> by design and are
+91
View File
@@ -1537,6 +1537,13 @@ namespace MobileGL::MG_Pipe {
PipeInputs::kBufferTargetCount,
"the record's per-target array and PipeInputs' have drifted");
MGPipeApplyAccess::SetContextValues(gPipeInputs, values);
// P5e (CONTRACT-P5E.md §2.1, escalation (i)): mirror the XFB-active flag into the
// applier state as well. The barriered predicate has to be computable on the SERVER
// from applier state alone - gPipeInputs is the block the run-ahead client stops
// filling, so a predicate that read the answer out of THERE would be reading the very
// memory the rule exists to stop it reading. One store, inert until the caps bit is
// published, and it is what makes the two roles' third clause the same clause.
g_applier.IsTransformFeedbackActive = values.IsTransformFeedbackActive != 0;
}
void MGPipeApplySetPatchState(const MGPPatchState& patch) {
@@ -2822,6 +2829,90 @@ namespace MobileGL::MG_Pipe {
MGPipeApplierNoteTextureStateMoved();
}
// ---------------------------------------------------------------------------------
// P5e's two entry points, DECLARED AND REFUSED HERE (MG_Remote/CONTRACT-P5E.md §1)
// ---------------------------------------------------------------------------------
//
// Package sb writes the first body and package pg the second. Until then a record that
// reached either one would be a binding set the server accepted and dropped, so the answer
// is a named abort rather than a quiet return: the picture would be wrong and the lane
// would be green, which is the one failure mode this campaign spends its refusals on.
//
// Neither is reachable today - no route row installs the table slot and the wire decoder's
// arms for both opcodes validate and decline (PipeCatalogueTest pins both slots null) - so
// this is a link-time seam, not a runtime one.
void MGPipeApplySetShaderBuffers(const MGPShaderBuffers& hdr, const MGPBufferRange* tail) {
(void)tail;
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_shader_buffers\"} - the applier entry point "
"is declared by P5e package c0e and bodied by package sb; a record for class %u "
"with %u range(s) reached it, which means a route was installed ahead of its "
"consumer",
hdr.Class, hdr.Count);
std::abort();
}
void MGPipeApplySetProgramBindings(const MGPProgramBindings& hdr, const Int32* blockBindings,
const MGPProgramSamplerUnit* samplerUnits,
const MGPProgramStorageOverride* storageOverrides,
const char* const* storageOverrideNames) {
(void)blockBindings;
(void)samplerUnits;
(void)storageOverrides;
(void)storageOverrideNames;
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_program_bindings\"} - the applier entry point "
"is declared by P5e package c0e and bodied by package pg; a record for shader CSO "
"{%u, %u} with %u/%u/%u tail entries reached it, which means a route was installed "
"ahead of its consumer",
hdr.Cso.Slot, hdr.Cso.Gen, hdr.BlockBindingCount, hdr.SamplerUnitCount,
hdr.StorageOverrideCount);
std::abort();
}
// ---------------------------------------------------------------------------------
// P5e: the barriered predicate and the applier's current-record flag (§2.1, §4.4)
// ---------------------------------------------------------------------------------
Bool MGPipeBarriered(MGPWireOp op, const void* payload, const MGPipeApplierState& st) {
// 1. The static column. Every reply row, present, and the rows whose apply still pulls
// a BARRIER_PULLED field or probes the client allocator.
if (MGPipeWaitClassFor(op) != kWaitNone) return true;
// 2. Escalation (i): an open transform-feedback span makes every verb inside it
// barriered. XFB is not migrated by P5e - StartPendingTransformFeedback holds
// frontend SharedPtrs across two verbs, which rule C forbids an applier entry point
// outright - so the span keeps P5C's semantics and the cost on the steady Espryt
// path is one bool test per draw.
if (MGPipeCallClassFor(op) == kCtxVerb && st.IsTransformFeedbackActive) return true;
// 3. Escalation (ii): a draw whose enabled attributes include a client-memory array.
// The server uploads those bytes by dereferencing a raw client pointer, so the draw
// cannot run ahead. Under run-ahead the client refuses it before it is published
// (§5.1), so this clause is what the LOCKSTEP arm - where the draw is legal - uses.
if (op == MGPWireOp::DrawVbo && payload != nullptr) {
const auto& draw = *static_cast<const MGPDrawInfo*>(payload);
if ((draw.Flags & static_cast<Uint8>(kDrawClientArrays)) != 0) return true;
}
return false;
}
// Server-private and deliberately NOT a member of MGPipeApplierState: it is not per-context
// state the applier owns, it is "which record is the apply thread inside right now", and 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.
//
// TRUE IS THE DEFAULT AND THE LOCKSTEP ANSWER. On a server that does not publish
// kCapRunAheadApply every record is barriered, the client is parked in its own wait for
// each of them, and the two named exemption scopes of CONTRACT-P5C §3.1 keep working
// exactly as they do today. Nothing writes this until package id sets it from
// MGPipeBarriered in the sink's ApplyOne.
namespace {
thread_local Bool g_currentRecordBarriered = true;
} // namespace
Bool MGPipeApplierCurrentRecordIsBarriered() { return g_currentRecordBarriered; }
void MGPipeApplierSetCurrentRecordBarriered(Bool barriered) {
g_currentRecordBarriered = barriered;
}
// ================================================================================
// w3: the shader CSO, the two program bindings and the default uniform block
//
+153
View File
@@ -11,6 +11,10 @@
#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
@@ -367,6 +371,16 @@ namespace MobileGL::MG_Pipe {
// 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;
@@ -377,6 +391,31 @@ namespace MobileGL::MG_Pipe {
Vector<Uint8> GlobalConstants;
Uint64 GlobalConstantsSerial = 0;
Uint64 Serial = 0;
// ---- 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
@@ -693,6 +732,42 @@ namespace MobileGL::MG_Pipe {
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.
Uint32 ShaderBufferWritableMask[kMGPipeShaderBufferClassCount] = {0, 0, 0};
// 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
@@ -782,6 +857,53 @@ namespace MobileGL::MG_Pipe {
// 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);
// 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
@@ -1145,6 +1267,37 @@ namespace MobileGL::MG_Pipe {
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
+136 -84
View File
@@ -11,11 +11,42 @@
// (scripts/gen_pipe.py -> MG_Pipe/generated/*.inc) and one unit test
// (MG_Test/Pipe/PipeCatalogueTest.cpp) pins the arithmetic.
//
// X(Name, PayloadStruct, Class, Flags)
// X(Name, PayloadStruct, Class, Flags, WaitClass)
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
// kScreen lands in struct MGPipeScreen, every other class in struct
// MGPipeContext (plan section 4.3).
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
// WaitClass : kWaitNone | kWaitApplied | kWaitReply | kWaitPresent
// P5e (MG_Remote/CONTRACT-P5E.md section 2.2). WHAT THE CLIENT DOES AFTER IT HAS
// PUBLISHED A RECORD OF THIS CALL, on a server that publishes kCapRunAheadApply.
// It is A VALUE AND NOT A NEW OP: the opcode order and the flag column are
// untouched, and gen_pipe.py emits the column beside kMGPipeCallFlags as
// MGPipeWaitClassFor(op) in generated/PipeWire.inc, so the emit table and the
// sink compute the barriered predicate from one table instead of two copies.
//
// kWaitReply the answer is not derivable and the client acts on it (R-5), so
// it blocks on the reply slot. EVERY kReplySlot ROW CARRIES IT and
// gen_pipe.py refuses a row where the two disagree - a reply-slot
// row that did not wait would be a waiter that hangs, and a waiting
// row with no slot would block on an answer nobody posts.
// resource_subdata is here because the row OWNS a slot; the buffer
// half's wantReply=false is a per-record ROUTE decision one level
// down (CONTRACT-P5E.md section 2.5), not a second wait class.
// kWaitPresent present, whose pacing is the present CREDIT taken BEFORE the
// encode rather than a wait after the publish (section 2.4).
// kWaitApplied the record's apply still reads a BARRIER_PULLED row or probes the
// client allocator, so the client parks in its own wait and the row
// keeps CONTRACT-P5C's semantics exactly: its pulls count into rsp
// and its probes are legal inside a scope. This column is the
// static half of MGPipeBarriered(op, payload, applierState); the
// other two escalations (an open XFB span, a draw with client
// vertex arrays) are payload-derived and live in that function.
// kWaitNone rule F holds for this row's sink: its apply names no client
// memory at all, so the client publishes and moves on.
//
// THE COLUMN IS INERT UNTIL THE INTEGRATION COMMIT publishes kCapRunAheadApply
// (MG_Backend/Init.cpp's kMGPipeP5eRunAheadReady): with the bit unpublished the
// client runs today's lockstep path and never reads the table.
// kNeedsAck on a call means records of this call MAY require an ack; a
// per-record predicate decides. resource_respecify carries it for
// glBufferStorage - a real synchronous allocation, and the only entry point
@@ -45,15 +76,15 @@
// appended server-side fence wait 1 and P5c's applier_reset 1
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
// kCtxCso 13 CSO create/bind/delete
// kCtxState 18 16 of the 18 set_* calls + the temporary set_residual_value_state
// + P5c rv's set_context_values
// kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state
// + P5c rv's set_context_values + P5e's set_program_bindings
// kCtxObject 10 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus
// P5c's object_death 1
// kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five
// P5b-appended verbs (bind_shader_image, patch_parameter,
// bind_stream_output, set_storage_block_binding,
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
// total 79
// total 80
//
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
// do not add up to a set of UNIQUE records and this file has to hold unique records:
@@ -101,9 +132,17 @@
// routed through gMGPipeContext like its seventeen siblings, and under monolith it has no
// producer at all - the fields keep coming through the residual fill, byte for byte as
// before (G1). 79 unique records.
// - P5e (MG_Remote/CONTRACT-P5E.md section 1) appended ONE more (opcode 80):
// set_program_bindings, the three POST-LINK mutable reflection fields - the uniform-block
// bindings, the sampler uniform units and the name-keyed storage-block override map. They
// live inside the LinkArtifacts the reflection archive snapshots, and all three move AFTER
// the link, so no existing row can carry them: create_shader_state describes the link and
// this one describes what happened to it since. It lands with a NULL ROUTE and a sink that
// refuses it by name until package pg wires the emitter, exactly as set_shader_buffers has
// sat catalogued-and-dead since P4a. 80 unique records.
// ---------------------------------------------------------------------------------------
#define MGP_CALL_LIST_DOCUMENTED_COUNT 79
#define MGP_CALL_LIST_DOCUMENTED_COUNT 80
// clang-format off
#define MGP_CALL_LIST(X) \
@@ -112,7 +151,7 @@
/* RendererInfo (MGPipeTypes.h). It carried none until P5 R-13.1; the flag is a */ \
/* property of the payload, so its absence was simply wrong. Adding a flag does not */ \
/* move an opcode: the opcode is this line's 1-based position. */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob) \
X(GetCaps, MGPCaps, kScreen, kReplySlot|kHasBlob, kWaitReply) \
/* THE FOUR ACCEPTANCE ROWS (P5 R-5, R-16). These four and map_persistent are the only */ \
/* applier entry points that RETURN something the client acts on destructively - clearing */ \
/* per-level dirty flags, latching parameters, advancing its own bookkeeping - so the */ \
@@ -120,7 +159,7 @@
/* until P5, because in monolith the answer is the return value of a direct call and there */ \
/* was nothing to declare. kMGPipeCallFlags is now what sizes the reply pool and what the */ \
/* decoder posts against, so a row that answers without the flag is a waiter that hangs. */ \
X(ResourceCreate, MGPResourceDesc, kScreen, kReplySlot) \
X(ResourceCreate, MGPResourceDesc, kScreen, kReplySlot, kWaitReply) \
/* NO kHasBlob, BY RULING (P5 R-13.3; CONTRACT-P5.md table 1 row 19). MGPResourceDesc */ \
/* owns no MGPBlobRef and gains none: under split `initialBytes` is always nullptr and */ \
/* the initial content of a glBufferData(size, data) / glTexImage*(..., data) arrives */ \
@@ -129,65 +168,65 @@
/* cost is one extra record. NOTE the SECOND uncarried companion on this call, the */ \
/* MGPRespecifiedLevel* scope (PipeApply.h:792) - it is NOT bytes and NOT covered by */ \
/* this ruling; table 1 row 19 gives it the two pad fields of MGPResourceDesc. */ \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck|kReplySlot) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional) \
X(FenceCreate, MGPHandleOnly, kScreen, kNone) \
X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot) \
X(FenceWait, MGPFenceWait, kScreen, kReplySlot) \
X(FenceDestroy, MGPHandleOnly, kScreen, kNone) \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNeedsAck|kReplySlot, kWaitReply) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone, kWaitNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional, kWaitReply) \
X(UnmapPersistent, MGPHandleOnly, kScreen, kOptional, kWaitNone) \
X(FenceCreate, MGPHandleOnly, kScreen, kNone, kWaitNone) \
X(FenceStatus, MGPHandleOnly, kScreen, kReplySlot, kWaitReply) \
X(FenceWait, MGPFenceWait, kScreen, kReplySlot, kWaitReply) \
X(FenceDestroy, MGPHandleOnly, kScreen, kNone, kWaitNone) \
/* ---- context: query objects (plan 4.3 gives the namespace to the context) ---- */ \
X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot) \
X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot) \
X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone) \
X(QueryCreate, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \
X(QueryBegin, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \
X(QueryEnd, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \
X(QueryAvailable, MGPHandleOnly, kCtxQuery, kReplySlot, kWaitReply) \
X(QueryResult, MGPQueryResultRequest, kCtxQuery, kReplySlot, kWaitReply) \
X(QueryDestroy, MGPHandleOnly, kCtxQuery, kNone, kWaitNone) \
/* ---- context: CSO create/bind/delete (plan 4.4.2, 4.5.2-4.5.5) ---- */ \
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \
X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob) \
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone) \
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob, kWaitNone) \
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone, kWaitNone) \
X(DeleteRenderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
X(CreateVertexElements, MGPVertexElements, kCtxCso, kHasBlob, kWaitNone) \
X(BindVertexElements, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
X(DeleteVertexElements, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
/* kHasBlob because MGPSamplerDesc owns an MGPBlobRef member, `Parameters` - the */ \
/* SamplerParameters POD byte for byte, borderColorForm included (P5 R-13.1). The */ \
/* companion pointer beside the record today is a TYPED frontend pointer */ \
/* (const SamplerParameters*), which is exactly what split may not carry. */ \
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob) \
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone) \
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone) \
X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob) \
X(BindShaderState, MGPHandleOnly, kCtxCso, kNone) \
X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone) \
X(CreateSamplerState, MGPSamplerDesc, kCtxCso, kHasBlob, kWaitNone) \
X(DeleteSamplerState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
X(CreateSamplerView, MGPSamplerView, kCtxCso, kNone, kWaitNone) \
X(DeleteSamplerView, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
X(CreateShaderState, MGPProgramDesc, kCtxCso, kHasBlob, kWaitNone) \
X(BindShaderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
X(DeleteShaderState, MGPHandleOnly, kCtxCso, kNone, kWaitNone) \
/* ---- context: set_* (plan 4.4.3) ---- */ \
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \
X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail) \
X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone) \
X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone) \
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \
X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail) \
X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail) \
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \
X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail) \
X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob) \
X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail) \
X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone) \
X(SetPatchState, MGPPatchState, kCtxState, kNone) \
X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone) \
X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone) \
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob, kWaitNone) \
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone, kWaitNone) \
X(SetVertexBuffers, MGPVertexBuffers, kCtxState, kVarTail, kWaitNone) \
X(SetIndexBuffer, MGPIndexBuffer, kCtxState, kNone, kWaitNone) \
X(SetIndirectBuffers, MGPIndirectBuffers, kCtxState, kNone, kWaitNone) \
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail, kWaitNone) \
X(BindSamplerStates, MGPSamplerStates, kCtxState, kVarTail, kWaitNone) \
X(SetShaderImages, MGPShaderImages, kCtxState, kVarTail, kWaitNone) \
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan, kWaitNone) \
X(SetStreamOutputTargets, MGPStreamOutputTargets, kCtxState, kVarTail, kWaitNone) \
X(SetGlobalConstants, MGPGlobalConstants, kCtxState, kHasBlob, kWaitNone) \
X(SetVertexAttribDefaults, MGPVertexAttribDefaults, kCtxState, kVarTail, kWaitNone) \
X(SetPixelPackState, MGPPixelPackState, kCtxState, kNone, kWaitNone) \
X(SetPatchState, MGPPatchState, kCtxState, kNone, kWaitNone) \
X(SetDrawProgram, MGPHandleOnly, kCtxState, kNone, kWaitNone) \
X(SetDispatchProgram, MGPHandleOnly, kCtxState, kNone, kWaitNone) \
/* Migration-only carrier for Track V, retired field by field across P2..P13. Its */ \
/* retirement is a compile error: MGL_RESIDUAL_BLOCK_SIZE only ever goes DOWN and the */ \
/* final step asserts sizeof(ResidualValueBlock) == 0 (plan 6.3). */ \
X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob) \
X(SetResidualValueState, MGPResidualValueState, kCtxState, kHasBlob, kWaitNone) \
/* ---- context: per-object state and transfer (plan 4.4.3 set_texture_params, 4.4.4) ---- */ \
X(SetTextureParams, MGPTextureParams, kCtxObject, kReplySlot) \
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail|kReplySlot) \
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional) \
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone) \
X(SetTextureParams, MGPTextureParams, kCtxObject, kReplySlot, kWaitReply) \
X(ResourceSubData, MGPSubData, kCtxObject, kHasBlob|kVarTail|kReplySlot, kWaitReply) \
X(BufferSubDataResident, MGPSubData, kCtxObject, kHasBlob|kOptional, kWaitNone) \
X(ResourceSubDataComplete, MGPSubDataComplete, kCtxObject, kNone, kWaitNone) \
/* NO kHasBlob AND NO MGPBlobRef, BY RULING (P5 R-13.2; CONTRACT-P5.md table 1 row 21). */ \
/* It takes a content pointer today, but under split it CARRIES NO BYTES AT ALL: it is */ \
/* a {range, AccessFlags} control record, and the bytes of [Offset, Offset+Size) arrive */ \
@@ -195,36 +234,36 @@
/* drives rewrites its range "from the authoritative shadow" (Managers.cpp:1047-1076), */ \
/* and under split the authoritative shadow is server-owned (R-11), so subdata is the */ \
/* only way bytes reach it - a blobref here would be a SECOND way to say the same thing. */ \
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone) \
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot) \
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone) \
X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone) \
X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot) \
X(ResourceFlushRange, MGPFlushRange, kCtxObject, kNone, kWaitNone) \
X(ResourceReadback, MGPReadback, kCtxObject, kReplySlot, kWaitReply) \
X(ResourceCopyRegion, MGPCopyRegion, kCtxObject, kNone, kWaitNone) \
X(GenerateMipmap, MGPMipPlan, kCtxObject, kNone, kWaitApplied) \
X(GetTextureImage, MGPReadbackInfo, kCtxObject, kReplySlot, kWaitReply) \
/* ---- context: transfer calls that read whole-context state, and the commands ---- */ \
X(Blit, MGPBlit, kCtxVerb, kNone) \
X(Clear, MGPClear, kCtxVerb, kNone) \
X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot) \
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \
X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone) \
X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone) \
X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone) \
X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone) \
X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone) \
X(Flush, MGPFlush, kCtxVerb, kNone) \
X(Present, MGPPresent, kCtxVerb, kNone) \
X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional) \
X(Blit, MGPBlit, kCtxVerb, kNone, kWaitNone) \
X(Clear, MGPClear, kCtxVerb, kNone, kWaitNone) \
X(ReadPixels, MGPReadbackInfo, kCtxVerb, kReplySlot, kWaitReply) \
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail, kWaitNone) \
X(LaunchGrid, MGPGridInfo, kCtxVerb, kNone, kWaitNone) \
X(MemoryBarrier, MGPMemoryBarrier, kCtxVerb, kNone, kWaitNone) \
X(BeginStreamOutput, MGPStreamOutputBegin, kCtxVerb, kNone, kWaitApplied) \
X(EndStreamOutput, MGPXfbAccounting, kCtxVerb, kNone, kWaitApplied) \
X(PauseStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \
X(ResumeStreamOutput, MGPStreamOutputControl, kCtxVerb, kNone, kWaitApplied) \
X(Flush, MGPFlush, kCtxVerb, kNone, kWaitNone) \
X(Present, MGPPresent, kCtxVerb, kNone, kWaitPresent) \
X(SetSwapInterval, MGPSwapInterval, kCtxVerb, kOptional, kWaitNone) \
/* ---- APPENDED. Opcodes are positional, so a late arrival goes at the END, never into ---- */ \
/* ---- its group: three live GLFunctionsTable entries the catalogue had no carrier for. ---- */ \
/* glGetInteger64v(GL_TIMESTAMP) - GetGpuTimestampNs, a synchronous server answer, which */ \
/* the reply slot carries. The query namespace is the context's (plan 4.3). */ \
X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot) \
X(QueryTimestamp, MGPTimestampRequest, kCtxQuery, kReplySlot, kWaitReply) \
/* glQueryCounter(GL_TIMESTAMP) - QueryCounterTimestamp, a one-shot stamp into a query */ \
/* object, NOT a begin/end pair. Kind carries GL_TIMESTAMP. */ \
X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone) \
X(QueryCounter, MGPQueryDesc, kCtxQuery, kNone, kWaitNone) \
/* glWaitSync - WaitSync, the GPU-side wait, distinct from FenceWait's client-side one. */ \
/* TimeoutNs is GL_TIMEOUT_IGNORED by contract. */ \
X(FenceWaitServer, MGPFenceWait, kScreen, kNone) \
X(FenceWaitServer, MGPFenceWait, kScreen, kNone, kWaitNone) \
/* ---- APPENDED BY P5b (MG_Remote/CONTRACT-P5B.md): five GLFunctionsTable verbs the ---- */ \
/* ---- class-C census measured as first blockers and that no existing row could carry. ---- */ \
/* ---- Opcodes 72..76. Every one reaches WireVerbSink, none has an MGPipeApply* entry ---- */ \
@@ -232,18 +271,18 @@
/* ---- directly, as today. The payload comments in MGPipeTypes.h say why each exists. ---- */ \
/* glBindImageTexture - Espryt syncs the unit's binding to the driver AT THE CALL */ \
/* (SyncImageTextureBinding), so the validate-time set_shader_images cannot stand in. */ \
X(BindShaderImage, MGPImageBind, kCtxVerb, kNone) \
X(BindShaderImage, MGPImageBind, kCtxVerb, kNone, kWaitNone) \
/* glPatchParameteri(GL_PATCH_VERTICES) - Espryt pushes glPatchParameteri at the call. */ \
X(PatchParameter, MGPPatchParameter, kCtxVerb, kNone) \
X(PatchParameter, MGPPatchParameter, kCtxVerb, kNone, kWaitNone) \
/* glBindTransformFeedback - the driver object rebind and the capture-binding shadow */ \
/* invalidation happen at the call; the XFB namespace has no wire lifetime. */ \
X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone) \
X(BindStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitApplied) \
/* glShaderStorageBlockBinding - the block NAME is the coordinate all three sides share */ \
/* (BackendObject.h), and a name is bytes: kHasBlob, staged whole in SEG_STAGE, NUL included. */ \
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob, kWaitApplied) \
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
/* which resource_copy_region (resource -> resource) cannot express. */ \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone) \
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone, kWaitApplied) \
/* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \
/* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \
/* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \
@@ -255,14 +294,14 @@
/* against every verb that follows. ContextSerial is the client context's make-current */ \
/* serial at the edge - ASSERTED against the session's, never dispatched on (P5c: one */ \
/* context per session). */ \
X(ApplierReset, MGPApplierReset, kScreen, kNone) \
X(ApplierReset, MGPApplierReset, kScreen, kNone, kWaitApplied) \
/* A frontend state object died. The record carries the HANDLE (and kind) the client's own */ \
/* allocator resolves for it - nothing is emitted for an object that never crossed (the */ \
/* server never saw it, so there is no twin to kill) - and the sink releases the kind's */ \
/* twin table by handle. The framebuffer family's FIRST wire delete opcode: its death used */ \
/* to hop to the apply thread through a stack-struct mailbox and a lifetime-id probe of */ \
/* the client's allocator, both rule-E surfaces. */ \
X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone) \
X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone, kWaitNone) \
/* ---- P5c rv (CONTRACT-P5C.md §5.3), opcode 79: the residual-value record. One POD that ---- */ \
/* ---- carries every value-class BARRIER_PULLED field left after the set_* catalogue: the ---- */ \
/* ---- two texture-unit counters, the 15 per-target touched-buffer-binding counts and the ---- */ \
@@ -270,7 +309,20 @@
/* ---- (whole-record hash suppression, D11's shape), ACTIVE ONLY with an active transport ---- */ \
/* ---- (PipeFill.cpp gates both the emission and the residual-fill skip on it): under ---- */ \
/* ---- monolith there is no producer and the fields keep being pulled, byte for byte (G1). ---- */ \
X(SetContextValues, MGPContextValues, kCtxState, kNone)
X(SetContextValues, MGPContextValues, kCtxState, kNone, kWaitNone) \
/* ---- P5e (MG_Remote/CONTRACT-P5E.md §1), opcode 80: the post-link binding record. ------ */ \
/* ---- THREE POST-LINK MUTABLE REFLECTION FIELDS LIVE INSIDE THE FRONTEND'S LinkArtifacts ---- */ \
/* ---- and therefore inside the archive create_shader_state carries - but glUniformBlock- ---- */ \
/* ---- Binding, glUniform1i on a sampler and glShaderStorageBlockBinding all move one ---- */ \
/* ---- AFTER the link. So an archive snapshot alone answers a draw with the bindings the ---- */ \
/* ---- program was LINKED with rather than the ones it is BOUND with, and the server's ---- */ \
/* ---- twin rebuilds against the wrong uniform-block points. Three tails, three index ---- */ \
/* ---- spaces (dense block index, sparse uniform location, and a NAME span for the storage ---- */ \
/* ---- override map, which is name-keyed by design). Emitted BEFORE create_shader_state at ---- */ \
/* ---- the same validate point: a rebuild inside the verb must already see the bindings. ---- */ \
/* ---- kWaitNone: nothing about it is read back, and under run-ahead a kCtxState row that ---- */ \
/* ---- names only handles and values is exactly what rule F admits. ---- */ \
X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone)
// clang-format on
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
+15 -1
View File
@@ -93,7 +93,7 @@
#define MGP_FIELDS_MGPProgramDesc(F) \
F(Cso) F(StageMask) F(GlobalUboSize) F(ReservedNumSamplesOffset) F(SpirvStatus) F(NativeFloat64) \
F(PointSizeDemoted) F(EnableSpirvValidation) F(Spirv) F(Reflection)
F(PointSizeDemoted) F(EnableSpirvValidation) F(LinkStatus) F(Spirv) F(Reflection)
// P4a, ID-12 / esprytobj DV-5: Pad0 became Uint16 TextureTarget. Same trip wire as
// MGPFramebufferState's Target below - PADDING_MEMBER_RE only excludes a member still NAMED
@@ -145,6 +145,19 @@
#define MGP_FIELDS_MGPStreamOutputTargets(F) \
F(Count) F(Generation) F(ContentHash)
// P5e (MG_Remote/CONTRACT-P5E.md §1): set_program_bindings and its two tail element types. The
// third tail's element embeds an MGHostSpan, which has a list of its own below, so the
// comparator sees the NAME SPAN's four members rather than memcmp'ing a struct with a pointer
// and a pad word in it - the exact false-difference MGPSamplerDesc's blob used to produce.
#define MGP_FIELDS_MGPProgramBindings(F) \
F(Cso) F(Signature) F(BlockBindingCount) F(SamplerUnitCount) F(StorageOverrideCount)
#define MGP_FIELDS_MGPProgramSamplerUnit(F) \
F(Location) F(Unit)
#define MGP_FIELDS_MGPProgramStorageOverride(F) \
F(Name) F(Binding)
#define MGP_FIELDS_MGPGlobalConstants(F) \
F(ShaderCso) F(Version) F(Blob)
@@ -394,6 +407,7 @@
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
P(MGPCopyFromFramebuffer) \
P(MGPApplierReset) P(MGPContextValues) \
P(MGPProgramBindings) P(MGPProgramSamplerUnit) P(MGPProgramStorageOverride) \
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
P(StencilFaceState) \
P(DynamicBackendParameters) P(MGHostSpan) \
@@ -106,9 +106,9 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] =
"-", // GetBlendEquationIndexed
"-", // GetBlendFuncIndexed
"-", // GetBoundTransformFeedbackName
"P8", // GetBoundVertexArray
"P5e (Espryt unbarriered), P7 (Magma)", // GetBoundVertexArray
"P8 (indirect), P9 (readback), P13 (transfer)", // GetBufferBindingSlot
"P3b/P4b, P7", // GetBufferBindingPoint
"P5e (Espryt unbarriered), P7 (Magma)", // GetBufferBindingPoint
"P7/P13", // GetBufferBindingPointCount
"-", // GetTouchedBufferBindingPointCount
"-", // GetClampReadColor
@@ -121,8 +121,8 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] =
"-", // GetDepthFunc
"-", // GetDepthMask
"-", // GetDepthRangeIndexed
"P3b/P4b (Espryt), P7 (Magma)", // GetFramebufferBindingSlot
"P3b/P4b, P7", // GetImageTextureBinding
"P5e (Espryt unbarriered), P7 (Magma)", // GetFramebufferBindingSlot
"P5e (Espryt unbarriered), P7 (Magma)", // GetImageTextureBinding
"-", // GetLineWidth
"-", // GetLogicOp
"-", // GetMaxTouchedTextureUnit
@@ -136,8 +136,8 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] =
"-", // GetPolygonOffsetFactor
"-", // GetPolygonOffsetUnits
"-", // GetPrimitiveRestartIndex
"P7 (Magma), P8 (Espryt)", // GetProgramForDispatch
"P8 (Espryt), P7 (Magma)", // GetProgramForDraw
"P5e (Espryt unbarriered), P7 (Magma)", // GetProgramForDispatch
"P5e (Espryt unbarriered), P7 (Magma)", // GetProgramForDraw
"P9", // GetProgramObject
"-", // GetProvokingVertexMode
"-", // GetRenderStateParameters
@@ -148,7 +148,7 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] =
"-", // GetTextureBindGeneration
"-", // GetTextureContextId
"P7", // GetTextureObject
"P3b/P4b, P7", // GetTextureUnitObject
"P5e (Espryt unbarriered), P7 (Magma)", // GetTextureUnitObject
"-", // GetTransformFeedbackCapturedVertices
"-", // GetTransformFeedbackGeneration
"-", // GetTransformFeedbackPausedPrimitiveCounter
+4 -2
View File
@@ -315,6 +315,7 @@ enum class MGPipeFieldEmitter : Uint8 {
SetFramebufferState,
SetPatchState,
SetSamplerViews,
SetShaderBuffers,
SetShaderImages,
SetVertexAttribDefaults,
};
@@ -331,6 +332,7 @@ inline constexpr const char* kMGPipeFieldEmitterNames[] = {
"SetFramebufferState",
"SetPatchState",
"SetSamplerViews",
"SetShaderBuffers",
"SetShaderImages",
"SetVertexAttribDefaults",
};
@@ -343,7 +345,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
MGPipeFieldEmitter::kNone, // GetBoundTransformFeedbackName
MGPipeFieldEmitter::BindVertexElements, // GetBoundVertexArray
MGPipeFieldEmitter::kNone, // GetBufferBindingSlot
MGPipeFieldEmitter::kNone, // GetBufferBindingPoint
MGPipeFieldEmitter::SetShaderBuffers, // GetBufferBindingPoint
MGPipeFieldEmitter::kNone, // GetBufferBindingPointCount
MGPipeFieldEmitter::SetContextValues, // GetTouchedBufferBindingPointCount
MGPipeFieldEmitter::SetDynamicState, // GetClampReadColor
@@ -400,7 +402,7 @@ inline constexpr MGPipeFieldEmitter kMGPipeFieldEmittedBy[kMGPipeInputFieldCount
MGPipeFieldEmitter::SetContextValues, // GetBoundTransformFeedbackLifetimeId
MGPipeFieldEmitter::kNone, // HasOpenTransformFeedbackSpan
};
inline constexpr SizeT kMGPipeEmittedFieldCount = 47;
inline constexpr SizeT kMGPipeEmittedFieldCount = 48;
struct MGPipeFilledState {
Uint64 CurrentVerbSerial;
+4 -3
View File
@@ -29,7 +29,7 @@ struct MGPipeScreen {
void (*ApplierReset)(const MGPApplierReset* payload);
};
// context: 67 calls. A null entry means the backend does not implement this
// context: 68 calls. A null entry means the backend does not implement this
// call and the frontend keeps its own path (plan B section 4.1).
struct MGPipeContext {
void (*QueryCreate)(const MGPQueryDesc* payload);
@@ -99,11 +99,12 @@ struct MGPipeContext {
void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload);
void (*ObjectDeath)(const MGPHandleOnly* payload);
void (*SetContextValues)(const MGPContextValues* payload);
void (*SetProgramBindings)(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount);
};
inline constexpr SizeT kMGPipeScreenCallCount = 12;
inline constexpr SizeT kMGPipeContextCallCount = 67;
inline constexpr SizeT kMGPipeCallCount = 79;
inline constexpr SizeT kMGPipeContextCallCount = 68;
inline constexpr SizeT kMGPipeCallCount = 80;
// A table that is not exactly its call count of function pointers has grown a
// member that no generator knows about.
@@ -332,3 +332,7 @@ inline void MGP_ObjectDeath(const MGPHandleOnly* payload) {
inline void MGP_SetContextValues(const MGPContextValues* payload) {
gMGPipeContext.SetContextValues(payload);
}
inline void MGP_SetProgramBindings(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount) {
gMGPipeContext.SetProgramBindings(payload, varTail, varTailCount);
}
+25 -1
View File
@@ -114,6 +114,9 @@ inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockB
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField);
inline Bool MGPipeVerify(const MGPProgramStorageOverride& a, const MGPProgramStorageOverride& b, const char** outField);
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
@@ -265,6 +268,12 @@ struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPContextValues> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramBindings> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramSamplerUnit> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<MGPProgramStorageOverride> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
template <>
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
@@ -678,6 +687,21 @@ inline Bool MGPipeVerify(const MGPContextValues& a, const MGPContextValues& b, c
return true;
}
inline Bool MGPipeVerify(const MGPProgramBindings& a, const MGPProgramBindings& b, const char** outField) {
MGP_FIELDS_MGPProgramBindings(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPProgramSamplerUnit& a, const MGPProgramSamplerUnit& b, const char** outField) {
MGP_FIELDS_MGPProgramSamplerUnit(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const MGPProgramStorageOverride& a, const MGPProgramStorageOverride& b, const char** outField) {
MGP_FIELDS_MGPProgramStorageOverride(MGP_VERIFY_FIELD)
return true;
}
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
return true;
@@ -725,4 +749,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
#undef MGP_VERIFY_FIELD
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 79;
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 82;
+135 -1
View File
@@ -149,7 +149,8 @@ enum class MGPWireOp : Uint16 {
ApplierReset = 77,
ObjectDeath = 78,
SetContextValues = 79,
kOpCount = 80,
SetProgramBindings = 80,
kOpCount = 81,
};
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
@@ -250,6 +251,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
/* 77 ApplierReset */ static_cast<Uint32>(kNone),
/* 78 ObjectDeath */ static_cast<Uint32>(kNone),
/* 79 SetContextValues */ static_cast<Uint32>(kNone),
/* 80 SetProgramBindings */ static_cast<Uint32>(kVarTail | kHostSpan),
};
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -349,6 +351,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast<SizeT>(MGPWireOp::kOpCount
/* 77 ApplierReset */ static_cast<Uint8>(kScreen),
/* 78 ObjectDeath */ static_cast<Uint8>(kCtxObject),
/* 79 SetContextValues */ static_cast<Uint8>(kCtxState),
/* 80 SetProgramBindings */ static_cast<Uint8>(kCtxState),
};
static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
@@ -361,6 +364,126 @@ inline constexpr MGPipeCallClass MGPipeCallClassFor(MGPWireOp op) {
: kCallClassCount;
}
// P5e (MG_Remote/CONTRACT-P5E.md 2.2): THE WAIT CLASS OF EVERY OPCODE, indexed the same way.
// Index 0 is kWaitClassCount - never a real class - for the flags table's reason: the one
// caller that can pass an opcode this catalogue never produced is a decoder holding bytes off
// a stream, and it must reach its own Fatal{ProtocolCorruption} rather than a wait decision.
//
// This is the STATIC HALF of MGPipeBarriered(op, payload, applierState), the predicate the emit
// table and the sink compute identically. The other half is payload-derived (an open
// transform-feedback span, a draw carrying client vertex arrays) and lives in that function,
// because it depends on state a table indexed by opcode cannot see.
inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kWaitClassCount),
/* 1 GetCaps */ static_cast<Uint8>(kWaitReply),
/* 2 ResourceCreate */ static_cast<Uint8>(kWaitReply),
/* 3 ResourceRespecify */ static_cast<Uint8>(kWaitReply),
/* 4 ResourceDestroy */ static_cast<Uint8>(kWaitNone),
/* 5 MapPersistent */ static_cast<Uint8>(kWaitReply),
/* 6 UnmapPersistent */ static_cast<Uint8>(kWaitNone),
/* 7 FenceCreate */ static_cast<Uint8>(kWaitNone),
/* 8 FenceStatus */ static_cast<Uint8>(kWaitReply),
/* 9 FenceWait */ static_cast<Uint8>(kWaitReply),
/* 10 FenceDestroy */ static_cast<Uint8>(kWaitNone),
/* 11 QueryCreate */ static_cast<Uint8>(kWaitNone),
/* 12 QueryBegin */ static_cast<Uint8>(kWaitNone),
/* 13 QueryEnd */ static_cast<Uint8>(kWaitNone),
/* 14 QueryAvailable */ static_cast<Uint8>(kWaitReply),
/* 15 QueryResult */ static_cast<Uint8>(kWaitReply),
/* 16 QueryDestroy */ static_cast<Uint8>(kWaitNone),
/* 17 CreateRenderState */ static_cast<Uint8>(kWaitNone),
/* 18 BindRenderState */ static_cast<Uint8>(kWaitNone),
/* 19 DeleteRenderState */ static_cast<Uint8>(kWaitNone),
/* 20 CreateVertexElements */ static_cast<Uint8>(kWaitNone),
/* 21 BindVertexElements */ static_cast<Uint8>(kWaitNone),
/* 22 DeleteVertexElements */ static_cast<Uint8>(kWaitNone),
/* 23 CreateSamplerState */ static_cast<Uint8>(kWaitNone),
/* 24 DeleteSamplerState */ static_cast<Uint8>(kWaitNone),
/* 25 CreateSamplerView */ static_cast<Uint8>(kWaitNone),
/* 26 DeleteSamplerView */ static_cast<Uint8>(kWaitNone),
/* 27 CreateShaderState */ static_cast<Uint8>(kWaitNone),
/* 28 BindShaderState */ static_cast<Uint8>(kWaitNone),
/* 29 DeleteShaderState */ static_cast<Uint8>(kWaitNone),
/* 30 SetDynamicState */ static_cast<Uint8>(kWaitNone),
/* 31 SetFramebufferState */ static_cast<Uint8>(kWaitNone),
/* 32 SetVertexBuffers */ static_cast<Uint8>(kWaitNone),
/* 33 SetIndexBuffer */ static_cast<Uint8>(kWaitNone),
/* 34 SetIndirectBuffers */ static_cast<Uint8>(kWaitNone),
/* 35 SetSamplerViews */ static_cast<Uint8>(kWaitNone),
/* 36 BindSamplerStates */ static_cast<Uint8>(kWaitNone),
/* 37 SetShaderImages */ static_cast<Uint8>(kWaitNone),
/* 38 SetShaderBuffers */ static_cast<Uint8>(kWaitNone),
/* 39 SetStreamOutputTargets */ static_cast<Uint8>(kWaitNone),
/* 40 SetGlobalConstants */ static_cast<Uint8>(kWaitNone),
/* 41 SetVertexAttribDefaults */ static_cast<Uint8>(kWaitNone),
/* 42 SetPixelPackState */ static_cast<Uint8>(kWaitNone),
/* 43 SetPatchState */ static_cast<Uint8>(kWaitNone),
/* 44 SetDrawProgram */ static_cast<Uint8>(kWaitNone),
/* 45 SetDispatchProgram */ static_cast<Uint8>(kWaitNone),
/* 46 SetResidualValueState */ static_cast<Uint8>(kWaitNone),
/* 47 SetTextureParams */ static_cast<Uint8>(kWaitReply),
/* 48 ResourceSubData */ static_cast<Uint8>(kWaitReply),
/* 49 BufferSubDataResident */ static_cast<Uint8>(kWaitNone),
/* 50 ResourceSubDataComplete */ static_cast<Uint8>(kWaitNone),
/* 51 ResourceFlushRange */ static_cast<Uint8>(kWaitNone),
/* 52 ResourceReadback */ static_cast<Uint8>(kWaitReply),
/* 53 ResourceCopyRegion */ static_cast<Uint8>(kWaitNone),
/* 54 GenerateMipmap */ static_cast<Uint8>(kWaitApplied),
/* 55 GetTextureImage */ static_cast<Uint8>(kWaitReply),
/* 56 Blit */ static_cast<Uint8>(kWaitNone),
/* 57 Clear */ static_cast<Uint8>(kWaitNone),
/* 58 ReadPixels */ static_cast<Uint8>(kWaitReply),
/* 59 DrawVbo */ static_cast<Uint8>(kWaitNone),
/* 60 LaunchGrid */ static_cast<Uint8>(kWaitNone),
/* 61 MemoryBarrier */ static_cast<Uint8>(kWaitNone),
/* 62 BeginStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 63 EndStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 64 PauseStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 65 ResumeStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 66 Flush */ static_cast<Uint8>(kWaitNone),
/* 67 Present */ static_cast<Uint8>(kWaitPresent),
/* 68 SetSwapInterval */ static_cast<Uint8>(kWaitNone),
/* 69 QueryTimestamp */ static_cast<Uint8>(kWaitReply),
/* 70 QueryCounter */ static_cast<Uint8>(kWaitNone),
/* 71 FenceWaitServer */ static_cast<Uint8>(kWaitNone),
/* 72 BindShaderImage */ static_cast<Uint8>(kWaitNone),
/* 73 PatchParameter */ static_cast<Uint8>(kWaitNone),
/* 74 BindStreamOutput */ static_cast<Uint8>(kWaitApplied),
/* 75 SetStorageBlockBinding */ static_cast<Uint8>(kWaitApplied),
/* 76 CopyFramebufferToTexture*/ static_cast<Uint8>(kWaitApplied),
/* 77 ApplierReset */ static_cast<Uint8>(kWaitApplied),
/* 78 ObjectDeath */ static_cast<Uint8>(kWaitNone),
/* 79 SetContextValues */ static_cast<Uint8>(kWaitNone),
/* 80 SetProgramBindings */ static_cast<Uint8>(kWaitNone),
};
static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"the wait-class table and the opcode space disagree");
inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeWaitClass>(kMGPipeWaitClasses[index])
: kWaitClassCount;
}
// THE COLUMN'S OWN INVARIANT, stated in C++ as well as in the generator: a reply-slot row
// waits for its reply and a waiting row owns a slot. gen_pipe.py refuses a catalogue where the
// two disagree, and this is the same statement where a reader of the generated table will look
// for it - a kReplySlot row that did not wait would run on and read a slot nobody had posted
// into, and a kWaitReply row with no slot would block on an answer nobody posts.
static_assert(MGPipeWaitClassFor(MGPWireOp::kInvalid) == kWaitClassCount,
"opcode 0 is not a call and has no wait class");
static_assert(MGPipeWaitClassFor(MGPWireOp::Present) == kWaitPresent,
"present is paced by the credit, not by a wait after the publish");
static_assert(MGPipeWaitClassFor(MGPWireOp::DrawVbo) == kWaitNone,
"the steady draw path is what P5e exists to stop waiting on");
static_assert(MGPipeWaitClassFor(MGPWireOp::ApplierReset) == kWaitApplied,
"a make-current is a fresh server and every verb after it must see the reset");
static_assert(MGPipeWaitClassFor(MGPWireOp::ReadPixels) == kWaitReply,
"a readback's answer is not derivable (R-5)");
// Spot checks the generator states about its own output, so that a catalogue edit that
// silently drops a flag is a build break here and not a wrong decode six packages away.
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
@@ -1031,6 +1154,14 @@ static_assert(sizeof(MGPWireRec_SetContextValues) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPContextValues) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetContextValues gained padding; the wire format moved");
struct alignas(8) MGPWireRec_SetProgramBindings {
MGPWireRecHeader Header;
MGPProgramBindings Payload;
};
static_assert(sizeof(MGPWireRec_SetProgramBindings) ==
((sizeof(MGPWireRecHeader) + sizeof(MGPProgramBindings) + 7u) & ~SizeT(7u)),
"MGPWireRec_SetProgramBindings gained padding; the wire format moved");
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
@@ -1317,6 +1448,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
case MGPWireOp::SetContextValues:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetContextValues, "SetContextValues");
break;
case MGPWireOp::SetProgramBindings:
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetProgramBindings, "SetProgramBindings");
break;
case MGPWireOp::kInvalid:
case MGPWireOp::kOpCount:
default:
+566
View File
@@ -0,0 +1,566 @@
# CONTRACT-P5E — the client runs ahead of apply on the Espryt draw path
Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17),
`CONTRACT-P5B.md` (the class-C slots) and `CONTRACT-P5C.md` (rule E, the two named exemptions,
SEG_EVENT, the guards). Where it disagrees with any of them this file is newer and wins; §8 lists
every such place. Base: `feat/disaggregated @ 2fde7034` (code head `1f8de61b`). Every `file:line`
was read at that commit and re-resolved at c0e's head; paths are under `MobileGL/` unless they
start with `docs/`.
**How to change it.** Package c0e's file, edited by the integrator first. A P5e package (id / vi /
sb / pg / tx2 / fb / ra) that needs a row changed goes through the integrator; the packages compile
against the rows below from day one.
**What c0e actually landed, where it differs from the plan this file was written as.** Four places,
each argued where it sits: `MGPProgramDesc` GREW 192 → 200 bytes because it has no spare byte (§1);
the applier's per-class binding-point windows are spelled `ShaderBufferStart/Count/WritableMask`
rather than bare `Start/Count/WritableMask` (§5.6); the suppressor's three slots are named
`SetShaderBuffersUniform/ShaderStorage/AtomicCounter` (§1); and the wire codec's record layout now
carries THREE tails rather than two, because `set_program_bindings` has three (§1). Everything else
below is the plan as written, and the nineteen rulings of §9 are as the integrator decided them
(ID-80…98).
---
## §0 What P5e is, and the rule above every row
P5d made `inproc` CPU-bound on the client thread with the frame still `client work + (per draw)
wait for apply + handoff` (R-1, `MG_Remote/Client/ClientSession.cpp:932-959`). The wait exists
because the apply of a draw still dereferences client-owned memory: the object-class BARRIER_PULLED
rows of `MG_Pipe/FieldOwnership.def:72-106` and the frontend-keyed twin registries
(`MG_Backend/DirectGLES/SlotTables.h:411-449`) inside the two P5C §3.1 exemption scopes. **P5e
retires the lockstep for every op on the Espryt draw path: the client publishes and moves on; the
server resolves every object from a handle a record carried, and the only client memory it may read
is the residual fill of a record the client is provably blocked behind.**
Rules A–E bind unchanged. P5e adds one definition and one rule:
**Definition — a BARRIERED record.** A record is barriered iff `MGPipeBarriered(op, payload,
applierState)` is true, a pure function of wire-visible data computed identically by the emit
table and by the sink (§2.1). The client blocks in `WaitForApplied(seq)` after publishing a
barriered record; it never blocks after an unbarriered one.
**Rule F — an unbarriered record's apply names no client memory.** With a live wire on a
run-ahead server (§6), the apply of an unbarriered record reads NOTHING from client-owned memory:
no frontend object dereference, no `gPipeInputs` BARRIER_PULLED row, no `MGPipeSlots()` probe or
mint, no `SharedPtr` to a frontend object taken or held. Every object is resolved from a handle the
record or the applier state carries, through a handle-keyed twin table (§4). A violation is
`Fatal{UnmigratedPipeInput, "<field>@<verb>"}` (a pulled row, §3.3), `Fatal{RoleViolation,
"MGPipeSlots"}` (an allocator probe, §4.4) or `Fatal{RoleViolation, "<surface>"}` (P5C §6 layer 1),
**regardless of `MOBILEGL_IPC_STRICT_ERRORS`** — the value would be torn or stale by construction,
so there is no "count it" arm. A barriered record's apply keeps P5C's rules exactly: its pulls count
into `rsp`, its probes are legal inside a scope, and the client's wait is what makes them legal.
Refusal vocabulary is P5C's, plus: `Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` (§5.1),
`Fatal{UnmigratedVerb, "set_global_constants"}` (§5.5), `Fatal{UnmigratedEmulation,
"image-bindable-redirty"}` (§5.2), `Fatal{ProtocolCorruption, "SetSamplerViews.Count"}` /
`"BindSamplerStates.Count"` / `"SetShaderBuffers.Class"` / `"SetShaderBuffers.Count"` /
`"SetProgramBindings.<field>"` / `"Present.FrameSerial"` / `"ImageView.Access"` (§1, §5),
`Fatal{PresentCreditTimeout}` (§2.4), `Fatal{RoleViolation, "gPipeInputs"}` (§3.5).
`Fatal{EventRingOverflow}` is RETIRED on a run-ahead server (§2.6).
---
## §1 Table 0 additions — encodings the P5e rows introduce
| field | the ruling | zero means | who reads it |
|---|---|---|---|
| **`MGPCapBit::kCapRunAheadApply = 1ull << 10`** (`MGPipeTypes.h`, bits 0..9 used) | Set in the DirectGLES arm of the server's `CallMask` (`MG_Backend/Init.cpp`), NEVER in the DirectVulkan arm. THE ARM IS A PURE FUNCTION, `MGPipeRunAheadCapBitsFor(backendType, ready)` in `MGPipeTypes.h`, so "Magma never" is a unit case (`MG_Test/Pipe/MagmaPipeIdentityTest.cpp`) rather than a line only a live split session reaches; `ready` is `MG_Backend/Init.cpp`'s `kMGPipeP5eRunAheadReady`. It says "this server applies unbarriered records without reading client memory". The client latches `RunAheadArmed()` = `m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply)` at the first caps adoption after `Start`; a later snapshot may only turn it OFF. | absent = lockstep (Magma; and Espryt until the integration commit flips `kMGPipeP5eRunAheadReady`, §6.2) | `ClientSession::RunAheadArmed`, `MGPipeBarriered` on both sides |
| **`MGPipeWaitClass`** — a FIFTH column of `PipeCalls.def`'s X-macro (`MGPipe.h`'s enum, spelled as a bare token like the class column), generated beside `kMGPipeCallFlags` into `generated/PipeWire.inc` as `MGPipeWaitClassFor(op)` | `kWaitReply` (every `kReplySlot` row), `kWaitPresent` (`Present`), `kWaitApplied` (the static barriered rows of §2.2), `kWaitNone` (everything else). Opcode order and the flag column are untouched: the column is a value, not a new op. `gen_pipe.py` refuses a token that is not an `MGPipeWaitClass` enumerator AND a row where the flag column and this one disagree in either direction — a `kReplySlot` row that did not wait is a waiter that hangs, a `kWaitReply` row with no slot blocks on an answer nobody posts. Three negative controls in `--self-test` (twelve now, up from nine). | — | `EmitAndWaitTails`, `ServerVerbSink::ApplyOne`, `MGPipeBarriered` |
| **`MGPDrawInfo::Flags` gains `kDrawClientArrays`** = `1u << 6`, the next free bit beside `kDrawIsIndirect` (`MGPipeTypes.h`'s `MGPDrawFlagBit`; `MGPDrawInfo` stays 56 bytes, the flag byte had two bits left) | Set by the client emit arm when any ENABLED attribute of the bound VAO has no buffer. §2.1 escalation (ii). `PipeCatalogueTest` pins the bit's value and its disjointness from the five before it. | no client array in this draw | `MGPipeBarriered`; `EmitDrawRecord`'s refusal (§5.1) |
| **`MGPPresent::FrameSerial`** | Minted by the CLIENT, `++m_presentsSent`, 1-based — no longer 0 (`EmitTables.cpp:1040`). The sink calls `ServerSession::ReturnPresentCredit(FrameSerial)` after `table->Present()` returns (today `ReturnPresentCredit` has no production caller, `ServerSession.cpp:677-682`), one credit per swap. | 0 arriving on a run-ahead server is `Fatal{ProtocolCorruption, "Present.FrameSerial"}` | `OnPresent`, `WaitForPresentAck` |
| **`MGPShaderBuffers` + `MGPBufferRange[]`** (`MGPipeTypes.h`, wire op 38, catalogued since P4a, no route/sink/emitter today) | One record per **Class** ∈ {Uniform=0, ShaderStorage=1, AtomicCounter=2} — the three values are `kMGPipeShaderBufferClassUniform/ShaderStorage/AtomicCounter` in `MGPipeTypes.h`, which is the ONLY spelling of them: the payload's comment had named them since P4a and nothing numbered them, so the emitter and the applier were one literal each away from disagreeing. `Start = 0` always; `Count = TouchedBufferBindingPointCount[target]` (the rv value already carried) clamped to `kMGPipeMaxBufferBindingPoints = 84` (pinned against `BufferState.h:29` in `PipeFill.cpp`); per entry `Res` = the buffer handle or null, `Offset/Size` from the point's range, **`Size = kMGPipeWholeBuffer` (`MGPipeTypes.h:406`) for a base binding** so the server re-resolves the extent against ITS descriptor at use (a `glBufferData` between emit and apply then binds the new extent, as GL does); `WritableMask` bit per entry for classes 1/2. `HostSpanCount = 0` on Espryt for the whole of P5e (`kCapNeedsHostUboBytes` is 0, `CONTRACT-P5.md:64`); the codec's honesty pass (`PipeWireCodec.cpp:1783-1799`) is the guard. `Class >= 3` → `Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}`; `Start + Count > 84` → `"SetShaderBuffers.Count"`. XFB targets do NOT ride this record (`set_stream_output_targets` keeps its row, still unemitted: XFB stays lockstep, §5.7). | `Res` null = nothing bound at that point | `MGPipeApplySetShaderBuffers`, `SyncBufferBindingPointsByRecord`, the UBO loop, `SyncAtomicCounterBuffers` |
| **`set_program_bindings` — opcode 80, appended.** `X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail\|kHostSpan, kWaitNone)` | Head `MGPProgramBindings { MGPipeHandle Cso; Uint64 Signature; Uint32 BlockBindingCount, SamplerUnitCount, StorageOverrideCount, Pad0; }` (32 B, `MGP_ASSERT_POD`); three tails in order: `Int32 BlockBindings[BlockBindingCount]` (dense, GL uniform-block index order), `MGPProgramSamplerUnit {Uint32 Location; Int32 Unit;}[SamplerUnitCount]` (8 B, sparse, ascending Location), `MGPProgramStorageOverride {MGHostSpan Name; Int32 Binding; Uint32 Pad0;}[StorageOverrideCount]` (40 B; the storage-block override map is name-keyed BY DESIGN, `ProgramObject.h:1030-1034`; the name travels as a host span staged whole). `Signature` = the client's commutative hash of the override map, the same function as `Managers.cpp:10832-10841`, so the server's rebuild clause is one `Uint64` compare. Bounds: `kMGPipeMaxProgramBlockBindings` 64, `kMGPipeMaxProgramSamplerUnits` 256, `kMGPipeMaxProgramStorageOverrides` 64, else `Fatal{ProtocolCorruption, "SetProgramBindings.<field>"}` — a larger sampler array is a counted refusal, not a truncation. **THE RECORD ALWAYS DECLARES THREE TAILS**, empty ones included: the tails are positional and the third one's offset is derived from the first two, so a record that declared "one tail" when only the block bindings were present would put the same bytes at a different offset than one that declared three, and the two halves of the wire would disagree about where the overrides start. The wire codec's `WireRecordLayout` grew from two tails to three for it (`MG_Remote/Wire/PipeWireCodec.h`), and the override names are walked through `CheckHostSpanIsHonest` in the decode arm rather than by the encoder's blanket second-tail pass — they are MEMBERS of a 40-byte element, and reading that tail as bare spans would see one span and a quarter of garbage. Emitted BEFORE `create_shader_state` at the same validate point (a rebuild inside the verb must already see the bindings). Dirty rule: bit `NewShaderBindings` (`Tracker.h:364-371, 419-448`) already mixes the three counters; the emitter latches on `(Cso, backendStateVersion, blockBindingVersion)`. A re-issued create clears the record's tails (`GlobalConstants`' rule, `PipeApply.cpp:2903-2909`). | all three counts 0 = "defaults" | `MGPipeApplySetProgramBindings`, the program twin's sync and memo keys |
| **`MGPProgramDesc::LinkStatus`** (a `Uint8`; **the descriptor grows 192 → 200**, `PipeCatalogueTest` pins the new size) | **THE DRAFT SAID "in the descriptor's existing pad, size unchanged" AND THAT WAS WRONG.** `MGPProgramDesc`'s four trailing `Uint8`s end at offset 24 and `MGPBlobRef` is 8-aligned, so the fixed head is exactly full and a fifth byte costs the alignment slack behind it: 20 bytes of handle and counters + 5 bytes of flags, padded to 32, plus 168 of blob refs. It is paid once, on a record that fires at most once per link, and it is confined to the push build — this payload is only ever instantiated under `MOBILEGL_PIPE_PUSH` (`PipeApply.cpp`, `PipeRoute.cpp` and the emitters are all push-only sources), which is why G1 does not see it. 1 = the link the archive describes succeeded. `create_shader_state` is re-issued on the same handle at every link that changes `GetLinkVersion` (today's rule) and the record's `Serial` moves with it; a failed link re-issues with `LinkStatus = 0` and empty blobs IFF the frontend reports the program unlinked afterwards (§9 ruling 9 pins which). A draw on a `LinkStatus == 0` program DECLINES on the server, as the frontend raises `INVALID_OPERATION`. | not linked | the program twin (`SyncCurrentProgram`) |
| **`MGPProgramDesc::Spirv[6]` / `Reflection` blobs are NON-EMPTY** on a live wire | The client encodes the link artefacts once per link with `ProgramArtifactsCodec` into `SEG_STAGE`; the applier decodes into RECORD-OWNED `LinkArtifacts`/`SpirvArtifacts` and the companion pointers (`ProgramEmit.h:230-260`) are deleted under a transport. `Size 0` with no companion is the existing trip wire `Fatal{ProtocolCorruption}` (`PipeApply.cpp:2858-2871`). A `SharedPtr` pin of the client's artefacts was considered and REFUSED: rule B, and `Link()` replaces them in place (`ProgramObject.cpp:348-356`). | — | the program twin's rebuild |
| **`MGPImageView::Access`** | 0 = `GL_READ_ONLY`, 1 = `GL_WRITE_ONLY`, 2 = `GL_READ_WRITE` — the encoding `MGPipeEncodeImageAccess` already writes (`MG_Impl/Pipe/ImageEmit.h`). The NUMBERS move to `MG_Pipe/MGPipeValueTypes.h` as `enum class MGPipeImageAccess : Uint8` with `MGPipeDecodeImageAccess` / `MGPipeImageAccessIsValid` / `MGPipeImageAccessReads` / `MGPipeImageAccessWrites` beside them, and the client encode NAMES those enumerators instead of literal 0/1/2 — one table, both roles, moved rather than duplicated. The encode also stops being a private static so the unit case can reach both halves. The server comment at `DirectGLES.cpp:2662-2669` is deleted as stale (fb). Any other value is `Fatal{ProtocolCorruption, "ImageView.Access"}` AT THE READER: the value header may not log, so it answers the question and the caller owns the refusal. | read-only | `SyncImageTextureBinding` |
| **`kMGPipeSubsystemBufferBindings = 1ull << 13`** (`MG_Pipe/MGPipe.h`, bits 14..62 now reserved) | Owns dirty bits 15/16/17 (`NewConstBuffers/NewShaderBuffers/NewSoTargets`, `Tracker.h:97-99`) and the `SetShaderBuffers` emitter. Caps-required under a transport like bits 7–12; the default push mask becomes `kMGPipeSubsystemsMigratedAtP5e` = `0x3fff`, and `0x1fff` survives as P5e's "everything P4a had and nothing of mine" control. Bit 13 requires bit 7 (a range names a Buffer handle). `NewSoTargets` rides the bit even though `set_stream_output_targets` stays unemitted for the whole of P5e: the bit answers "which A/B switch owns this family's legacy arm", and an operator clearing it has to get the whole binding-point family's frontend walk back rather than two thirds of it. `TrackerTest.cpp` is rewritten, not deleted, and now also pins that the phase constant is the WHOLE dirty-bit set — after P5e every bit names a subsystem. | bit clear = the frontend walk (monolith / A/B only) | `MGPipeSubsystemForDirty`, `SubsystemForEmitter` |
| **`SetHashSuppressor` slots** (`MG_Impl/Pipe/SetHashSuppressor.h`) | `SetShaderBuffers` becomes THREE slots (`SetShaderBuffersUniform` / `SetShaderBuffersShaderStorage` / `SetShaderBuffersAtomicCounter`) — one per Class, so an emission of one class never cancels another's; `SetProgramBindings` gets one. `SetStreamOutputTargets` stays "P4b" (unemitted). | — | the emitters |
| **`IpcTable::RunAhead`** = `MOBILEGL_IPC_RUN_AHEAD` (default 1, range 0..1); **`IpcTable::PresentCredit`** = `MOBILEGL_IPC_PRESENT_CREDIT` (default **1**, range 1..8) | Parsed unconditionally like every IPC knob (`ConfigLoader.cpp:356-403`), and both are in the IPC log line, because a knob that silently means nothing on one arm of an A/B is how an A/B stops being one. `PipeVerify` forces `RunAhead = 0` beside `BatchWaits = 0`. `RunAhead=1` on a server without the caps bit logs once "run-ahead requested, server does not publish kCapRunAheadApply" and runs lockstep. The stats line gains `credit-waits`. | `RunAhead=0` is the A/B control: identical server code, only the client's wait differs | `RunAheadArmed`, `EmitPresent` |
| **`BufferState` bind-point generations** (client-only, `#if MOBILEGL_PIPE_PUSH`) | One `Uint64` per `BufferBindPointTargets` entry beside `NoteBufferChanged` (`BufferState.h:69-75`), bumped by `glBindBufferBase/Range` (`GL_Buffer.cpp:1501-1520, 1624-1650`) and `SetNamedTransformFeedbackBinding`. Not a wire field; listed because it is the emit rule's input and the pull build's object must not resize (the licence `TextureState::NoteImageUnitTouched` took at P5d r3). | — | bits 15/16/17's shutters |
---
## §2 ra — the wait rule
### 2.1 The barriered predicate (one function, two callers)
```
MGPipeBarriered(op, payload, st) =
MGPipeWaitClassFor(op) != kWaitNone // static column, §2.2
|| (MGPipeCallClassFor(op) == kCtxVerb && st.IsTransformFeedbackActive) // (i) XFB stays lockstep
|| (op == DrawVbo && (payload.Flags & kDrawClientArrays)) // (ii) client arrays
```
`st.IsTransformFeedbackActive` is `MGPContextValues`' value (`MGPipeTypes.h:1698`): on the client
it is `ctx.IsTransformFeedbackActive()` at emit, on the server the applied value — `set_context_values`
precedes the verb on the ring, so the two agree. **c0e LANDED THE FUNCTION, all three clauses, in
`MG_Pipe/PipeApply.cpp`**, and gave `MGPipeApplierState` its own `IsTransformFeedbackActive` mirror
written by `MGPipeApplySetContextValues`: the predicate has to be computable on the server from
APPLIER state, and `gPipeInputs` is the block a run-ahead client stops filling, so reading the
answer out of there would be reading the very memory rule F exists to stop it reading. `payload`
may be null, which makes clause 3 fall through — a caller holding an opcode but not yet the bytes
must not be told a draw is unbarriered on that account. Escalation (ii) is a refusal under run-ahead
(§5.1), so on a run-ahead server it never reaches the sink; it is listed so the predicate is total.
**No other runtime escalation exists**; adding one is an integrator ruling and a row here.
### 2.2 The static column, row by row
| class | rows | why |
|---|---|---|
| `kWaitReply` | every `kReplySlot` row (`PipeCalls.def:115-202, 206, 221`): GetCaps, ResourceCreate/Respecify, MapPersistent, FenceStatus/Wait, QueryAvailable/Result/Timestamp, SetTextureParams, ResourceSubData (texture half, §2.5), ResourceReadback, GetTextureImage, ReadPixels | the answer is not derivable (R-5) |
| `kWaitPresent` | `Present` | the credit, §2.4 |
| `kWaitApplied` | `ApplierReset`; `GenerateMipmap` (`kCtxObject`, waits today for the reason at `ClientSession.cpp:900-914` — until tx2 lands its `VerbMipRes` arm, then `kWaitNone`); `SetStorageBlockBinding` (`kCtxVerb`, resolves through `GetProgramObject(GlName)`, `PipeApplier.cpp:805-808`; trailing: pg resolves through `MGPStorageBlockBinding::ShaderCso` and flips it); `BeginStreamOutput`, `EndStreamOutput`, `PauseStreamOutput`, `ResumeStreamOutput`, `BindStreamOutput` (§5.7); `CopyFramebufferToTexture` (the CopyTex endpoint still syncs the frontend texture, `DirectGLES.cpp:9714-9735`; trailing, tx2) | each still pulls a row or probes; the wait keeps P5C's semantics for it |
| `kWaitNone` | `DrawVbo`, `LaunchGrid`, `Clear`, `Blit`, `MemoryBarrier`, `Flush`, `SetSwapInterval`, `BindShaderImage`, `PatchParameter`; every `kCtxState` / `kCtxCso` / `kCtxObject` row without a reply (incl. `ObjectDeath`, `ResourceSubData` buffer half); `ResourceDestroy`, `UnmapPersistent`, `FenceCreate/Destroy`, `FenceWaitServer`; `QueryCreate/Begin/End/Destroy/Counter` | rule F holds for their sinks after §4/§5 |
The column is inert until the integration commit publishes the caps bit (§6.2), after vi/sb/pg/tx2/fb.
### 2.3 `EmitAndWaitTails` after `PublishAndNotify` (`ClientSession.cpp:877`)
```
if (ownsReplySlot) -> WaitForAppliedBudget + DrainEventRing + ReadReply (unchanged)
else if (RunAheadArmed())
switch (class) kWaitNone: return seq;
kWaitApplied: WaitForAppliedBudget + DrainEventRing + ReleaseFillPins(); return seq;
kWaitPresent: return seq; // the credit wait ran BEFORE encode, §2.4
else -> today's :915-959 shape, byte for byte (lockstep, Magma, verify)
```
`ReleaseFillPins()` clears the four O-class `SharedPtr` rows of `gPipeInputs` (`PipeInputs.h:801-809`)
on the GL thread once the barriered apply returned — under run-ahead the next fill may be a frame
away and a pinned VAO/program must not make the apply thread its last owner. `VerbBarrier==0`
keeps its lockstep meaning (nothing but reply rows waits); under run-ahead it removes the
`kWaitApplied` waits too and is the run-ahead NEGATIVE CONTROL: expected red is
`Fatal{UnmigratedPipeInput, ...}` at the first barriered row's stale pull.
### 2.4 Present pacing
`EmitPresent` (`EmitTables.cpp:977-1053`): `BeforeReadOnlyVerb(); if (m_presentsSent >= PresentCredit)
WaitForPresentAck(m_presentsSent + 1 - PresentCredit, 30 s)` — `ShutDown` returns, `TimedOut` is
`Fatal{PresentCreditTimeout}`; then `DrainEventRing`, encode, publish, `PumpControlPlane`. Credit 1
= the client publishes frame N+1 while the server applies and swaps frame N: one frame of overlap,
at most one frame of added latency. Budget at ~850 draws + ~0.36 MB pmap/frame: SEG_CMD ~70 KB +
set/CSO records per frame against 8 MiB, SEG_STAGE ~1 MB/frame against 32 MiB, SEG_REPLY unchanged.
The credit, never the bytes, is the steady-state backpressure; `WaitForCmdSpace` and the stage bell
(`PipeWireCodec.cpp:836`) are the safety net and both drain events on exit (§2.6).
### 2.5 Drains and forced waits
Drain points: every wait exit in `ClientSession` (reply, applied, present-ack, cmd-space, stage
bell), the four EGL RPC returns (`BackendObject_Remote.cpp:200-245`), `Stop`. Forced waits:
`MapBuffer(READ)` / `GetBufferSubData` / a `CopyBufferSubData` source → `SyncGpuWrites` (reply
row, unchanged); `glFinish` → `WaitForApplied(LastPublishedSeq)` + drain (a new client `Finish`
slot; `Flush`/`Finish` stay no-ops on the wire, ARCHITECTURE:415); every `Server*` EGL forwarder
waits `WaitForApplied(LastPublishedSeq)` BEFORE the RPC (the mailbox is pumped between drain
batches, `ServerLoop.cpp:385-390`, so a make-current would otherwise land between two run-ahead
records); `ServerSwapEGLBuffers` is not on that list — present is the swap. **`glGetError`
relaxation:** a run-ahead verb's `kEventGlError` is observed at the next drain point, i.e. at most
one present credit later than the call after it; P5C §4.2 already assigns ordering to P9 and P5e
names the window. **`resource_subdata`:** the BUFFER half passes `wantReply = false` through
`MGPipeRouteResourceSubData` (the Bool is discarded today, `PipeFill.cpp:839`; the server still
posts, the slot is never read — legal, `ReplySlot.h:16, 105`), which is what makes the persistent-map
push (`PersistentMapTracker.cpp:735, 850`) fire-and-forget; the TEXTURE half keeps its wait because
`DrainTextureSubData` clears the level's dirty flag on the accepted reply (D-D5; trailing).
### 2.6 Event-ring flow control (ARCHITECTURE §11.7 made real)
On a run-ahead server `Reserve == nullptr` is NOT `Fatal{EventRingOverflow}`: the producer latches
`eventRingFull` (exists, `EventRing.h:135-143`), publishes, rings, and `ApplyThreadMain` parks at
the record boundary with `eventRingFull == 0` added to `ready` (`ServerLoop.cpp:380-383`). The
client re-checks `m_events.RingIsFull()` at every park exit and drains before re-parking. Deadlock
argument: the server blocks only on the event ring; the client blocks only on watermarks the
server advances; every client wait drains; so at most one side is parked at any instant. The
lockstep arm keeps the Fatal (P5C §4.4).
### 2.7 Object lifetime under run-ahead
The client frees a slot only after its death record went out (`PipeFill.cpp:1623-1626`), the ring
is in order, every record naming `{s,g}` precedes `destroy{s,g}` which precedes `create{s,g+1}`,
and every server twin table Gen-checks (§4.3). **No wait is added at any delete.** Persistent maps:
`Forget()` (`PersistentMapTracker.cpp:599-606`) drops tracker state only; pushed bytes were copied
into SEG_STAGE at `StageOptional` and reclaim waits on `retiredSeq`, so a map released with blocks
in flight is safe. The P5d deferred-destroy queue (`PipeMutation.h:216-236`) STAYS LIVE: a
barriered record's apply may still pin a frontend object (its fill's `SharedPtr` rows, XFB
`targets`), so the apply thread can still be a last owner. Under `RunAheadArmed` an enqueue from an
UNBARRIERED apply is a finding: `MGLOG_E_ONCE` with the kind, Fatal under strict.
---
## §3 `gPipeInputs` — server-role memory for unbarriered records
1. **The client fills only for barriered records.** In `MGPipeValidateForVerb`
(`PipeFill.cpp:2962-3306`), when `RunAheadArmed()` and `!MGPipeBarriered(...)`: step 2 (tracker
walk) and step 3 (emitters) run unchanged; `:2989` (`++CurrentVerbSerial`), `:2999-3001`
(`SetVerb/SetIdentity`), step 4 (`:3252-3270`, `CopyField` + `FilledGen`) and `MGPipeLeaveVerb`'s
writes are SKIPPED; `MGPipeNoteFrontendMutation` returns at `:554`. The residual-value trip
wire (`:3284-3294`) still emits — it compares on the server against the applier mirror.
`RunAhead=0` on a caps-publishing server uses the SAME fill decision (the A/B changes one
variable: the wait). `PipeVerify` forces lockstep, a fill for every verb, and every record
barriered (both roles read the in-process flag; the comparator `:3304` is inproc-only).
2. **The server's stamp is server-private.** `MGPipeServerStampVerbBoundary` writes
`m_filled/m_currentVerb/m_serverStampedVerb` into applier-owned storage, never into the shared
block; `MGPipeServerClearVerbBoundary()` from the GL thread (`:2997`) is DELETED under a
transport. For a barriered record the server reads the client's fill (the client is parked) and
its own stamp; for an unbarriered one it does not touch the block. `SetIdentity` (`:3001`) moves
to `ApplyOne`, once per session. This removes the E note's hazard (b.4).
3. **The detector.** `MGPipeInputUnfreshRead` (`PipeInputs.cpp:191-224`) under an unbarriered
record takes `StrictBarrierPullFatal` (`:60-66`) regardless of `StrictErrors`; the seven sticky
forwards (`MGPipeStickyForwardPull`, `:251-257`) the same; `CountBarrierPull` stays for barriered
records. This is what turns `integration-split-strict` into a hard green lane (§7).
4. `VertexAttribDefaultsOf` (`PipeFill.cpp:84-86`) diffs against the emitter's own last header
(`g_attribDefaultLastHeader`, `:2949`), never against the block.
5. `RefusePipeInputsTouchWhileApplierOwnsIt` (`ClientSession.cpp:1015-1056`) is re-derived: the
`BatchWaits` early return at `:1037` goes; under `RunAheadArmed` any GL-thread write to the block
outside a barriered fill is `Fatal{RoleViolation, "gPipeInputs"}` by name.
`Fatal{BarrierViolation}` (`:840`) stays for `BatchWaits==0 && !RunAhead`.
---
## §4 id — handle-keyed twins
### 4.1 The rule
With a live wire, every twin on the apply thread is resolved BY THE HANDLE the applier state or
the record carries, never by a frontend object: `GetOrCreate(MGPipeHandle)` (`SlotTables.h:294-342`),
`FindByHandle` (`:420-426`), `ReleaseByHandle` (`:394-408`). `HandleOf(stateObj)`,
`Find(StateObject*)`, `GetOrCreate(const StatePtr&)`, `NoteStateForHandle/StateForHandle` and
`Entry::stateRef` survive as the MONOLITH-GLUE half (§5.8) and are unreachable from an
unbarriered apply. The twin sync bodies take `(MGPipeHandle, const <Record>&)` and store
`m_handle` (the `m_pushedSyncHandle` pattern, `Managers.h:1857`); the object parameter is deleted
from the transport overload, not defaulted to null. The E note's "content-addressed VAO CSO" is
corrected: every kind mints its slot off the object's own lifetime id (`VertexInputEmit.h:170-172`,
`TextureEmit.h:422`, `FramebufferEmit.h:257`, `ProgramEmit.h:411-422`), so the rekey is a
replacement of the lookup ARGUMENT, not of the table.
### 4.2 Per kind
| kind / table | key on the server | the record that names the handle before any verb reads it | twin created | twin dies |
|---|---|---|---|---|
| Buffer `g_backendBufferResources` (already handle-keyed) | `Res` from `VertexBuffers[]`, `IndexBuffer`, `BoundShaderBuffers[c][i]`, `Desc.BufferForTexBuffer`, `VerbIndirect*` | `resource_create/respecify` | `EnsureBufferResourceForHandle(nullptr, h)` | `resource_destroy` |
| VAO `g_backendVertexArrayObjects` | `st.BoundVertexElements` (`PipeApply.h:594`; one CSO per VAO, re-created on the same handle when the config moves) | `create/bind_vertex_elements` | first `ResolveVaoTwin(handle)` at a draw | `object_death{VertexElementsCso}` / `delete_vertex_elements` |
| Texture `g_backendTextureObjects` | `BoundSamplerViews[u].Texture`, `BoundShaderImages[u].Res`, `FramebufferRecords[h].State.{Color[i],Depth,Stencil}.Res`, `VerbMipRes`, `VerbCopyTexDst`, `MGPCopyImage` src/dst | `resource_create/respecify` (Texture) | first `SyncTextureToBackendByHandle(h)` | `object_death{Texture}` / `resource_destroy` |
| Renderbuffer `g_backendRenderbufferObjects` | `MGPSurface.Res` with `Kind == Renderbuffer` | `resource_create/respecify` (Renderbuffer) | first attach sync | `object_death{Renderbuffer}` |
| Framebuffer `g_backendFramebufferObjects` | `st.BoundFramebuffer[t]`, `record.Fbo`, `VerbBlitReadFbo/DrawFbo`; `kMGPipeDefaultFramebuffer` is never a twin | `set_framebuffer_state` | `GetOrCreateByHandle(record.Fbo)` (already, `DirectGLES.cpp:3197`) | `object_death{Framebuffer}` |
| Program `g_backendProgramObjects` | `st.DrawProgram` / `DispatchProgram` / `BoundShaderCso`; composites at the band | `create_shader_state` (+ `set_program_bindings`) | first `SyncCurrentProgram(handle)` | `object_death{ShaderCso}` / `delete_shader_state` |
| Sampler `g_backendSamplerObjects` | the CSO family ONLY: `st.BoundSamplerStates[u]` or `TextureResources[h].Params.BuiltinSampler` | `create_sampler_state` | `ResolveSamplerCsoTwin` (already, `Managers.cpp:13457-13486`) | `object_death{SamplerCso}` |
| SamplerView `g_backendSamplerViews` | `BoundSamplerViews[u].View` (no draw-path resolver today; unchanged) | `create_sampler_view` | — | `object_death{SamplerViewCso}` |
The identity family of samplers (`ResolveUnitSamplerBackend` `DirectGLES.cpp:4985-5026`, the mint
at `:5623-5630`, `Managers.cpp:13264-13280`) is deleted under a transport; a unit whose
`BoundSamplerStates[u]` is null while the frontend held a sampler is a missing record and §5.3's
window rule makes it unrepresentable. The creation moment is LAZY everywhere (no twin at
`create_*`); the applier's Gen check refuses a bind or verb naming an unknown or stale handle
(`PipeApply.cpp:2266, 2549, 2625, 2683, 2896`), so a twin is never minted for an object the ring has
not introduced. `applier_reset` touches neither twins nor object records (`PipeApply.h:800-832`).
### 4.3 ABA
`Gen` moves only on reuse (`SlotAllocator.cpp:176-189`, and `:240-258` for the composite band —
"the one place Gen may move", in the file's own words). At the server every record naming `{s,g}`
is applied before `object_death{s,g}`, which is applied before `create{s,g+1}`. `GetOrCreate({s,g+1})`
on a live `{s,g}` entry resets it (`SlotTables.h:338`); `FindByHandle({s,g})` after the recycle
answers null (`:424`); a backward `{s,g-1}` is refused (`:331`). Even with a death skipped (an
object that never crossed emits none, `WireTables.cpp:595-596`) the forward-Gen reset answers.
Twin memos key on `{slot,gen}` plus server serials, never on a pointer. `BackendSlotTable` gains
the composite band (`m_band`, indexed `slot - base`, as `SlotAllocator.h:144` / `PipeApply.h:511`)
so a pipeline program (slot >= 983040) does not grow the ordinary table to `kMaxHandleSlot` entries
— a P5e prerequisite, not a follow-up, because the rekey makes it the ordinary path.
### 4.4 The scopes and the guard
`MGPipeRefuseAllocatorFromApplyThread` (`SlotAllocator.cpp:22-38`) exempts a probe iff a scope is
active AND `MGPipeApplierCurrentRecordIsBarriered()` (server-private, set by `ApplyOne` from
§2.1; `true` for every record on a non-run-ahead server, §6). **c0e landed the storage and the
writer** (`MGPipeApplierSetCurrentRecordBarriered`, `MG_Pipe/PipeApply.cpp`) with `true` as the
default — the only seam in this phase whose body is implemented rather than aborting, because
there the safe answer and the landed lockstep answer are the same answer, and an abort would take
down the monolith and the lockstep split alike. id owns the `ApplyOne` call site. **`MGPipeFrontendKeyedRegistryScope`
is deleted at its draw-path sites** — `DirectGLES.cpp:1609, 1734, 2588, 3303, 4236, 4329, 4585, 4926,
4993, 5328, 5689, 6729, 6775, 9695` and `Managers.cpp:256, 2938 (draw-path callers), 4544, 5882,
7087, 8481, 8717, 9256, 9310, 9709, 12949, 13273, 13570` — with the frontend probes they wrap; the
class survives only around the barriered-row sites (`DirectGLES.cpp:6166, 8837, 9161, 9216, 10029,
11884`, the verify arms `Managers.cpp:10382, 10403`) with their P8/P9 phases re-annotated.
`MGPipeReverseAnnouncementScope` is renamed `MagmaP7AllocatorDebtScope`, compiled for the Magma
sites only (`VulkanRenderer.cpp:4554, 4634, 8986`, `ResourceTracker.h:723`), exempting only when
the server backend is DirectVulkan. The renderbuffer cross-check `Managers.cpp:9709-9720` (not
transport-gated today, unlike `:9628-9632`) is deleted by the rekey, not gated. The NoSession
server-only fixture hop (`Managers.cpp:224-246`) is deleted; such a fixture's twins die at Stop.
---
## §5 Per family — the clean gate, the records, the windows
### 5.1 VAO / vertex input / index (vi)
Twin from `st.BoundVertexElements`; memo key `{BoundVertexElements, rec->ContentSerial,
VertexBuffersSerial, IndexBufferSerial}` (all server-owned); the buffer walk is over
`st.VertexBuffers[Start..+Count).Res` (dedupe on `{slot,gen}`) with
`EnsureBufferResourceForHandle(nullptr, Res)` and `IsBufferDrawCleanByHandle(h, res, nullptr)`;
`ResolvedDrawBuffers::Entry::frontend` leaves the split arm; the IBO arm reads `st.IndexBuffer.Res`
+ `IndexBufferSerial` (also for the restart substitution, `DirectGLES.cpp:6447-6461`);
`SyncCurrentVertexAttributeValues` keys on `{elementsHandle, ContentSerial, activeMask}` and reads
`rec->Attributes[i].Enabled`; the dead `GetConfigVersion` read at `:4775` is deleted. **Client-memory
vertex arrays are NOT refused under split today** (only `CLIENT_INDICES/CLIENT_COMMANDS/
UNBOUND_PARAMETER`, `EmitTables.cpp:651-685`; the server dereferences `attrib.Offset` as a raw
pointer, `Managers.cpp:5382`): under `RunAheadArmed` a draw with `kDrawClientArrays` is
`Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` raised on the GL thread in `EmitDrawRecord`'s
array arm; the lockstep arm is unchanged; the staged form (a per-attribute `{BindingIndex,
MGHostSpan}` tail like `kDrawHasUserIndices`) is P8's. Two pins by test: `CurrentBufferMutationEpoch()`
(`Managers.cpp:794`) is apply-thread-only under a transport; the VAO twin key is 1:1 only while the
vertex-elements CSO stays identity-addressed.
### 5.2 Textures (tx2)
Every draw-path entry is `SyncTextureToBackendByHandle(h, imageBindable)`: record first, twin by
`GetOrCreateByHandle`, three syncs `(h, *rec)`; the by-value twin copy + second `Find`
(`DirectGLES.cpp:1745-1755, 1781-1805`) go with the map arm. Clean = `m_isInitialized &&
m_syncedResourceSerial == rec->Serial && rec->PendingUploads.empty() && m_syncedParamsSerial ==
rec->ParamsSerial && !rec->Params.ForceResync && !m_forceTextureParamsResync &&
m_syncedBuiltinSampler == rec->Params.BuiltinSampler && m_syncedBuiltinSamplerSerial ==
SamplerCso(rec->Params.BuiltinSampler)->Serial && !rec->Params.SamplerResync && !m_forceSamplerResync
&& rec->Desc.StorageKind == Mipmap` — no new state. The four in-body live reads (`GetTarget` at
`Managers.cpp:8876-8877, 8620-8621`; `IsTextureView` `:7060`; the TexBuffer backing `:8223-8231`)
read `Desc.Target / Desc.ViewOf / Desc.BufferForTexBuffer`; `GetExternalIndex` in logs becomes
`Desc.GlNameForDiag`. The unit work list is `{Res, backend}` keyed `(ContextSerial,
SamplerViewsSerial, SamplerViewStart, SamplerViewCount, g_backendContextGeneration)`;
`PairingsIntact` is deleted; `ResolveAndBindUnitTextures` is one pass over the window with the
target from `SamplerViewCsos[View].View.Target`, then an unbind of every target the window did not
claim driven by `g_boundTexturesCache`; the alias arbiter dies (the client arbitrated,
`SamplerEmit.h:751-757`). **Named delta (P5e-5):** the unit list syncs the program-resolved texture
per unit, not every slot of every touched unit; coverage is unchanged (views ∪ image units ∪
attachment lists ∪ the waited texture ops). `RequireImageBindableStorage`'s re-dirty arm
(`Managers.cpp:5930-5962`, already aborting three frames deep) becomes a NAMED refusal at its
entry under a transport, `Fatal{UnmigratedEmulation, "image-bindable-redirty"}`; `ImageBindableHint`
is the prevention, P9 owns the pull. `GenerateMipmap(target)` under a transport resolves the
texture from `VerbMipRes` (as `EnsureGenerateMipmapStorageAllocated` already does, `DirectGLES.cpp:8736`),
never the active unit (`:9563-9566`), and its row flips to `kWaitNone`. `m_force*Resync` are
server-set and never cleared by the wire; the wire bits are never cleared by the server (D-E2, now
load-bearing for a gate). The pinned guard list (`MipmapStorage.cpp:39-51`, `RemoteClientTest.cpp:1938-1978`)
gains the six shape accessors it exempts today plus `GetSamplerObject/GetTextureParamsVersion/GetContentVersion`.
### 5.3 Samplers and the two unit windows (tx2; the rule binds the emitters)
The effective sampler of unit u is `st.BoundSamplerStates[u]` if non-null else
`TextureResources[view.Texture].Params.BuiltinSampler`; its values are the CSO record's
(`MGPipeValueTypes.h:462-486`); the raw-depth-fetch substitution stays on the server
(ARCHITECTURE §5.5); `texture2D->GetFormat()` becomes `SamplerViewCsos[View].View.InternalFormat`.
**Window rule (A8 closed):** `set_sampler_views` and `bind_sampler_states` MUST carry `Start == 0`
and `Count >= MaxTouchedTextureUnit + 1` (0 when none touched), as `SamplerEmit.h:733-737` emits
today; at every draw/dispatch apply the sink checks the two counts against the applied
`MGPContextValues::MaxTouchedTextureUnit` and a narrower window is `Fatal{ProtocolCorruption,
"SetSamplerViews.Count"}` / `"BindSamplerStates.Count"` — a server-side re-derivation is refused.
The `BindCurrentTextures` memo key is `{DrawProgram.{Slot,Gen}, ShaderCso.Serial, BindingsSerial,
SamplerViewsSerial}` (ruling 6). The decline arms `DirectGLES.cpp:5152-5169` and `:2073-2098` become
named refusals under a live wire; `CaptureUnitBindings/UnitBindingsUnchanged/g_observedUnitBindings`
(`:1867-1957`) are monolith-only text.
### 5.4 Framebuffers, attachments, images (fb)
`SyncCurrentFBO` is `SyncCurrentFBOByRecord` only; a decline under a transport is
`Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"}`; `FramebufferRecordMatchesBinding`
(`:3017-3033`) is monolith-only. `BackendFramebufferObject::SyncToBackend(record, target)`: the
attachment walk is over `Color[0..7] + Depth + Stencil` (`Color[0]` alone when `IsDefault`) — the
record's 11 surfaces ARE the point set (the emitter refuses a point at or above the wire width,
`FramebufferEmit.h:489-501`; FRONT/BACK tokens exist only on the default framebuffer); the empty
point is `Kind == None || Res null` for the detach (`Managers.cpp:10310`) and the N-6 refusal
(`:9601-9606`) alike; `m_syncedFrontendAttachmentVersions` is replaced by the record hash;
`g_attachmentBackendIdGeneration` stays. `SyncAttachmentSurface(target, surface, attachment)` takes
no object; storage through `SyncMipmapsToBackendByHandle` / RBO `SyncToBackendByHandle(surface.Res)`.
The two attachment texture lists key `(record.Fbo, ContentHash, ContextSerial,
g_backendContextGeneration)` with entries `{Res, backend}` and `PairingsIntact` = `entry.Res ==
surface.Res`. `ForceBindCurrentFBO` is handle-only and stamps nothing frontend (the stamps at
`:4734-4738` and the legacy memo are skipped together under the transport test). The named blit
takes two `const MGPFramebufferState&` (draw buffers, `ReadSurface`, `Kind`, layer/level/layered/
format from the surface, `Samples` from the record, `kMGPipeDefaultFramebuffer` for the default)
and retires `StateForHandle`; the bound arm reads `FramebufferRecordFor(st.BoundFramebuffer[t])`.
`ScopedDetachedTextureFramebufferAttachments` walks a reverse index texture → FBO slots maintained
at `set_framebuffer_state` apply. `MGPFramebufferState::Complete` is never a "may I draw" gate.
`SyncReadFramebufferTextureAttachments` is not gated on the framebuffer bit (`:2227-2230`), so its
retirement moves the object A/B baseline in BOTH arms (re-baselined, not a regression).
**Default-framebuffer resize:** the surface-changed consumer's `AllocateStorage` (`ClientSession.cpp:210-221`)
reaches `MGP_NOTE_AGGREGATE(FramebufferAttachment)` (`TextureObject.cpp:84-102`), so bit
`NewFramebuffer` (`Tracker.h:594`) re-emits the record — verified at this head, pinned by a test in fb.
Images: `SyncImageTextureBinding(const MGPImageView&)`: twin by `FindByHandle(Res)`, `Access`
decoded, target and storage format off the TWIN (no new wire field; `MGPImageView` is 24 B, no
pad); `ResolveShaderImageRecord`'s identity test and I5 seam log go. The sweep gate is
`(ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration)` — never a
backend re-mint counter, so the "re-minted inside the sweep" property (`:2890-2901`) holds; the walk
stays the UNION of the record window and `g_imageUnitHighWaterMark` (`:2811-2821`), never narrowed
to the window. `MarkWritableImageBufferTexturesGpuWritten` is DELETED under a transport (the client
owns the GPU-write set, `GpuWritePending.h:9-55`, `:83-115`); `WritableMask` survives as the record
of which points P9's narrowing will name.
### 5.5 Programs (pg)
Twin from the ShaderCso handle; `SyncToBackend(h, const MGPipeShaderCsoRecord&)` answers every
reflection question from the record-owned archive and the bindings tails (`GetLinkedShaderSnapshot`
`:12049` is a debug log, deleted on this arm). The nine-clause clean condition (`DirectGLES.cpp:4437-4507`)
keeps its clause count and moves its inputs: `record.Serial` vs `m_syncedShaderCsoSerial` (exists,
stamped at `Managers.cpp:12944-12966`, unread today — P5e is its first reader), `BindingsSerial`,
`record.Signature`, `Desc.SpirvStatus`, `Desc.LinkStatus`; `ImageUnitFormatsStillMatch` and the
patch clauses are fb's / value rows. The stash `g_currentDrawFrontendProgram` (`:4097-4098`)
becomes `{MGPipeHandle, twin*}` compared against `st.DrawProgram`; `PrepareForCompute` reads
`st.DispatchProgram`. The sampler-pass memo (`:5522`) keys on `BindingsSerial`;
`sampledTargetForUnit` reads the sampler-unit tail and the archive's uniform types. **`MapUBO()`
has no run-ahead answer:** when `ResolveGlobalConstantsRecord` declines under a transport it is
`Fatal{UnmigratedVerb, "set_global_constants"}`, not a torn upload. `GetProgramObject/
ValidateProgramName` keep their P9 rows: `GetBackendProgramId(name)` (`:6151`) is a monolith entry;
`ShaderStorageBlockBinding` (`:10020`) is reached from the barriered `set_storage_block_binding`
(§2.2) until pg's trailing item resolves it by `MGPStorageBlockBinding::ShaderCso`. Codec
completeness: `ProgramArtifactsCodec.cpp:242`'s skipped member is named and either encoded or
proven unread by the twin before pg claims "every reflection question is answered".
### 5.6 Buffer binding points (sb)
Applier: `BoundShaderBuffers[3][84]`, `ShaderBufferStart[3]` / `ShaderBufferCount[3]` /
`ShaderBufferWritableMask[3]` (the draft said bare `Start/Count/WritableMask`; they sit in
`MGPipeApplierState` beside `SamplerViewStart`/`SamplerViewCount` and take the same shape), ONE
`ShaderBuffersSerial` advanced on every applied record and ADVANCED (never zeroed) by
`MGPipeApplierReset` (`VertexBuffersSerial`'s rule, `PipeApply.h`); the emitter's latch resets with
it (`PipeFill.cpp:3129-3143`'s list). c0e declares all of it, with
`MGPipeApplySetShaderBuffers` declared and aborting by name until sb writes the body.
**`WritableMask` IS A `Uint32` AND THE WINDOW IS 84** (`MGPShaderBuffers::WritableMask`, unchanged
since P4a): the mask can only describe the first 32 points of a class, so sb either narrows the
window it sets bits for, widens the field, or states the bound — an integrator ruling sb must ask
for rather than pick, because the field is on the wire. `SyncBufferBindingPointsByRecord(Class, glTarget)`: per entry
`EnsureBufferResourceForHandle(nullptr, Res)`; whole-vs-range is `Offset == 0 && Size ==
kMGPipeWholeBuffer`; clean = `resource->syncedChangeSerial` vs the resource record's `Serial`
(`Managers.cpp:3151`) plus `ShaderBuffersSerial`; no twin key is introduced (a point names an object,
it is not one). The UBO loop (`DirectGLES.cpp:5447-5497`) reads `BoundShaderBuffers[Uniform][binding]`
where `binding` is the program's block binding (pg's tail); `binding >= Count` = nothing bound;
`GetBufferResource(obj)` `:5472` becomes `FindBufferResourceForHandle`. `SyncAtomicCounterBuffers`
reads `Count[AtomicCounter]`; `GetBufferBindingPointCount`'s sticky forward becomes FATAL under
split (`InvalidateCompileEnv`'s shape, `FieldOwnership.def:163-165`). The three backend GPU-write
marks (`DirectGLES.cpp:606, 658, 2775`) are deleted under a transport in ONE edit with fb's.
**Client shutter (the P4b hole, verified: bits 15/16/17 read the content aggregate, `Tracker.h:525,
636-639`, and `glBindBufferBase` moves only `BindingSlot::m_version`):** bit 15 =
`Mix(bindPointGen[Uniform], programIdentity)`, bit 16 = `Mix(gen[ShaderStorage], gen[AtomicCounter])`,
bit 17 = `Mix(gen[TransformFeedback], TransformFeedbackGeneration)`; the content aggregate leaves all
three. `Touch` joins `gen_pipe_dirty_surface.py`'s `MUTATOR_PREFIXES` (`:87-90`) and `DirtySurface.def`
gains the rows the generator then demands. `Coverage.def`'s EMITTED list gains `GetBufferBindingPoint
→ SetShaderBuffers` as SHAPE-ONLY — **landed by c0e, with BOTH of its `PipeFill.cpp` switch arms
(`SubsystemForEmitter` and `EmittedCallSuppliesTheWholeField`) and the pairing `static_assert`s**,
because that file belongs to the contract package for the whole phase and an emitter enumerator
whose dispatch arm lived in the family's own worktree is exactly the merge trap the file's own
comment was written about. It is inert until the family's wired constant leaves 0.
`FieldOwnership.def:80-81` flips to RECORD_SUPPLIED when the four Espryt consumers are gone. `GetBufferBindingSlot`'s 15-pointer copy (`PipeFill.cpp:116-124`) needs no
special case: an unbarriered record has no fill at all.
### 5.7 XFB — stays lockstep
`StartPendingTransformFeedback` (`:1381-1510`), `SyncTransformFeedbackBindingPoints` (`:562-592`),
`ReadbackCapturedRanges` (`:1118-1160`) and the `xfb.targets` pins are not migrated
(`GetTransformFeedbackProgram` is an object row no package retires; the targets hold `SharedPtr`s
across two verbs, which rule C forbids an applier entry to do without a barrier). Escalation (i)
makes every verb inside an active span barriered; the cost on the Espryt steady path is one bool
test per draw (`:1386`).
### 5.8 Monolith / G1 / the push build under `Transport=monolith` (ruling 1)
Every server-side handle arm is selected by `MG_Config::Transport != Monolith` (the idiom at
`DirectGLES.cpp:3226, 4725, 6534`, `Managers.cpp:9629`) AND the family bit; it runs under ANY
active transport, lockstep or run-ahead, so `RUN_AHEAD=0` is a pure wait-rule A/B on identical
server code. The push build under `Transport=monolith` keeps its frontend arms token for token
(behaviour and bytes), which the verify comparator needs; the pull build compiles none of it (every
new symbol sits under `MOBILEGL_PIPE_PUSH` / `MOBILEGL_BUILD_DISAGGREGATED`; the `TwinRegistry`
alias keeps its mangled name, `Managers.h:626-640`; `BackendTextureObject`'s pull-build layout does
not move — the admitted-resize set stays empty). Two overloads, not an `#if` inside one body — the
frontend one is visibly the monolith-glue half, as `GetOrCreate(const StatePtr&)` is
(`SlotTables.h:236-244`). G1 0/0/0/0, G2/G14 name-set rules kept; the new public names are listed in
BRIEF-P5E per package. `MOBILEGL_PIPE_VERIFY` forces lockstep (`ConfigLoader.cpp:377`, confirmed).
---
## §6 Magma keeps the lockstep
1. The DirectVulkan arm of `CallMask` never sets `kCapRunAheadApply`; `RunAheadArmed()` is false;
the client runs today's `:915-959` path; `MGPipeApplierCurrentRecordIsBarriered()` answers `true`
for every record on a server that does not publish the bit, so Magma's probes inside
`MagmaP7AllocatorDebtScope` and its BARRIER_PULLED reads keep P5C's semantics and `rsp`
accounting. P7 retires them.
2. On Espryt the bit is published by `Init.cpp`'s DirectGLES arm gated on one constant
(`kMGPipeP5eRunAheadReady`, false until the integration commit), so every package lands with the
column inert and nothing changes behaviour until the integrator flips it.
3. Negative controls: `MOBILEGL_IPC_RUN_AHEAD=1` on Magma logs once and runs lockstep;
`MagmaPipeIdentityTest`/`ServerLoopTest` pin that a DirectVulkan `CallMask` has no bit 10.
---
## §7 The strict gate
`integration-split-strict` (`.github/workflows/test.yml:1140-1206`) becomes a HARD GREEN lane for
the Espryt run-ahead server: with `MOBILEGL_IPC_STRICT_ERRORS=1` and the caps bit published, every
scenario must complete and every `[BARRIER-PULLED, ...]` marker in a private log is red. Because
§3.3 aborts on an unbarriered pull regardless of the knob, strict adds only the barriered rows'
pulls (readbacks, GenerateMipmap until tx2, XFB, CopyTex, set_storage_block_binding) — the lane's
step lists those rows as the ONLY admitted markers (an allowlist by `<field>@<verb>`) and fails on
any other. A revert of any handle arm in vi/sb/pg/tx2/fb goes red HERE, by field and verb, not in
a picture. Magma keeps the expected-red step. `rsp` on unbarriered records is 0 in every scenario
summary (pinned: a non-zero count would be a Fatal that did not fire). The `PipeSlotPeek` harness
asserts zero scope entries per frame on the draw path.
---
## §8 Amendments to CONTRACT-P5C (the complete list)
1. **§3.1's two named exemptions** are superseded by §4.4: the registry scope is deleted at every
draw-path site and exempts only a barriered record elsewhere; the announcement scope is Magma's.
2. **§4.4 overflow**: lossless stays, but a run-ahead server parks on `Reserve == nullptr` (§2.6).
3. **§5.2 `object_death`**: the ring keeps the ordering; the row is `kWaitNone`.
4. **§5.1 `applier_reset`** is `kWaitApplied`; its `ContextSerial` assertion stands.
5. **§6 CI lane**: the two-sided lane becomes one-sided green for Espryt (§7).
6. **§6 layer 2** gains `Fatal{RoleViolation, "gPipeInputs"}` (§3.5); `InBarrierWait()` is
re-derived as `MGPipeBarriered`. 7. **§3.3's state note** is retired by fb (§5.4).
8. **`FieldOwnership.def`**: `GetBoundVertexArray`, `GetTextureUnitObject`, `GetImageTextureBinding`,
`GetFramebufferBindingSlot`, `GetProgramForDraw/Dispatch`, `GetBufferBindingPoint` gain retiring
phase "P5e (Espryt unbarriered), P7 (Magma)"; `GetBufferBindingPointCount`'s forward → FATAL under
split; `GetTransformFeedbackProgram`, `GetProgramObject`, `GetTextureObject`, `ValidateProgramName`,
`HasOpenTransformFeedbackSpan`, `RecordError` keep their rows (barriered-only readers).
9. **Table 1** gains `set_program_bindings` (80), appended; `set_shader_buffers` (38) gains its
route, sink and emitter (`PipeCatalogueTest.cpp:170-171, 242-243` invert). **Table 0** gains §1's
thirteen rows. **Table 3** gains `IpcTable::RunAhead/PresentCredit`.
10. **P5C §6's `rsp` paragraph**: the measured object-class pulls (948.5/frame on bsl) go to 0 on
unbarriered records; what remains is the barriered allowlist of §7.
## §9 The rulings, as made (ID-80…98; BRIEF-P5E §6 has the conflicts they resolve)
All nineteen are ACCEPTED AS WRITTEN. They are restated here rather than referenced, because a
package reads this file and not the brief.
1. Monolith arm selection: transport check + family bit, frontend arms kept for monolith (§5.8) —
NOT S7's "handle arm under monolith too" (it would make the monolith push build depend on the
new emissions and lose the verify comparator's frontend arm).
2. Client vertex arrays: refuse under run-ahead (§5.1), stage in P8; wait-escalation rejected (it
would need `Find(vao)` on the apply thread, which §4.4 forbids for a draw).
3. XFB / atomic dispatch waits: the wire-derived predicate (§2.1); atomic-counter dispatch needs no
escalation because sb lands 4.5.
4. Present credit default 1, range 1..8; credit 2 is a measurement arm only.
5. `resource_subdata`: buffer half `kWaitNone`, texture half keeps the reply (trailing D-D5).
6. `BindCurrentTextures` memo key: both S2's and S4's rows (§5.3).
7. Image sweep gate: applier serials, never a backend counter (§5.4); keep the union walk.
8. Program archive: encode per link into SEG_STAGE (§1), measured on MC + a shader pack before pg
is sized; the `SharedPtr` pin is refused; lazy encode at first bind is the fallback.
9. Failed relink: re-issue with `LinkStatus = 0` iff the frontend reports the program unlinked
afterwards; pg verifies which at `ProgramObject.cpp:348-356, 518-522`. Never an `object_death`.
10. `set_shader_buffers` window: the touched high-water count per target; capacity 84 on the wire,
the backend clamps (`DirectGLES.cpp:500-504`). 11. Suppressor: three class slots.
12. Magma scope: one renamed scope, backend-kind-keyed exemption (§4.4).
13. Deferred-destroy queue: stays live (§2.7). 14. `ResolvedDrawBuffers`: measured after vi.
15. `Blit`'s `kWaitNone` is fb's landing, not a separate package; `Clear`'s is tx2+fb.
---
## §10 What c0e landed, by name — the surface every other package compiles against
Inert by construction: the caps bit is not published (`kMGPipeP5eRunAheadReady` is `false`), the
new opcode has no route, the new subsystem bit is not in `kMGPipeWiredSubsystems`, and every new
applier entry point and by-handle backend signature has a body that aborts by name. Unit,
`integration-split` and the generator gates are green with all of it in the tree.
| where | the names |
|---|---|
| `MG_Pipe/MGPipeTypes.h` | `kCapRunAheadApply`, `MGPipeRunAheadCapBitsFor(BackendType, Bool)`, `kMGPipeMaxBufferBindingPoints`, `kMGPipeShaderBufferClass{Uniform,ShaderStorage,AtomicCounter,Count}`, `MGPProgramBindings`, `MGPProgramSamplerUnit`, `MGPProgramStorageOverride`, `kMGPipeMaxProgram{BlockBindings,SamplerUnits,StorageOverrides}`, `MGPProgramDesc::LinkStatus`, `kDrawClientArrays` |
| `MG_Pipe/MGPipeValueTypes.h` | `MGPipeImageAccess`, `kMGPipeImageAccess{ReadOnly,WriteOnly,ReadWrite}`, `MGPipeImageAccessIsValid`, `MGPipeDecodeImageAccess`, `MGPipeImageAccessReads/Writes` |
| `MG_Pipe/MGPipe.h` | `MGPipeWaitClass` (`kWaitNone/kWaitApplied/kWaitReply/kWaitPresent/kWaitClassCount`), `kMGPipeSubsystemBufferBindings`, `kMGPipeSubsystemsMigratedAtP5e` |
| `MG_Pipe/generated/PipeWire.inc` | `MGPWireOp::SetProgramBindings` (80), `kMGPipeWaitClasses[]`, `MGPipeWaitClassFor(op)` |
| `MG_Pipe/PipeApply.h` | `MGPipeBarriered(op, payload, st)` (implemented), `MGPipeApplierCurrentRecordIsBarriered()` / `MGPipeApplierSetCurrentRecordBarriered()` (implemented, default `true`), `MGPipeApplySetShaderBuffers` and `MGPipeApplySetProgramBindings` (declared, aborting), `MGPipeApplierState::{BoundShaderBuffers, ShaderBufferStart, ShaderBufferCount, ShaderBufferWritableMask, ShaderBuffersSerial, IsTransformFeedbackActive}`, `MGPipeShaderCsoRecord::{BlockBindings, SamplerUnits, StorageOverrides, Signature, BindingsSerial}`, `MGPipeProgramStorageOverride` |
| `MG_Impl/Pipe/SetHashSuppressor.h` | `MGPipeSuppressorSlot::{SetShaderBuffersUniform, SetShaderBuffersShaderStorage, SetShaderBuffersAtomicCounter, SetProgramBindings}` |
| `MG_Impl/Pipe/Tracker.h` | `kMGPipeDirtyEmittedAtP5e`; bits 15/16/17 mapped onto bit 13 |
| `MG_Impl/Pipe/ImageEmit.h` | `MGPipeEncodeImageAccess` promoted to a free function over the shared table |
| `MG_Backend/DirectGLES/Managers.h` | `VertexArrayImpl::ResolveVaoTwin(handle)`, `TextureImpl::SyncTextureToBackendByHandle`, `TextureImpl::ResolveTextureTwin`, `BackendTextureObject::SyncMipmapsToBackendByHandle`, `BackendFramebufferObject::SyncToBackendByHandle`, `BackendRenderbufferObject::SyncToBackendByHandle`, `BackendProgramObjectImpl::SyncToBackendByHandle`, `PrgramImpl::ResolveProgramTwin` — all declared, all aborting by name, one block at the end of `Managers.cpp` |
| `Config.h` / `ConfigLoader.cpp` | `IpcTable::RunAhead`, `IpcTable::PresentCredit`; the push default is `kMGPipeSubsystemsMigratedAtP5e` |
| `MG_Backend/Init.cpp` | `kMGPipeP5eRunAheadReady` (`false`) and the caps arm through `MGPipeRunAheadCapBitsFor` |
**The three red-onces c0e ran, and what they name.** (1) flipping one row's `WaitClass` without
regenerating → `gen_pipe: OUT OF DATE: MobileGL/MG_Pipe/generated/PipeWire.inc`, rc 1; with the
regeneration, `PipeCatalogue.EveryRowCarriesTheWaitClassTheContractGivesIt` fails naming
`GenerateMipmap`. (2) `sizeof(MGPProgramBindings)` 32 → 40 →
`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` fails "Which is: 40 / 32". (3) making
the caps arm ignore the backend type →
`MagmaPipeIdentityTest.AMagmaServerNeverPublishesTheRunAheadCapBit` fails "Which is: 1024 / 0".
+84 -3
View File
@@ -242,7 +242,8 @@ namespace MobileGL::MG_Remote::Wire {
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \
X(ApplierReset, MGPApplierReset) \
X(ObjectDeath, MGPHandleOnly) \
X(SetContextValues, MGPContextValues)
X(SetContextValues, MGPContextValues) \
X(SetProgramBindings, MGPProgramBindings)
namespace {
@@ -602,6 +603,7 @@ namespace MobileGL::MG_Remote::Wire {
// believed, and the bound is the header's own 32-bit Size field.
Uint64 tail0 = 0;
Uint64 tail1 = 0;
Uint64 tail2 = 0; // P5e: set_program_bindings is the one three-tail row
Uint32 tails = 0;
switch (op) {
case MGPWireOp::SetVertexBuffers: {
@@ -667,6 +669,47 @@ namespace MobileGL::MG_Remote::Wire {
tails = 2;
break;
}
case MGPWireOp::SetProgramBindings: {
// THREE TAILS, THREE COUNTS, THREE INDEX SPACES (P5e,
// MG_Remote/CONTRACT-P5E.md §1): Int32[BlockBindingCount] in GL uniform-block index
// order, MGPProgramSamplerUnit[SamplerUnitCount] ascending by Location, and
// MGPProgramStorageOverride[StorageOverrideCount] whose Name is a host span staged
// whole - the override map is name-keyed by design.
//
// EACH COUNT IS REFUSED AGAINST ITS DECLARED BOUND, never truncated: a sampler
// array past 256 locations is a program this record cannot describe, and silently
// describing 256 of them would put uniforms on the wrong units with no marker
// anywhere. The counts come off the wire, so every one is bounded before it is
// multiplied.
const auto& p = *static_cast<const MGPProgramBindings*>(payload);
if (p.BlockBindingCount > kMGPipeMaxProgramBlockBindings) {
WireProtocolFatalAt("SetProgramBindings.BlockBindingCount", p.BlockBindingCount,
kMGPipeMaxProgramBlockBindings);
}
if (p.SamplerUnitCount > kMGPipeMaxProgramSamplerUnits) {
WireProtocolFatalAt("SetProgramBindings.SamplerUnitCount", p.SamplerUnitCount,
kMGPipeMaxProgramSamplerUnits);
}
if (p.StorageOverrideCount > kMGPipeMaxProgramStorageOverrides) {
WireProtocolFatalAt("SetProgramBindings.StorageOverrideCount",
p.StorageOverrideCount, kMGPipeMaxProgramStorageOverrides);
}
tail0 = TailBytesFor(p.BlockBindingCount, sizeof(Int32));
tail1 = TailBytesFor(p.SamplerUnitCount, sizeof(MGPProgramSamplerUnit));
tail2 = TailBytesFor(p.StorageOverrideCount, sizeof(MGPProgramStorageOverride));
// ALWAYS THREE, even when a count is 0. The tails are positional and the third one's
// offset is derived from the first two, so a record that declared "one tail" when
// only the block bindings were present would put the same bytes at a different
// offset than a record that declared three - and the two halves of the wire would
// disagree about where the overrides start. An empty tail is zero bytes at an
// 8-aligned offset, which costs nothing.
tails = 3;
// NOT SecondTailIsHostSpans: the spans are MEMBERS of the third tail's elements,
// not a bare MGHostSpan array, so the encoder's blanket honesty pass would read 40
// bytes of {span, binding, pad} as one span and a quarter of garbage. The arm in
// ApplyChecked walks the elements and checks each Name itself.
break;
}
case MGPWireOp::SetVertexAttribDefaults: {
// TWO DECLARANTS THAT MUST AGREE (contract table 1 row 15): Count and
// popcount(Mask). Nothing checks this today; a disagreement is a wire fault that
@@ -749,6 +792,15 @@ namespace MobileGL::MG_Remote::Wire {
out.TailBytes[1] = tail1;
out.TotalBytes = out.TailOffset[1] + tail1;
}
// P5e: the third tail, realigned to 8 for the second one's reason - set_program_bindings'
// first tail is Int32[] and its elements are four bytes, so the array behind it would
// otherwise start at a 4-aligned offset and every MGHostSpan in the third would be
// misaligned on a host that cares.
if (tails >= 3) {
out.TailOffset[2] = Align8(out.TailOffset[1] + tail1);
out.TailBytes[2] = tail2;
out.TotalBytes = out.TailOffset[2] + tail2;
}
out.TailCount = tails;
out.TotalBytes = Align8(out.TotalBytes);
if (out.TotalBytes > kMaxRecordBytesOnTheWire) {
@@ -1435,12 +1487,14 @@ namespace MobileGL::MG_Remote::Wire {
// record declaring Count = 4000 while carrying 8 bytes to its own arithmetic.
if (size != layout.TotalBytes) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"%s\"} the record declares Size=%llu and "
"its own count fields describe %llu bytes (fixed payload %llu + tails %llu/%llu)",
"its own count fields describe %llu bytes (fixed payload %llu + tails "
"%llu/%llu/%llu)",
WireOpName(op), static_cast<unsigned long long>(size),
static_cast<unsigned long long>(layout.TotalBytes),
static_cast<unsigned long long>(layout.PayloadBytes),
static_cast<unsigned long long>(layout.TailBytes[0]),
static_cast<unsigned long long>(layout.TailBytes[1]));
static_cast<unsigned long long>(layout.TailBytes[1]),
static_cast<unsigned long long>(layout.TailBytes[2]));
std::abort();
}
@@ -1806,6 +1860,33 @@ namespace MobileGL::MG_Remote::Wire {
// Same as above: no applier, off the reduced path, both tails validated.
return false;
case MGPWireOp::SetProgramBindings: {
// P5e, opcode 80 (MG_Remote/CONTRACT-P5E.md §1). NO APPLIER ENTRY POINT EXISTS YET
// and c0e does not invent one: the record's consumer is the program twin's
// rebuild, which package pg writes. What the record needs NOW and cannot get later
// is its shape - the three declared counts against their bounds and the three-tail
// arithmetic, both of which MGPipeWireRecordLayout above has already run - plus the
// honesty of every name span it carries.
//
// THE SPANS ARE WALKED HERE RATHER THAN BY THE ENCODER'S BLANKET PASS because they
// are MEMBERS of the third tail's 40-byte elements, not a bare MGHostSpan array:
// SecondTailIsHostSpans reads a tail AS spans, and doing that to {span, binding,
// pad} would read one span and a quarter of garbage. All four arms of
// CheckHostSpanIsHonest run, the segment-range one included, so an override name
// pointing outside SEG_STAGE is caught on the way in rather than at the rebuild.
if (layout.TailCount == 3 && layout.TailBytes[2] != 0 && m_segments != nullptr) {
const auto* overrides = tailAt(2);
const Uint64 count = layout.TailBytes[2] / sizeof(MGPProgramStorageOverride);
for (Uint64 i = 0; i < count; ++i) {
MGPProgramStorageOverride entry{};
std::memcpy(&entry, overrides + i * sizeof(MGPProgramStorageOverride),
sizeof(MGPProgramStorageOverride));
CheckHostSpanIsHonest(entry.Name, *m_segments);
}
}
return false;
}
case MGPWireOp::SetGlobalConstants: {
const auto& rec = *static_cast<const MGPGlobalConstants*>(payload);
// The length cross-check is the applier's (PipeApply.cpp:2784, against the
+18 -7
View File
@@ -171,10 +171,19 @@ namespace MobileGL::MG_Remote::Wire {
// holds a pointer and two Uint64s. P5 emits no host span at all, so this rule costs
// nothing now and is stated now because the phase that arms kDrawHasUserIndices would
// otherwise have to discover it as a misaligned load on a device.
// THREE TAILS AND NOT TWO SINCE P5e. set_program_bindings (opcode 80,
// MG_Remote/CONTRACT-P5E.md §1) carries the uniform-block bindings, the sampler units and
// the storage-override map as three arrays in three different index spaces, so the array
// that used to be exactly "the two-tail rows plus draw_vbo's conditional second" grew by
// one. Every loop over TailCount below already reads the count rather than the literal 2;
// what changed is the storage and the assembly.
inline constexpr Uint32 kMaxWireRecordTails = 3;
struct WireRecordLayout {
Uint64 PayloadBytes = 0; // sizeof the op's payload struct
Uint64 TailOffset[2] = {0, 0}; // from the START of the record, header included
Uint64 TailBytes[2] = {0, 0};
Uint64 TailOffset[kMaxWireRecordTails] = {0, 0, 0}; // from the START of the record,
// header included
Uint64 TailBytes[kMaxWireRecordTails] = {0, 0, 0};
Uint32 TailCount = 0;
Uint64 TotalBytes = 0; // header + payload + gaps + tails, rounded up to 8
// P5b: WHAT THE SECOND TAIL IS. SetShaderBuffers' second tail is MGHostSpan[HostSpanCount]
@@ -194,9 +203,9 @@ namespace MobileGL::MG_Remote::Wire {
// The catalogue's own spelling of an opcode, for a Fatal line. Out of range is "<opcode>".
const char* WireOpName(MG_Pipe::MGPWireOp op);
// One tail array. Two of the 71 rows carry two (SetShaderBuffers, SetStreamOutputTargets)
// and DrawVbo carries a conditional second one, which is why EncodeRecord's one-tail form
// could not stay the only one.
// One tail array. Two of the rows carry two (SetShaderBuffers, SetStreamOutputTargets),
// DrawVbo carries a conditional second one and P5e's SetProgramBindings carries three,
// which is why EncodeRecord's one-tail form could not stay the only one.
struct WireTail {
const void* Bytes = nullptr;
Uint64 Size = 0;
@@ -234,8 +243,10 @@ namespace MobileGL::MG_Remote::Wire {
Uint64 EncodeRecord(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
const void* varTail = nullptr, Uint64 varTailBytes = 0);
// The same call for the three rows that carry TWO tails. The one-tail form above is
// this one with tailCount <= 1; nothing is duplicated between them.
// The same call for the rows that carry more than one tail - two for
// set_shader_buffers, set_stream_output_targets and draw_vbo's conditional second,
// three for P5e's set_program_bindings. The one-tail form above is this one with
// tailCount <= 1; nothing is duplicated between them.
//
// The tails a caller hands over are CROSS-CHECKED against the layout the payload
// itself declares (MGPipeWireRecordLayout): a caller whose Count says 4000 while its
+7 -2
View File
@@ -734,8 +734,13 @@ TEST_F(FieldOwnershipTest, StrictErrorsTurnsABarrierPulledReadIntoANamedAbort) {
EXPECT_NE(r.Log.find("BARRIER-PULLED"), std::string::npos) << r.Log;
EXPECT_NE(r.Log.find("MOBILEGL_IPC_STRICT_ERRORS=1"), std::string::npos) << r.Log;
// The strict line names the phase that owes the answer; a strict abort that did not would
// leave the reader exactly where the gate found them.
EXPECT_NE(r.Log.find("retires in P8]"), std::string::npos) << r.Log;
// leave the reader exactly where the gate found them. P5e re-annotated this row (and the
// five object-class rows beside it) from "P8" to the phase that actually retires the pull,
// which is why the expected text moved with FieldOwnership.def rather than the case being
// re-pointed at another field: the string IS the debt entry, read out of the generated
// table, and a case that stopped checking it would let the annotation rot.
EXPECT_NE(r.Log.find("retires in P5e (Espryt unbarriered), P7 (Magma)]"), std::string::npos)
<< r.Log;
}
TEST_F(FieldOwnershipTest, TheSameReadWithoutStrictErrorsSurvivesAndIsCounted) {
@@ -56,7 +56,8 @@ namespace {
X(MagmaPipeIdentityTest, AnIdleSlotIsRetiredAndReusedWithANewGeneration) \
X(MagmaPipeIdentityTest, AReusedSlotDoesNotServeItsPredecessorsMemo) \
X(MagmaPipeIdentityTest, TheAbaControlKnobServesTheStaleMemoAcrossAReusedSlot) \
X(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo)
X(MagmaPipeIdentityTest, ALiveObjectKeepsItsSlotItsGenerationAndItsMemo) \
X(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit)
#define MGL_DECLARE_PULL_SKIP(Suite, Name) \
TEST(Suite, Name) { GTEST_SKIP() << "compiled only under MOBILEGL_PIPE_PUSH"; }
@@ -213,5 +214,48 @@ namespace {
<< "a live object's memo was cleared without its slot changing owner";
EXPECT_EQ(mint.Count(), 1u);
}
// ---- P5e (MG_Remote/CONTRACT-P5E.md §1, §6, ruling 12) -------------------------------
//
// MAGMA DOES NOT RUN AHEAD, AND THIS IS WHERE THAT IS A TEST RATHER THAN A COMMENT.
// kCapRunAheadApply is the client's whole permission to publish a record and move on: the
// moment a server sets it, the apply thread promises it reads nothing of the client's. That
// promise is FALSE for Magma for the whole of P5e - the four apply-thread allocator sites
// inside MagmaP7AllocatorDebtScope are real debt P7 retires, and
// MGPipeApplierCurrentRecordIsBarriered() answering true for every record on a server
// without the bit is exactly what keeps them inside P5C's semantics and keeps rsp honest.
//
// The arm is a pure function precisely so this case can reach it: InitSplitRoles needs a
// live session, a backend and a handshake, and none of those belong in a unit lane. RED
// ONCE by making MGPipeRunAheadCapBitsFor answer for DirectVulkan too (the exact
// perturbation the phase's red-once list names) - both EXPECTs below fail, by name.
TEST_F(MagmaPipeIdentityTest, AMagmaServerNeverPublishesTheRunAheadCapBit) {
// Whatever the integration constant says. `true` is what the P5e integration commit
// will pass, so the Magma answer is pinned on BOTH sides of that flip and the case
// does not quietly stop asserting anything the day the constant moves.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/false) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
// And the Espryt half, so the case says what the arm IS and not only what it is not:
// the bit is published for DirectGLES and ONLY once the integration commit flips
// kMGPipeP5eRunAheadReady. Until then every P5e package lands with the wait rule inert.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/false) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
0u);
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true) &
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply),
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
// Bit 10 and nothing else: the arm contributes ONE bit, so a future editor cannot fold
// an unrelated capability into it and have the two pins above still pass.
EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true),
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
static_assert(static_cast<Uint64>(MG_Pipe::kCapRunAheadApply) == (1ull << 10),
"kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10");
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace
+192 -17
View File
@@ -65,8 +65,8 @@ TEST(PipeCatalogue, FrontendNeverTakesAnApplierAddress) {
namespace {
// Counting expansions of the catalogue. The Class parameter is a real enumerator, so a
// per-class count is a constant expression too.
#define MGP_COUNT_ONE(Name, Payload, Class, Flags) +1
#define MGP_COUNT_CLASS(Name, Payload, Class, Flags) +((Class) == countedClass ? 1 : 0)
#define MGP_COUNT_ONE(Name, Payload, Class, Flags, Wait) +1
#define MGP_COUNT_CLASS(Name, Payload, Class, Flags, Wait) +((Class) == countedClass ? 1 : 0)
constexpr SizeT kExpandedCallCount = 0 MGP_CALL_LIST(MGP_COUNT_ONE);
@@ -77,13 +77,32 @@ namespace {
// Every payload named in the catalogue must be a memcpy-able POD, and so must every
// payload the verify comparator knows about.
#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags) \
#define MGP_ASSERT_CALL_PAYLOAD_POD(Name, Payload, Class, Flags, Wait) \
static_assert(std::is_trivially_copyable_v<Payload>, #Name "'s payload " #Payload " is not trivially copyable");
MGP_CALL_LIST(MGP_ASSERT_CALL_PAYLOAD_POD)
#define MGP_ASSERT_VERIFY_PAYLOAD_POD(Payload) \
static_assert(std::is_trivially_copyable_v<Payload>, #Payload " is not trivially copyable");
MGP_VERIFY_PAYLOAD_LIST(MGP_ASSERT_VERIFY_PAYLOAD_POD)
// The catalogue's own spelling of an opcode, for a failure message. The wire codec has one
// of these and MG_Pipe may not reach it (MG_Remote is above this layer), so the table is
// expanded from the catalogue here rather than a name being duplicated per EXPECT.
#define MGP_CATALOGUE_NAME_ROW(Name, Payload, Class, Flags, Wait) #Name,
constexpr const char* kCatalogueOpNames[] = {
"<kInvalid>",
MGP_CALL_LIST(MGP_CATALOGUE_NAME_ROW)
};
#undef MGP_CATALOGUE_NAME_ROW
static_assert(sizeof(kCatalogueOpNames) / sizeof(kCatalogueOpNames[0]) ==
static_cast<SizeT>(MGPWireOp::kOpCount),
"the diagnostic name table and the opcode space disagree");
const char* WireOpNameForDiag(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount) ? kCatalogueOpNames[index]
: "<opcode out of range>";
}
} // namespace
// The handle is the whole object model. Eight bytes, a register pair, no padding.
@@ -127,11 +146,12 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// The per-class counts PipeCalls.def documents in its header.
// kScreen is 11 + P5c's applier_reset (MG_Remote/CONTRACT-P5C.md §5.1); kCtxObject is
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record.
// kCtxState is 17 + P5c rv's set_context_values (§5.3), the residual-value record, + P5e's
// set_program_bindings (MG_Remote/CONTRACT-P5E.md §1), the post-link binding record.
EXPECT_EQ(ClassCount<kScreen>(), 12u);
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 18u);
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
EXPECT_EQ(ClassCount<kCtxObject>(), 10u);
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
// patch_parameter, bind_stream_output, set_storage_block_binding,
@@ -142,9 +162,10 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
// migrated, keep pulling" means (plan B section 4.1).
//
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 41 ROWS WITH
// NO MGPipeApply* ENTRY POINT (79 - the 38 that have one; the number was 34 at P5, 39 after
// P5b's five sink-only verbs, and rv's set_context_values grew BOTH sides of the difference):
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 42 ROWS WITH
// NO MGPipeApply* ENTRY POINT (80 - the 38 that have one; the number was 34 at P5, 39 after
// P5b's five sink-only verbs, rv's set_context_values grew BOTH sides of the difference, and
// P5e's set_program_bindings grew the null side alone):
// the other 38 have an applier, R-17 installs adapters over them,
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
@@ -161,7 +182,7 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
#if MOBILEGL_PIPE_PUSH
MGPipeInstallMonolithTables();
// The 41 rows with no MGPipeApply* entry point are still null, and null still means "this
// The 42 rows with no MGPipeApply* entry point are still null, and null still means "this
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
// count is the other case's job and two cases asserting the same number would both go red
// for one change. P5c's two control records are among them by design (CONTRACT-P5C.md §5:
@@ -177,6 +198,10 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
// P5e's opcode 80 joins them: the row is catalogued, its codec layout is written and its
// tails are validated, but there is no MGPipeApplySetProgramBindings and no route - package
// pg installs both. A null here is the contract, not an omission.
EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr);
#else
// A pull build compiles no applier and no routing, so the pre-migration statement is the
// whole truth there and this case is the one that says so.
@@ -251,6 +276,9 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
// notice's mailbox are the producers, byte for byte as before (G1/G2).
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
// P5e's set_program_bindings (CONTRACT-P5E.md §1): catalogued with a null route until pg
// installs the applier entry point and the adapter beside it.
EXPECT_EQ(gMGPipeContext.SetProgramBindings, nullptr);
#else
// A pull build compiles no applier and no routing, so the pre-migration statement is still
// the whole truth there.
@@ -584,7 +612,14 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
// - fixed POD, no blob, no tail, no reply - with an MGPipeApply* entry point, which the two
// control records deliberately do not have.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetContextValues), 79);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 80);
// P5e (MG_Remote/CONTRACT-P5E.md §1) appended ONE more, by the same rule: opcode 80, and
// nothing before it moved. set_program_bindings is a kCtxState row with THREE tails and a
// host span in the third; it lands with a null route and a sink that refuses it by name,
// exactly as set_shader_buffers has sat catalogued-and-dead since P4a.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetProgramBindings), 80);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 81);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetProgramBindings),
static_cast<Uint32>(kVarTail | kHostSpan));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast<Uint32>(kNone));
@@ -618,9 +653,44 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(sizeof(MGPContextValues::TouchedBufferBindingPointCount), 60u);
EXPECT_EQ(sizeof(MGPAttribValue), 56u);
EXPECT_EQ(sizeof(MGPVertexAttribDefaults), 8u);
// P5e's payloads (CONTRACT-P5E.md §1), pinned at runtime like every other MGP_ASSERT_POD.
//
// MGPProgramDesc GREW 192 -> 200, and that is the phase's one wire-format widening: the
// descriptor's four trailing Uint8s end exactly at offset 24 and MGPBlobRef is 8-aligned,
// so there was no spare byte for LinkStatus and the draft's "in the descriptor's existing
// pad" was wrong. Pinned here, by value, because a record whose size moved silently is a
// protocol break no other test would see - and because THIS number is the red-once the
// phase names: change MGPProgramBindings' or the descriptor's size and this case fails.
EXPECT_EQ(sizeof(MGPProgramDesc), 200u);
EXPECT_EQ(sizeof(MGPProgramBindings), 32u);
EXPECT_EQ(sizeof(MGPProgramSamplerUnit), 8u);
EXPECT_EQ(sizeof(MGPProgramStorageOverride), 40u);
// The three declared tail bounds. A program past one of them is a COUNTED REFUSAL and never
// a truncation, so the numbers are the contract rather than an implementation detail.
EXPECT_EQ(kMGPipeMaxProgramBlockBindings, 64u);
EXPECT_EQ(kMGPipeMaxProgramSamplerUnits, 256u);
EXPECT_EQ(kMGPipeMaxProgramStorageOverrides, 64u);
// LinkStatus is a real byte and a zeroed descriptor means "not linked", which is what makes
// the server's decline the safe default.
MGPProgramDesc program{};
EXPECT_EQ(program.LinkStatus, 0u);
// set_shader_buffers' capacity, pinned on the wire side; MG_Impl/Pipe/PipeFill.cpp is the
// one translation unit that also sees BufferState.h's constant and pins them together.
EXPECT_EQ(kMGPipeMaxBufferBindingPoints, 84u);
// The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value.
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
// P5e's draw flag: the next free bit, and disjoint from all five before it.
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays), 1u << 6);
EXPECT_EQ(static_cast<Uint32>(kDrawClientArrays) &
static_cast<Uint32>(kDrawHasUserIndices | kDrawPrimitiveRestart |
kDrawIndicesAreClient | kDrawHasIndexRange |
kDrawHasXfbCount | kDrawIsIndirect),
0u);
// ... and it still fits the one byte MGPDrawInfo::Flags is, which is why the record did not
// grow: MGPDrawInfo stays 56 bytes.
EXPECT_EQ(sizeof(MGPDrawInfo), 56u);
EXPECT_LE(static_cast<Uint32>(kDrawClientArrays), 0xFFu);
// The clear discriminants have ONE spelling now, and Whole is 0 so a zeroed record is a
// whole-framebuffer clear.
EXPECT_EQ(kMGPipeClearKindWhole, 0u);
@@ -628,6 +698,107 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
EXPECT_EQ(kMGPipeClearValueClassUint + 1, kMGPipeClearValueClassCount);
}
// ---- P5e: the wait-class column (MG_Remote/CONTRACT-P5E.md §2.2) --------------------------
//
// THE POINT OF THIS CASE IS THAT IT DOES NOT READ PipeCalls.def. Every other statement about
// the column - the generated table, the two gates in gen_pipe.py, the static_asserts in
// PipeWire.inc - is derived from the def, so all of them move together when a row is edited.
// This case re-states CONTRACT-P5E §2.2's table BY NAME, in the contract's own grouping, so a
// row silently moved from kWaitApplied to kWaitNone has to be argued in two places or it goes
// red here. That is the difference between a table and a contract.
//
// It matters because the column is not decoration: on a run-ahead server kWaitNone is a promise
// that the record's apply reads NOTHING of the client's (rule F), and a row demoted to it by
// accident is a torn read that renders wrong rather than aborting.
TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) {
// kWaitPresent: present alone, paced by the credit taken BEFORE the encode (§2.4).
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::Present), kWaitPresent);
// kWaitReply: every row whose answer is not derivable (R-5). Named, all fourteen, because
// the flag-to-class agreement is what keeps a waiter from hanging on a slot nobody posts.
const MGPWireOp replyRows[] = {
MGPWireOp::GetCaps, MGPWireOp::ResourceCreate, MGPWireOp::ResourceRespecify,
MGPWireOp::MapPersistent, MGPWireOp::FenceStatus, MGPWireOp::FenceWait,
MGPWireOp::QueryAvailable, MGPWireOp::QueryResult, MGPWireOp::QueryTimestamp,
MGPWireOp::SetTextureParams, MGPWireOp::ResourceSubData, MGPWireOp::ResourceReadback,
MGPWireOp::GetTextureImage, MGPWireOp::ReadPixels,
};
for (const MGPWireOp op : replyRows) {
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitReply) << WireOpNameForDiag(op);
EXPECT_NE(MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot), 0u)
<< WireOpNameForDiag(op);
}
// kWaitApplied: the rows whose apply STILL reads a BARRIER_PULLED row or probes the client
// allocator, so the client parks in its own wait and CONTRACT-P5C's semantics hold for them
// unchanged - their pulls count into rsp and their probes are legal inside a scope. Two of
// them are trailing items and are expected to leave this list in a later package:
// generate_mipmap the moment tx2 resolves the texture from VerbMipRes instead of the active
// unit, and set_storage_block_binding the moment pg resolves it through
// MGPStorageBlockBinding::ShaderCso. When they do, THIS list is what has to be edited.
const MGPWireOp appliedRows[] = {
MGPWireOp::ApplierReset, MGPWireOp::GenerateMipmap,
MGPWireOp::SetStorageBlockBinding, MGPWireOp::BeginStreamOutput,
MGPWireOp::EndStreamOutput, MGPWireOp::PauseStreamOutput,
MGPWireOp::ResumeStreamOutput, MGPWireOp::BindStreamOutput,
MGPWireOp::CopyFramebufferToTexture,
};
for (const MGPWireOp op : appliedRows) {
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitApplied) << WireOpNameForDiag(op);
}
// kWaitNone: the steady draw path and every state/CSO/object row without a reply. Named for
// the rows the phase exists for, and then counted for the rest, so a new row cannot join
// the set unnoticed.
const MGPWireOp noneRows[] = {
MGPWireOp::DrawVbo, MGPWireOp::LaunchGrid, MGPWireOp::Clear,
MGPWireOp::Blit, MGPWireOp::MemoryBarrier, MGPWireOp::Flush,
MGPWireOp::SetSwapInterval, MGPWireOp::BindShaderImage, MGPWireOp::PatchParameter,
MGPWireOp::ObjectDeath, MGPWireOp::ResourceDestroy, MGPWireOp::UnmapPersistent,
MGPWireOp::FenceCreate, MGPWireOp::FenceDestroy, MGPWireOp::FenceWaitServer,
MGPWireOp::QueryCreate, MGPWireOp::QueryBegin, MGPWireOp::QueryEnd,
MGPWireOp::QueryDestroy, MGPWireOp::QueryCounter, MGPWireOp::SetContextValues,
MGPWireOp::SetProgramBindings, MGPWireOp::SetShaderBuffers,
MGPWireOp::SetVertexBuffers, MGPWireOp::SetFramebufferState,
MGPWireOp::CreateShaderState, MGPWireOp::SetDrawProgram,
};
for (const MGPWireOp op : noneRows) {
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitNone) << WireOpNameForDiag(op);
}
// The partition, by count. 14 + 1 + 9 = 24 rows wait; every other row of the catalogue does
// not. A row that changed class moves two of these numbers at once.
SizeT reply = 0, applied = 0, present = 0, none = 0, other = 0;
for (SizeT i = 1; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
switch (MGPipeWaitClassFor(static_cast<MGPWireOp>(i))) {
case kWaitReply: ++reply; break;
case kWaitApplied: ++applied; break;
case kWaitPresent: ++present; break;
case kWaitNone: ++none; break;
default: ++other; break;
}
}
EXPECT_EQ(reply, 14u);
EXPECT_EQ(applied, 9u);
EXPECT_EQ(present, 1u);
EXPECT_EQ(none, static_cast<SizeT>(kMGPipeCallCount) - 24u);
EXPECT_EQ(other, 0u) << "a row carries the kWaitClassCount terminator as its class";
// And the reply half of the partition BOTH WAYS, over the whole catalogue: exactly the rows
// that own a slot wait for one. gen_pipe.py refuses a catalogue where they disagree; this
// is the same statement where a reader of the table will look for it.
for (SizeT i = 1; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
const auto op = static_cast<MGPWireOp>(i);
const Bool ownsSlot = (MGPipeCallFlagsFor(op) & static_cast<Uint32>(kReplySlot)) != 0;
EXPECT_EQ(ownsSlot, MGPipeWaitClassFor(op) == kWaitReply) << WireOpNameForDiag(op);
}
// Opcode 0 is not a call and has no class, for the flags table's reason: a decoder holding
// a byte off a corrupt stream must reach its own Fatal rather than a wait decision.
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kInvalid), kWaitClassCount);
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::kOpCount), kWaitClassCount);
}
// A well-formed record passes the applier's bounds gate. P0 has no applier, so "accepted"
// is reported as "not applied" rather than "fatal".
TEST(PipeCatalogue, ApplierAcceptsAWellFormedRecord) {
@@ -817,8 +988,12 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
// each with its own field list, so the comparator sees every one of them: 77. P5c appended
// applier_reset's MGPApplierReset (CONTRACT-P5C.md §5.1) - object_death reuses
// MGPHandleOnly, which has had a list since P0 - and rv added set_context_values'
// MGPContextValues (§5.3): 79.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 79u);
// MGPContextValues (§5.3): 79. P5e appended set_program_bindings' MGPProgramBindings AND
// the two TAIL ELEMENT types beside it, MGPProgramSamplerUnit and MGPProgramStorageOverride
// (MG_Remote/CONTRACT-P5E.md §1) - the tails are listed for the same reason MGPBufferRange
// and MGPVertexAttribWire are: the comparator has to see INTO an element whose members
// include an MGHostSpan and a pad word, or it would memcmp the padding: 82.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 82u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
@@ -925,7 +1100,7 @@ TEST(PipeCatalogue, BufferRangeCarriesNoInlineHostSpan) {
// The call still declares the span it may carry, so the transport lays the tail out.
Uint32 flags = 0;
#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags) \
#define MGP_FLAGS_OF_SET_SHADER_BUFFERS(Name, Payload, Class, Flags, Wait) \
if (std::strcmp(#Name, "SetShaderBuffers") == 0) flags = static_cast<Uint32>(Flags);
MGP_CALL_LIST(MGP_FLAGS_OF_SET_SHADER_BUFFERS)
#undef MGP_FLAGS_OF_SET_SHADER_BUFFERS
@@ -1048,7 +1223,7 @@ TEST(PipeCatalogue, VertexWireViewsAreFlatAndCarryIsLongSeparately) {
// inherits this pin rather than the guess.
TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
Uint32 flags = 0;
#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags) \
#define MGP_FLAGS_OF_RESOURCE_RESPECIFY(Name, Payload, Class, Flags, Wait) \
if (std::strcmp(#Name, "ResourceRespecify") == 0) flags = static_cast<Uint32>(Flags);
MGP_CALL_LIST(MGP_FLAGS_OF_RESOURCE_RESPECIFY)
#undef MGP_FLAGS_OF_RESOURCE_RESPECIFY
@@ -1056,7 +1231,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
// And it is the ONLY call that carries it: a second one would be a second decision, and
// this predicate answers for exactly one call.
Uint32 ackingCalls = 0;
#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags) \
#define MGP_COUNT_ACKING_CALLS(Name, Payload, Class, Flags, Wait) \
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kNeedsAck)) != 0) ++ackingCalls;
MGP_CALL_LIST(MGP_COUNT_ACKING_CALLS)
#undef MGP_COUNT_ACKING_CALLS
@@ -1071,7 +1246,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
// ROWS, whose applier entry points return a Bool the client acts on destructively and which
// carried no flag because in monolith that answer is a direct call's return value.
Uint32 replySlotCalls = 0;
#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags) \
#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags, Wait) \
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) ++replySlotCalls;
MGP_CALL_LIST(MGP_COUNT_REPLY_SLOT_CALLS)
#undef MGP_COUNT_REPLY_SLOT_CALLS
@@ -1082,7 +1257,7 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
// (PipeApply.h:820, :868, :897, :1023); map_persistent's void* is the fifth answer and was
// already declared.
Uint32 acceptanceWithSlot = 0;
#define MGP_COUNT_ACCEPTANCE_ROWS(Name, Payload, Class, Flags) \
#define MGP_COUNT_ACCEPTANCE_ROWS(Name, Payload, Class, Flags, Wait) \
if ((std::strcmp(#Name, "ResourceCreate") == 0 || std::strcmp(#Name, "ResourceRespecify") == 0 || \
std::strcmp(#Name, "ResourceSubData") == 0 || std::strcmp(#Name, "SetTextureParams") == 0) && \
(static_cast<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) { \
+27 -12
View File
@@ -362,29 +362,44 @@ namespace {
// keeps the name it was born with and follows the phase constant instead of a literal
// five: what it has always asserted is "a bit names a subsystem if and only if this build
// emits a call for it", which is the property the emission gate and the residual-fill
// skip both rest on. P3a took the vertex-input family over and P4a takes seven more bits
// across four subsystems, so the set it compares against is now kMGPipeDirtyEmittedAtP4a -
// and a bit that gained an arm without gaining an emitter, or the reverse, still fails
// here.
// skip both rest on. P3a took the vertex-input family over, P4a took seven more bits across
// four subsystems, and P5e takes THE LAST THREE onto kMGPipeSubsystemBufferBindings, so the
// set it compares against is now kMGPipeDirtyEmittedAtP5e - and a bit that gained an arm
// without gaining an emitter, or the reverse, still fails here.
TEST_F(TrackerWalk, OnlyTheFiveEmittedBitsNameASubsystem) {
for (SizeT i = 0; i < kMGPipeDirtyCount; ++i) {
const auto bit = static_cast<MGPipeDirty>(i);
const Bool emitted = (kMGPipeDirtyEmittedAtP4a & MGPipeDirtyBit(bit)) != 0;
const Bool emitted = (kMGPipeDirtyEmittedAtP5e & MGPipeDirtyBit(bit)) != 0;
EXPECT_EQ(MGPipeSubsystemForDirty(bit) != 0, emitted) << kMGPipeDirtyNames[i];
}
// Each phase's constant SURVIVES as the next phase's A/B control, so the three are
// pinned as a chain rather than one being edited into the next: 0x1ff is P4a's "T2"
// arm and 0x7f is P3a's, and an operator's recorded mask has to keep meaning what it
// meant.
EXPECT_EQ(kMGPipeDirtyEmittedAtP5e & kMGPipeDirtyEmittedAtP4a, kMGPipeDirtyEmittedAtP4a);
EXPECT_EQ(kMGPipeDirtyEmittedAtP4a & kMGPipeDirtyEmittedAtP3a, kMGPipeDirtyEmittedAtP3a);
EXPECT_EQ(kMGPipeDirtyEmittedAtP3a & kMGPipeDirtyEmittedAtP2, kMGPipeDirtyEmittedAtP2);
// The three bits P4a still does not emit for - the const-buffer, shader-buffer and
// stream-output sets - name no subsystem, so their fields keep going through the
// residual fill. Stated positively as well as through the loop above, because "only
// these three are left" is the phase's own scope statement.
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewConstBuffers), 0u);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers), 0u);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSoTargets), 0u);
// THE LAST THREE BITS, which P4a still did not emit for and P5e takes: the const-buffer,
// shader-buffer and stream-output sets are ONE family - the indexed buffer binding
// points - on ONE subsystem, so an operator clearing bit 13 gets the whole family's
// frontend walk back rather than two thirds of it. Stated positively as well as through
// the loop above, because "these three, together" is the phase's own scope statement.
//
// set_stream_output_targets stays UNEMITTED for the whole of P5e (XFB is lockstep by
// escalation, MG_Remote/CONTRACT-P5E.md §5.7) and still names the subsystem, for the
// same reason: the bit answers "which A/B switch owns this family's legacy arm", not
// "is there a record on the wire for me today".
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewConstBuffers),
kMGPipeSubsystemBufferBindings);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewShaderBuffers),
kMGPipeSubsystemBufferBindings);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewSoTargets),
kMGPipeSubsystemBufferBindings);
// AND THE PHASE CONSTANT IS NOW THE WHOLE BIT SET: after P5e every dirty bit names a
// subsystem, so the loop above has no "names none" case left to exercise, and this is
// what says so rather than leaving the loop looking stronger than it is.
EXPECT_EQ(kMGPipeDirtyEmittedAtP5e,
static_cast<Uint32>((Uint64{1} << kMGPipeDirtyCount) - 1u));
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack);
EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState);
+7 -5
View File
@@ -11,7 +11,8 @@
| **P5b** inproc 下的 verb 迁移(Minecraft 优先) | **已收官(2026-09-16)** | `37fc4fdb..82683d4a`;`MEASUREMENTS.md` §7 |
| **P5c** `inproc` 共享内存读点归零 | **已收官(2026-09-17)** | `11ac3de6..b88e8487` + triage 修复;契约 `MobileGL/MG_Remote/CONTRACT-P5C.md`;审计 `~/w7/notes/p5c/p5c-audit-v1.md` |
| **P5d** `inproc` 性能专项 | **已收官(2026-09-18,三轮)** | `cb06538c`、`56a77348`、`1f8de61b`;报告 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md);`MEASUREMENTS.md` §9 |
| P6 spawn transport | **下一个** | 届时只是传输替换 |
| **P5e** 退役 Espryt draw path 的 lockstep | **进行中** | 契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`(c0e 落地);计划 `~/w7/notes/p5e/BRIEF-P5E.md`、裁定 `~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`(ID-80..98);包序 c0e → id → {vi, sb, pg, tx2, fb} ∥ ra,集成 commit 翻 `kMGPipeP5eRunAheadReady` |
| P6 spawn transport | P5e 之后 | 届时只是传输替换 |
## 2. 当前头实测
@@ -85,10 +86,11 @@
## 6. 下一步
0. P5d 的遗留(`P5D-INPROC-PERFORMANCE.md` "什么没完成"):R-1 序列化留给 P3b/P4b → P11(`gPipeInputs` 版本化已写进 P11 行);线程放置记录;小项随 P3b/P4b 顺手。
1. **P6 spawn transport**:`SocketTransport` + `ServerMain` + 握手 / 退出语义 + EGL forwarder 的控制面帧;P5c 之后这只是传输替换。注意 P5c 留下的:`s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置;两个豁免 scope 里的探测在 spawn 下根本不存在对应内存,P6 第一天的红就是它们的清单。
2. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、`texture-remint-pull` 仿真槽)。
3. P6 出口门:P5b 的完整渲染路径在 `spawn` 下绿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。
4. Redmi 四臂复测(P5c 的记录项,需设备窗口);79 trace 普查重跑(需全集语料)。
1. **P5e 退役 draw path 的 lockstep**(进行中):c0e 已落地契约与线上行——`PipeCalls.def` 的第五列 `WaitClass` + 生成的 `MGPipeWaitClassFor(op)`、`set_program_bindings`(opcode 80,空路由)、`kCapRunAheadApply`(位 10,只在 DirectGLES 臂且只在 `kMGPipeP5eRunAheadReady` 为真时发布)、`kDrawClientArrays`、子系统位 13 与 push 默认 `0x3fff`、`MGPipeImageAccess` 一张表、`MOBILEGL_IPC_RUN_AHEAD` / `MOBILEGL_IPC_PRESENT_CREDIT`、以及跨包 seam(`MGPipeBarriered`、两个 applier 入口、六个 by-handle 后端签名)。全部 inert:caps 位未发布前 client 跑今天的 lockstep 路径。下一个是 **id**(registry 重键 + by-handle resolver + 分配器守卫),然后 {vi, sb, pg, tx2, fb} 并行、ra 从第一天起并行。
2. **P6 spawn transport**:`SocketTransport` + `ServerMain` + 握手 / 退出语义 + EGL forwarder 的控制面帧;P5c 之后这只是传输替换。注意 P5c 留下的:`s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置;两个豁免 scope 里的探测在 spawn 下根本不存在对应内存,P6 第一天的红就是它们的清单。
3. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、`texture-remint-pull` 仿真槽)。
4. P5e 出口门(`~/w7/notes/p5e/BRIEF-P5E.md` §3 / §4):`integration-split-strict` 转硬绿车道、三条阴性对照、Redmi 四臂(monolith / lockstep / credit 1 / credit 2)。P6 出口门:P5b 的完整渲染路径在 `spawn` 下绿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。
5. Redmi 四臂复测(P5c 的记录项,需设备窗口);79 trace 普查重跑(需全集语料)。
## 7. 记录位置
+4 -3
View File
@@ -1,6 +1,6 @@
# MGPipe 路线图
> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b、P5c、P5d 已收官**(P5c:2026-09-17,代码头 `b88e8487`;P5d:三轮,2026-09-18,代码头 `1f8de61b`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**;设计见 `ARCHITECTURE.md`;逐阶段实测见 `MEASUREMENTS.md`;P5d 报告见 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md)。**下一个是 P6 spawn transport**(此时只是传输替换)。
> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b、P5c、P5d 已收官**(P5c:2026-09-17,代码头 `b88e8487`;P5d:三轮,2026-09-18,代码头 `1f8de61b`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**;设计见 `ARCHITECTURE.md`;逐阶段实测见 `MEASUREMENTS.md`;P5d 报告见 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md)。**当前在 P5e**(退役 Espryt draw path 的 lockstep,契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`);之后才是 P6 spawn transport(此时只是传输替换)。
## 通用纪律(每个 commit)
@@ -12,7 +12,7 @@
- **拆分不借机顺手修 `dev` 的 bug**:句柄化过程中发现的 `dev` 侧缺陷记成独立条目、独立 PR(开放问题 15、17)。
- 过程(用户 2026-09-16,ID-66):不过度验证;轮次间不做对抗性审查,每个 P 阶段收官由 Codex 审一次,发现进入下一阶段首轮;只读 / 非核心任务派给 Codex;实现方与审查方错开模型族。
两条跑道分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b → **P5c** → P6 → P9 → P10 → P11 → P12。目标顺序:先完整的 separate-thread rendering(P5b,已达成),再让 `inproc` 变成只经 wire 交换的诚实两角色(P5c),最后才是 separate-process transport(P6,此时只是传输替换)。
两条跑道分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b → P5c → P5d → **P5e** → P6 → P9 → P10 → P11 → P12。目标顺序:先完整的 separate-thread rendering(P5b,已达成),再让 `inproc` 变成只经 wire 交换的诚实两角色(P5c),再把它的 CPU 代价压到可接受(P5d),然后退役 draw path 上的 lockstep(P5e:client 发了就走,server 只从记录带来的句柄解析对象),最后才是 separate-process transport(P6,此时只是传输替换)。
## 阶段
@@ -28,7 +28,8 @@
| **P5b** `inproc` verb migration | ✅ `37fc4fdb..82683d4a` | class-C 普查决定顺序;d1 19 / i1 7 / t2 6 / f1 11 槽迁移;sync 五槽;具名 blit;GLES mip descriptor;r1 / r2;收官审查三项修复。发射表 A=2 / B=54 / C=15 | 主机全门 `348d22a4` complete;`integration-split` 107/107;合并普查零回退、79 trace 72/6/1;**Redmi 正确性 8/8 + 四臂 A/B、barrier tax 首测**。§7 |
| **P5c** `inproc` 共享内存读点归零 | ✅ `11ac3de6..b88e8487` | server 端纹理 staged shadow(`StagedTextureStore`,句柄为键、整段覆盖、defined-ness 追踪);`SEG_EVENT` 三个 producer + `kEventGlError`(反向通道零裸指针);sink / twin 按记录句柄解析(blit 具名臂经 verb 句柄工作区,Magma 走 G6 消费臂);`applier_reset` / `object_death` 控制记录(opcode 77/78,framebuffer 首个 wire delete);`set_context_values`(79)退役全部值类 BARRIER-PULLED 行,三 shutter 自答;双层 `Fatal{RoleViolation}` 守卫 + `InBarrierWait` 接线;两个具名豁免 scope(通告家族 P4b/P7、G6 registry 家族 P3b/P4b)承接未到期的债 | 角色守卫开启下 unit 2187/2187(strict 下同绿)、`integration-split` 111/111、每层守卫 red-once 按名变红;纹理 `0xDD` audit 在 bsl / complementary 两条 in-world trace 零 Fatal;rsp 按帧实测(bsl 948.5/帧,残留全为钉住的对象类 15 行);G1 三个认定 resize(SwapchainObject::Create / CopyTexSubImage2D / ScopedRestartIndexSubstitution,均已具名)+ `.text` −16 B、pull 构建零 MG_Remote 符号;G5 保护区字节一致;双生成器 --check/--self-test 绿 | P5b |
| **P5d** `inproc` 性能专项 | ✅ `cb06538c`、`56a77348`、`1f8de61b` | 一二轮:persistent-map 推送三级臂(mprotect 脏页追踪 + 哈希抑制 + 绑定过滤 `PushDrawConsumers`)、barrier 批处理 `MOBILEGL_IPC_BATCH_WAITS` + 延期销毁队列、自旋时钟 64 合 1、零页注册 + shadow 页对齐分配。三轮(按符号化 profile 逐项切,四包 + 一份 lockstep 可行性研究):apply 空转轮询零锁(控制邮箱原子影子位)、`Doorbell::Wait` 校准自旋稳态不读时钟、wait/park 计数进 stats;`OnApplyThread()` 内联为线程指针比较、三个 scope 深度计数器退掉 `thread_local`;shadow 分配尺寸补页、追踪范围向外对齐、边块哈希消失、脏页排空修复;image unit 高水位、残余填充 memo、server stamp 常量表;`generate_mipmap` 不再免等(apply 读残余输入)、death 测试围栏 | 三轮设备(Redmi,CPU 定频):inproc **64.3 → 103-106 fps**(p50),client 线程 CPU 15.0 → 9.2 ms/帧、apply 12.4 → 7.0-7.4;monolith 同场景 115(120 Hz vsync 封顶的运行)/ 206(一次未封顶,渲染线程 5.0 ms/帧 大核);配对比较以逐线程 CPU/帧为主,见报告"数字"节;unit 2203/2203、`integration-split` 111/111(两条曾 flaky 的用例复跑 6/6);双生成器、include 闭包、dirty-surface、doc 引用绿。未完成:R-1 序列化(研究结论:留给 P3b/P4b 后的 P11 `gPipeInputs` 版本化)、线程放置(内核忽略亲和)、`SetHashSuppressor` 哈希等小项。报告:[`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md) | P5b |
| **P6** spawn transport | P5c 之后 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch;EGL forwarder 的控制面帧;P5c 留下的 `s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 |
| **P5e** 退役 Espryt draw path 的 lockstep | 🚧 进行中 | 契约 `MG_Remote/CONTRACT-P5E.md`(规则 F:未设障记录的 apply 不得读任何 client 内存;19 条裁定见 §9)。八包:**c0e** 契约 + 线上行(`PipeCalls.def` 第五列 `WaitClass` + `MGPipeWaitClassFor`、`set_program_bindings` opcode 80 空路由、`kCapRunAheadApply` 位 10、`kDrawClientArrays`、子系统位 13、`MGPipeImageAccess` 一张表、两个旋钮、跨包 seam 声明)→ **id** 身份(registry 按 `{slot, gen}` 重键、by-handle resolver、分配器守卫)→ **vi / sb / pg / tx2 / fb** 五个 per-draw 家族并行(VAO / 缓冲绑定点 / program 归档 + 绑定 / 纹理 + sampler 窗口 / FBO + image)∥ **ra** 传输机制(等待规则 `EmitAndWaitTails`、present credit、`gPipeInputs` 变 server 角色内存、事件环流控)。集成 commit 翻 `kMGPipeP5eRunAheadReady` | 集成门(§3):unit 两臂绿;`integration-split` 111+ 绿 + inproc 普查零回退 + 79 trace 无新首阻塞;`integration-split-strict` 转**硬绿车道**(只允许 §7 白名单的 `[BARRIER-PULLED]` 标记,未设障记录 `rsp` = 0);三条阴性对照(`MOBILEGL_IPC_RUN_AHEAD=0` SSIM 一致、`VERB_BARRIER=0` 在设障行变红、Magma 上 `RUN_AHEAD=1` 记一条日志后跑 lockstep);每包 red-once 在集成树复跑;G1 0/0/0/0、G2、G14、G5;monolith push 四条 A/B trace SSIM 不变。设备出口(§4,Redmi 30 s 在世界窗口,四臂):fps ≥ lockstep、client 线程 CPU ms/帧 < lockstep、SSIM 不变,且逐 op 等待清单里**没有 `kWaitApplied` 行、没有逐 draw 的 reply 行** | P5d |
| **P6** spawn transport | P5e 之后 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch;EGL forwarder 的控制面帧;P5c 留下的 `s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 |
| **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 原生化;回读 / pack state;P4a 记下的 R-3 / R-4 / R-5 / R-7 / R-10 / R-11;`ProgramArtifacts.h` 的 NDK 尺寸钉 | 纹理 / program 场景;CTS `texture_*` / `shader_image_*` / `packed_pixels` 在 0.5 pp 内;每一个 Iris trace;`TextureUploadShapeScenario` 升级成门 |
| **P7** DirectVulkan(Magma)全量迁移 | 待排 | 其余 10 个子系统:`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化;具名 UBO host payload(D-B8);内部 shader 烘焙;`VertexInputStateFactory` 内容寻址 CSO;D18 容器纪律保留 | 集成 + trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;`nm -D libMobileGLServer.so \| grep glslang` 为空;CTS 0.5 pp 内。**再基线检查点:中点完成子系统 < 40% 立即重定基线** |
| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 待排 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,逐站点 reconcile);`Server/IndexHostMirror`;CopyImage 镜像搬到 client;viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;大 blob carrier(开放问题 11);无 present fence tick;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同;trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` fixture;`ClientArrayAfterComputeWriteScenario`;`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes` 逐用例发布 |
+123 -4
View File
@@ -136,12 +136,15 @@ def read(path):
class Call(object):
def __init__(self, index, name, payload, cls, flags):
def __init__(self, index, name, payload, cls, flags, wait="kWaitNone"):
self.Index = index # 1-based; this is the wire opcode
self.Name = name
self.Payload = payload
self.Class = cls
self.Flags = flags
# P5e: PipeCalls.def's fifth column. Defaulted so the self-test's canned Calls -
# which exercise the FLAG gates and say nothing about waiting - stay one line.
self.Wait = wait
@property
def IsScreen(self):
@@ -176,7 +179,8 @@ class Call(object):
return ", ".join(params), ", ".join(args)
CALL_RE = re.compile(r"^\s*X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*([\w|]+?)\s*\)\s*\\?\s*$")
CALL_RE = re.compile(r"^\s*X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*([\w|]+?)\s*,"
r"\s*(\w+)\s*\)\s*\\?\s*$")
def parse_calls():
@@ -195,7 +199,7 @@ def parse_calls():
match = CALL_RE.match(line)
if match:
calls.append(Call(len(calls) + 1, match.group(1), match.group(2), match.group(3),
match.group(4).split("|")))
match.group(4).split("|"), match.group(5)))
# The macro ends at the first line without a continuation backslash.
if not line.rstrip().endswith("\\"):
inside = False
@@ -564,6 +568,60 @@ def check_call_flags_are_known(calls):
% (call.Name, "|".join(f for f in call.Flags if f != "kNone")))
WAIT_ENUM_RE = re.compile(r"enum\s+MGPipeWaitClass\s*:\s*Uint8\s*\{(.*?)\}\s*;", re.S)
WAIT_MEMBER_RE = re.compile(r"^\s*(kWait\w+)\s*(?:=\s*\d+\s*)?,", re.M)
def parse_wait_classes():
"""The MGPipeWaitClass enumerators, READ OUT OF MGPipe.h, for parse_call_flags' reason:
this list is what the generated kMGPipeWaitClasses[] table spells into C++, and a copy
here would go stale in exactly the case that matters. kWaitClassCount is the terminator and
is NOT a class a row may carry - a row that named it would generate a table entry the
lookup uses to mean 'this opcode never came from the catalogue'."""
text = read(os.path.join(PIPE_DIR, "MGPipe.h"))
body = WAIT_ENUM_RE.search(text)
if not body:
sys.exit("MGPipe.h: enum MGPipeWaitClass : Uint8 is missing or has changed shape")
names = WAIT_MEMBER_RE.findall(body.group(1))
if "kWaitClassCount" not in names:
sys.exit("MGPipe.h: enum MGPipeWaitClass has no kWaitClassCount terminator")
classes = [n for n in names if n != "kWaitClassCount"]
if not classes:
sys.exit("MGPipe.h: enum MGPipeWaitClass declares no wait classes")
return classes
KNOWN_WAIT_CLASSES = tuple(parse_wait_classes())
def check_wait_classes_are_known(calls):
"""P5e (CONTRACT-P5E.md §2.2). Two gates over PipeCalls.def's fifth column, both in --check
as well, because the column is what the client's wait rule and the sink's barriered
predicate are BOTH computed from - a typo here is a wrong wait on a live wire rather than a
compile error in one consumer.
1. The token must be an MGPipeWaitClass enumerator, and never the kWaitClassCount
terminator, which the generated lookup uses for "not from this catalogue".
2. kReplySlot and kWaitReply must agree, BOTH WAYS. A reply-slot row that did not wait
would be a waiter that hangs - the client would run on and read a slot nobody had
posted into yet; a waiting row with no slot would block on an answer nobody posts.
This is the same class of defect R-13.4 found between the flag column and the payloads,
and it is stated here for the same reason: nothing else has both facts in one place."""
for call in calls:
if call.Wait not in KNOWN_WAIT_CLASSES:
sys.exit("PipeCalls.def: %s carries wait class %s, which is not an MGPipeWaitClass "
"enumerator (%s)" % (call.Name, call.Wait, ", ".join(KNOWN_WAIT_CLASSES)))
owns_slot = "kReplySlot" in call.Flags
waits_reply = call.Wait == "kWaitReply"
if owns_slot and not waits_reply:
sys.exit("PipeCalls.def: %s owns a reply slot but its wait class is %s; a "
"kReplySlot row must be kWaitReply or the client reads a slot nobody "
"posted into" % (call.Name, call.Wait))
if waits_reply and not owns_slot:
sys.exit("PipeCalls.def: %s is kWaitReply but carries no kReplySlot; it would "
"block on an answer nobody posts" % call.Name)
def parse_coverage():
text = read(os.path.join(PIPE_DIR, "Coverage.def"))
accessors = []
@@ -800,6 +858,48 @@ inline constexpr MGPipeCallClass MGPipeCallClassFor(MGPWireOp op) {
: kCallClassCount;
}
// P5e (MG_Remote/CONTRACT-P5E.md 2.2): THE WAIT CLASS OF EVERY OPCODE, indexed the same way.
// Index 0 is kWaitClassCount - never a real class - for the flags table's reason: the one
// caller that can pass an opcode this catalogue never produced is a decoder holding bytes off
// a stream, and it must reach its own Fatal{ProtocolCorruption} rather than a wait decision.
//
// This is the STATIC HALF of MGPipeBarriered(op, payload, applierState), the predicate the emit
// table and the sink compute identically. The other half is payload-derived (an open
// transform-feedback span, a draw carrying client vertex arrays) and lives in that function,
// because it depends on state a table indexed by opcode cannot see.
inline constexpr Uint8 kMGPipeWaitClasses[static_cast<SizeT>(MGPWireOp::kOpCount)] = {
/* 0 */ static_cast<Uint8>(kWaitClassCount),
""")
for call in calls:
out.append(" /* %2d %-24s*/ static_cast<Uint8>(%s)," % (call.Index, call.Name, call.Wait))
out.append("};")
out.append("static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==")
out.append(" static_cast<SizeT>(MGPWireOp::kOpCount),")
out.append(" \"the wait-class table and the opcode space disagree\");")
out.append("""
inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) {
const SizeT index = static_cast<SizeT>(op);
return index < static_cast<SizeT>(MGPWireOp::kOpCount)
? static_cast<MGPipeWaitClass>(kMGPipeWaitClasses[index])
: kWaitClassCount;
}
// THE COLUMN'S OWN INVARIANT, stated in C++ as well as in the generator: a reply-slot row
// waits for its reply and a waiting row owns a slot. gen_pipe.py refuses a catalogue where the
// two disagree, and this is the same statement where a reader of the generated table will look
// for it - a kReplySlot row that did not wait would run on and read a slot nobody had posted
// into, and a kWaitReply row with no slot would block on an answer nobody posts.
static_assert(MGPipeWaitClassFor(MGPWireOp::kInvalid) == kWaitClassCount,
"opcode 0 is not a call and has no wait class");
static_assert(MGPipeWaitClassFor(MGPWireOp::Present) == kWaitPresent,
"present is paced by the credit, not by a wait after the publish");
static_assert(MGPipeWaitClassFor(MGPWireOp::DrawVbo) == kWaitNone,
"the steady draw path is what P5e exists to stop waiting on");
static_assert(MGPipeWaitClassFor(MGPWireOp::ApplierReset) == kWaitApplied,
"a make-current is a fresh server and every verb after it must see the reset");
static_assert(MGPipeWaitClassFor(MGPWireOp::ReadPixels) == kWaitReply,
"a readback's answer is not derivable (R-5)");
// Spot checks the generator states about its own output, so that a catalogue edit that
// silently drops a flag is a build break here and not a wrong decode six packages away.
static_assert(MGPipeCallFlagsFor(MGPWireOp::kInvalid) == static_cast<Uint32>(kNone),
@@ -1515,6 +1615,23 @@ def self_test(accessors):
"combines kNone with",
lambda: check_call_flags_are_known([flag_kNone])))
# P5e's two gates over the fifth column (CONTRACT-P5E.md §2.2). The column decides whether
# a live client waits after publishing, so a typo or a disagreement with the flag column is
# a wrong wait on the wire rather than a compile error in one consumer - which is exactly
# the shape R-13.4 found between the flag column and the payloads.
wait_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitApplyed")
wait_slot_no_wait = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kReplySlot"], "kWaitNone")
wait_no_slot = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone"], "kWaitReply")
controls.append(("wait class that is not an MGPipeWaitClass enumerator",
"which is not an MGPipeWaitClass enumerator",
lambda: check_wait_classes_are_known([wait_typo])))
controls.append(("a kReplySlot row that does not wait for its reply",
"owns a reply slot but its wait class is",
lambda: check_wait_classes_are_known([wait_slot_no_wait])))
controls.append(("a kWaitReply row with no reply slot",
"but carries no kReplySlot",
lambda: check_wait_classes_are_known([wait_no_slot])))
# R-16 meta-control (codex review finding 9; verified by execution in
# p5-results/wave1-codex-verify.md §9). The exact perturbation there replaced the
# flag-typo control's callback with `sys.exit("unrelated parser failure")` and,
@@ -1540,8 +1657,9 @@ def self_test(accessors):
# The positive control: the canned struct's exact list passes, and the parser sees the
# padding member as padding and the function as not a member.
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])
# ... and the real catalogue's real flags pass the same gate.
# ... and the real catalogue's real flags and wait classes pass the same gates.
check_call_flags_are_known(calls_for_control)
check_wait_classes_are_known(calls_for_control)
if trips == 0:
sys.exit("gen_pipe: self-test: no negative control tripped - the gates are not checking anything")
if trips != len(controls):
@@ -1561,6 +1679,7 @@ def main():
calls = parse_calls()
payloads = parse_verify_payloads()
check_call_flags_are_known(calls)
check_wait_classes_are_known(calls)
check_call_payloads_have_field_lists(calls, payloads)
check_field_lists_cover_struct_members(parse_field_lists(), payloads)
accessors, deltas, sticky, emitted = parse_coverage()
+9 -3
View File
@@ -702,9 +702,15 @@ def self_test():
"the field set (%d of %d)" % (total, len(accessors)))
if len(sticky) != 7:
sys.exit("gen_pipe_field_ownership: self-test: Coverage.def no longer has seven sticky fields")
if len(refused) != 7:
# EIGHT SINCE P5e (MG_Remote/CONTRACT-P5E.md 5.6): GetBufferBindingPoint joined the seven
# when set_shader_buffers got its Coverage.def emitted row. The number is pinned rather
# than merely derived because it is the SIZE OF THE SHAPE-ONLY SET - the rows where an
# emitted call exists and still does not supply the field - and a row that quietly left
# that set would flip to RECORD_SUPPLIED and let the residual fill skip a pull nothing
# replaces. Every addition to it is a phase's deliberate act, so it is spelled here.
if len(refused) != 8:
sys.exit("gen_pipe_field_ownership: self-test: EmittedCallSuppliesTheWholeField refuses %d "
"fields, not the seven the contract's derivation is written against" % len(refused))
"fields, not the eight the contract's derivation is written against" % len(refused))
if trips == 0:
sys.exit("gen_pipe_field_ownership: self-test: no negative control tripped - the gates are "
"not checking anything")
@@ -713,7 +719,7 @@ def self_test():
% (len(controls) - trips, len(controls)))
print("gen_pipe_field_ownership: self-test: %d negative-control trip(s), each asserted against "
"its OWN message; harness control OK; positive control OK "
"(%d fields partitioned, 7 sticky forwards, 7 refusals)" % (trips, total))
"(%d fields partitioned, 7 sticky forwards, 8 refusals)" % (trips, total))
return 0