diff --git a/MobileGL/Config.h b/MobileGL/Config.h index f21f2b87..c4bb4db6 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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). diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 2344901f..5d4d6a9d 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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 @@ -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(ipc.StrictErrors), static_cast(ipc.Audit), + ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, + static_cast(ipc.StrictErrors), static_cast(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 " diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 39fd56ef..6d170b25 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -13697,4 +13697,88 @@ namespace MobileGL::MG_Backend::DirectGLES { TwinRegistry 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& 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 4f2a2a88..4af571a4 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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& 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& SyncTextureObjectToBackend( const SharedPtr& 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& 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& 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& 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& 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; diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 8a8fb04f..97168fe4 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -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()); diff --git a/MobileGL/MG_Impl/Pipe/ImageEmit.h b/MobileGL/MG_Impl/Pipe/ImageEmit.h index 59ece2b0..9b18ef53 100644 --- a/MobileGL/MG_Impl/Pipe/ImageEmit.h +++ b/MobileGL/MG_Impl/Pipe/ImageEmit.h @@ -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(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(access)); - return 0; - } - } - Array m_entries{}; MGPShaderImages m_lastImages{}; Uint32 m_window = 0; diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index e5304d78..fe0999ef 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -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; diff --git a/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h index 99c0cc18..82659443 100644 --- a/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h +++ b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h @@ -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 diff --git a/MobileGL/MG_Impl/Pipe/Tracker.h b/MobileGL/MG_Impl/Pipe/Tracker.h index 9ec51c01..01cee868 100644 --- a/MobileGL/MG_Impl/Pipe/Tracker.h +++ b/MobileGL/MG_Impl/Pipe/Tracker.h @@ -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; } } diff --git a/MobileGL/MG_Pipe/Coverage.def b/MobileGL/MG_Pipe/Coverage.def index d67d4e53..b5d06dcc 100644 --- a/MobileGL/MG_Pipe/Coverage.def +++ b/MobileGL/MG_Pipe/Coverage.def @@ -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, 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) \ diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def index f60190cd..4c3bc75f 100644 --- a/MobileGL/MG_Pipe/FieldOwnership.def +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -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 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 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; 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; 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 */ \ diff --git a/MobileGL/MG_Pipe/MGPipe.h b/MobileGL/MG_Pipe/MGPipe.h index dc531293..e30d0a13 100644 --- a/MobileGL/MG_Pipe/MGPipe.h +++ b/MobileGL/MG_Pipe/MGPipe.h @@ -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. diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 90f89fa0..01adf005 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -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(kCapRunAheadApply) + : static_cast(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."}. + 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 diff --git a/MobileGL/MG_Pipe/MGPipeValueTypes.h b/MobileGL/MG_Pipe/MGPipeValueTypes.h index e2da5312..5749fb4d 100644 --- a/MobileGL/MG_Pipe/MGPipeValueTypes.h +++ b/MobileGL/MG_Pipe/MGPipeValueTypes.h @@ -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(MGPipeImageAccess::ReadOnly); + inline constexpr Uint8 kMGPipeImageAccessWriteOnly = static_cast(MGPipeImageAccess::WriteOnly); + inline constexpr Uint8 kMGPipeImageAccessReadWrite = static_cast(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(MGPipeImageAccess::Count); + } + + inline constexpr MGPipeImageAccess MGPipeDecodeImageAccess(Uint8 encoded) { + return static_cast(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 by design and are diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 6f023a24..52f91983 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -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(payload); + if ((draw.Flags & static_cast(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 // diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 7abb38a1..872d582d 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -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 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 BlockBindings; // dense, GL uniform-block index order + Vector SamplerUnits; // sparse, ascending Location + Vector 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 + 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 diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 4c2ec2c3..db4083cf 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -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"): diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index ebe2df50..ca5d77c3 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -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) \ diff --git a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc index 46835c98..1aac550c 100644 --- a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc +++ b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc @@ -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 diff --git a/MobileGL/MG_Pipe/generated/PipeFilled.inc b/MobileGL/MG_Pipe/generated/PipeFilled.inc index e109b551..a9e5f860 100644 --- a/MobileGL/MG_Pipe/generated/PipeFilled.inc +++ b/MobileGL/MG_Pipe/generated/PipeFilled.inc @@ -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; diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index efa9fa4e..10fabc8c 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -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. diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 0139b3a7..643b78d1 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -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); +} diff --git a/MobileGL/MG_Pipe/generated/PipeVerify.inc b/MobileGL/MG_Pipe/generated/PipeVerify.inc index dd9ce0fb..d11fbb5c 100644 --- a/MobileGL/MG_Pipe/generated/PipeVerify.inc +++ b/MobileGL/MG_Pipe/generated/PipeVerify.inc @@ -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 : std::true_type {}; template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> +struct MGPipeHasFieldVerifier : std::true_type {}; +template <> struct MGPipeHasFieldVerifier : std::true_type {}; template <> struct MGPipeHasFieldVerifier : 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; diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index 6f14039c..d1bb7246 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -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(MGPWireOp::kOpCount) /* 77 ApplierReset */ static_cast(kNone), /* 78 ObjectDeath */ static_cast(kNone), /* 79 SetContextValues */ static_cast(kNone), + /* 80 SetProgramBindings */ static_cast(kVarTail | kHostSpan), }; static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_cast(MGPWireOp::kOpCount), @@ -349,6 +351,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast(MGPWireOp::kOpCount /* 77 ApplierReset */ static_cast(kScreen), /* 78 ObjectDeath */ static_cast(kCtxObject), /* 79 SetContextValues */ static_cast(kCtxState), + /* 80 SetProgramBindings */ static_cast(kCtxState), }; static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_cast(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(MGPWireOp::kOpCount)] = { + /* 0 */ static_cast(kWaitClassCount), + + /* 1 GetCaps */ static_cast(kWaitReply), + /* 2 ResourceCreate */ static_cast(kWaitReply), + /* 3 ResourceRespecify */ static_cast(kWaitReply), + /* 4 ResourceDestroy */ static_cast(kWaitNone), + /* 5 MapPersistent */ static_cast(kWaitReply), + /* 6 UnmapPersistent */ static_cast(kWaitNone), + /* 7 FenceCreate */ static_cast(kWaitNone), + /* 8 FenceStatus */ static_cast(kWaitReply), + /* 9 FenceWait */ static_cast(kWaitReply), + /* 10 FenceDestroy */ static_cast(kWaitNone), + /* 11 QueryCreate */ static_cast(kWaitNone), + /* 12 QueryBegin */ static_cast(kWaitNone), + /* 13 QueryEnd */ static_cast(kWaitNone), + /* 14 QueryAvailable */ static_cast(kWaitReply), + /* 15 QueryResult */ static_cast(kWaitReply), + /* 16 QueryDestroy */ static_cast(kWaitNone), + /* 17 CreateRenderState */ static_cast(kWaitNone), + /* 18 BindRenderState */ static_cast(kWaitNone), + /* 19 DeleteRenderState */ static_cast(kWaitNone), + /* 20 CreateVertexElements */ static_cast(kWaitNone), + /* 21 BindVertexElements */ static_cast(kWaitNone), + /* 22 DeleteVertexElements */ static_cast(kWaitNone), + /* 23 CreateSamplerState */ static_cast(kWaitNone), + /* 24 DeleteSamplerState */ static_cast(kWaitNone), + /* 25 CreateSamplerView */ static_cast(kWaitNone), + /* 26 DeleteSamplerView */ static_cast(kWaitNone), + /* 27 CreateShaderState */ static_cast(kWaitNone), + /* 28 BindShaderState */ static_cast(kWaitNone), + /* 29 DeleteShaderState */ static_cast(kWaitNone), + /* 30 SetDynamicState */ static_cast(kWaitNone), + /* 31 SetFramebufferState */ static_cast(kWaitNone), + /* 32 SetVertexBuffers */ static_cast(kWaitNone), + /* 33 SetIndexBuffer */ static_cast(kWaitNone), + /* 34 SetIndirectBuffers */ static_cast(kWaitNone), + /* 35 SetSamplerViews */ static_cast(kWaitNone), + /* 36 BindSamplerStates */ static_cast(kWaitNone), + /* 37 SetShaderImages */ static_cast(kWaitNone), + /* 38 SetShaderBuffers */ static_cast(kWaitNone), + /* 39 SetStreamOutputTargets */ static_cast(kWaitNone), + /* 40 SetGlobalConstants */ static_cast(kWaitNone), + /* 41 SetVertexAttribDefaults */ static_cast(kWaitNone), + /* 42 SetPixelPackState */ static_cast(kWaitNone), + /* 43 SetPatchState */ static_cast(kWaitNone), + /* 44 SetDrawProgram */ static_cast(kWaitNone), + /* 45 SetDispatchProgram */ static_cast(kWaitNone), + /* 46 SetResidualValueState */ static_cast(kWaitNone), + /* 47 SetTextureParams */ static_cast(kWaitReply), + /* 48 ResourceSubData */ static_cast(kWaitReply), + /* 49 BufferSubDataResident */ static_cast(kWaitNone), + /* 50 ResourceSubDataComplete */ static_cast(kWaitNone), + /* 51 ResourceFlushRange */ static_cast(kWaitNone), + /* 52 ResourceReadback */ static_cast(kWaitReply), + /* 53 ResourceCopyRegion */ static_cast(kWaitNone), + /* 54 GenerateMipmap */ static_cast(kWaitApplied), + /* 55 GetTextureImage */ static_cast(kWaitReply), + /* 56 Blit */ static_cast(kWaitNone), + /* 57 Clear */ static_cast(kWaitNone), + /* 58 ReadPixels */ static_cast(kWaitReply), + /* 59 DrawVbo */ static_cast(kWaitNone), + /* 60 LaunchGrid */ static_cast(kWaitNone), + /* 61 MemoryBarrier */ static_cast(kWaitNone), + /* 62 BeginStreamOutput */ static_cast(kWaitApplied), + /* 63 EndStreamOutput */ static_cast(kWaitApplied), + /* 64 PauseStreamOutput */ static_cast(kWaitApplied), + /* 65 ResumeStreamOutput */ static_cast(kWaitApplied), + /* 66 Flush */ static_cast(kWaitNone), + /* 67 Present */ static_cast(kWaitPresent), + /* 68 SetSwapInterval */ static_cast(kWaitNone), + /* 69 QueryTimestamp */ static_cast(kWaitReply), + /* 70 QueryCounter */ static_cast(kWaitNone), + /* 71 FenceWaitServer */ static_cast(kWaitNone), + /* 72 BindShaderImage */ static_cast(kWaitNone), + /* 73 PatchParameter */ static_cast(kWaitNone), + /* 74 BindStreamOutput */ static_cast(kWaitApplied), + /* 75 SetStorageBlockBinding */ static_cast(kWaitApplied), + /* 76 CopyFramebufferToTexture*/ static_cast(kWaitApplied), + /* 77 ApplierReset */ static_cast(kWaitApplied), + /* 78 ObjectDeath */ static_cast(kWaitNone), + /* 79 SetContextValues */ static_cast(kWaitNone), + /* 80 SetProgramBindings */ static_cast(kWaitNone), +}; +static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == + static_cast(MGPWireOp::kOpCount), + "the wait-class table and the opcode space disagree"); + +inline constexpr MGPipeWaitClass MGPipeWaitClassFor(MGPWireOp op) { + const SizeT index = static_cast(op); + return index < static_cast(MGPWireOp::kOpCount) + ? static_cast(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(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(size), static_cast(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: diff --git a/MobileGL/MG_Remote/CONTRACT-P5E.md b/MobileGL/MG_Remote/CONTRACT-P5E.md new file mode 100644 index 00000000..a33de58e --- /dev/null +++ b/MobileGL/MG_Remote/CONTRACT-P5E.md @@ -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, "@"}` (a pulled row, §3.3), `Fatal{RoleViolation, +"MGPipeSlots"}` (an allocator probe, §4.4) or `Fatal{RoleViolation, ""}` (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."` / `"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."}` — 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 &)` 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@"}`; `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 `@`) 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". diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index fabec6c9..342b3e6e 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -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(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(size), static_cast(layout.TotalBytes), static_cast(layout.PayloadBytes), static_cast(layout.TailBytes[0]), - static_cast(layout.TailBytes[1])); + static_cast(layout.TailBytes[1]), + static_cast(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(payload); // The length cross-check is the applier's (PipeApply.cpp:2784, against the diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 5c3eb9c2..05d42005 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -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 "". 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 diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index ad45fb11..17ea339e 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -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) { diff --git a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp index e7ce2e90..c84f6f6d 100644 --- a/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp +++ b/MobileGL/MG_Test/Pipe/MagmaPipeIdentityTest.cpp @@ -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(MG_Pipe::kCapRunAheadApply), + 0u); + EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectVulkan, /*ready=*/true) & + static_cast(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(MG_Pipe::kCapRunAheadApply), + 0u); + EXPECT_EQ(MG_Pipe::MGPipeRunAheadCapBitsFor(BackendType::DirectGLES, /*ready=*/true) & + static_cast(MG_Pipe::kCapRunAheadApply), + static_cast(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(MG_Pipe::kCapRunAheadApply)); + static_assert(static_cast(MG_Pipe::kCapRunAheadApply) == (1ull << 10), + "kCapRunAheadApply moved bit; CONTRACT-P5E table 0 names bit 10"); + } #endif // MOBILEGL_PIPE_PUSH } // namespace diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 3d1e9d26..359dd707 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -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, #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 " 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[] = { + "", + MGP_CALL_LIST(MGP_CATALOGUE_NAME_ROW) + }; +#undef MGP_CATALOGUE_NAME_ROW + static_assert(sizeof(kCatalogueOpNames) / sizeof(kCatalogueOpNames[0]) == + static_cast(MGPWireOp::kOpCount), + "the diagnostic name table and the opcode space disagree"); + + const char* WireOpNameForDiag(MGPWireOp op) { + const SizeT index = static_cast(op); + return index < static_cast(MGPWireOp::kOpCount) ? kCatalogueOpNames[index] + : ""; + } } // 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(), 12u); EXPECT_EQ(ClassCount(), 8u); EXPECT_EQ(ClassCount(), 13u); - EXPECT_EQ(ClassCount(), 18u); + EXPECT_EQ(ClassCount(), 19u); EXPECT_EQ(ClassCount(), 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(&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(MGPWireOp::SetContextValues), 79); - EXPECT_EQ(static_cast(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(MGPWireOp::SetProgramBindings), 80); + EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 81); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetProgramBindings), + static_cast(kVarTail | kHostSpan)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetContextValues), static_cast(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(kDrawIsIndirect), 1u << 5); EXPECT_EQ(static_cast(kDrawHasUserIndices) & static_cast(kDrawIsIndirect), 0u); + // P5e's draw flag: the next free bit, and disjoint from all five before it. + EXPECT_EQ(static_cast(kDrawClientArrays), 1u << 6); + EXPECT_EQ(static_cast(kDrawClientArrays) & + static_cast(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(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(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(MGPWireOp::kOpCount); ++i) { + switch (MGPipeWaitClassFor(static_cast(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(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(MGPWireOp::kOpCount); ++i) { + const auto op = static_cast(i); + const Bool ownsSlot = (MGPipeCallFlagsFor(op) & static_cast(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::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::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(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(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(Flags) & static_cast(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(Flags) & static_cast(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(Flags) & static_cast(kReplySlot)) != 0) { \ diff --git a/MobileGL/MG_Test/Pipe/TrackerTest.cpp b/MobileGL/MG_Test/Pipe/TrackerTest.cpp index e248da03..9711e0cb 100644 --- a/MobileGL/MG_Test/Pipe/TrackerTest.cpp +++ b/MobileGL/MG_Test/Pipe/TrackerTest.cpp @@ -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(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((Uint64{1} << kMGPipeDirtyCount) - 1u)); EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewRenderState), kMGPipeSubsystemRenderState); EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPixelPack), kMGPipeSubsystemPixelPack); EXPECT_EQ(MGPipeSubsystemForDirty(MGPipeDirty::NewPatchState), kMGPipeSubsystemPatchState); diff --git a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md index db020965..82d8420f 100644 --- a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md +++ b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md @@ -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. 记录位置 diff --git a/docs/Disaggregated/ROADMAP.md b/docs/Disaggregated/ROADMAP.md index 2c5bda74..29ef0b81 100644 --- a/docs/Disaggregated/ROADMAP.md +++ b/docs/Disaggregated/ROADMAP.md @@ -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` 逐用例发布 | diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index af7dca4a..4db7c34e 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -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(MGPWireOp::kOpCount)] = { + /* 0 */ static_cast(kWaitClassCount), +""") + for call in calls: + out.append(" /* %2d %-24s*/ static_cast(%s)," % (call.Index, call.Name, call.Wait)) + out.append("};") + out.append("static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) ==") + out.append(" static_cast(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(op); + return index < static_cast(MGPWireOp::kOpCount) + ? static_cast(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(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() diff --git a/scripts/gen_pipe_field_ownership.py b/scripts/gen_pipe_field_ownership.py index ab887570..71d06ab3 100644 --- a/scripts/gen_pipe_field_ownership.py +++ b/scripts/gen_pipe_field_ownership.py @@ -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