From 4fc93c1ab92aac79f4423cc165cf5b5e8d98048f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:10:10 -0400 Subject: [PATCH] [MG_Remote] (Disaggregated): P5e vi - kDrawClientArrays on the wire, and the run-ahead refusal - ReadDrawBindings answers ClientVertexArrays in the SAME single read of the bindings the rest of the draw plan is made from: an enabled attribute with no buffer behind it, which is exactly what EmitVertexBuffers publishes as Res == kMGPipeNullHandle. - PlanDrawInfo sets kDrawClientArrays for every draw shape, not only the array ones - a glDrawElements can fetch its VERTICES from client memory too - because MGPipeBarriered is computed from the record on both sides and a null Res inside the window is also what a disabled attribute publishes. - EmitDrawRecord and the reduced DrawArrays path refuse such a draw under run-ahead with Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"} on the GL thread, before the hooks (ID-82 / ruling 2). LOCKSTEP IS UNCHANGED: the client is parked in WaitForApplied for that record, so the server's upload reads memory that is not moving (ruling 4). Staging the bytes as a per-attribute {BindingIndex, MGHostSpan} tail is P8's. - The armed predicate is spelled here from the three inputs MGPipeTypes.h names for RunAheadArmed() rather than called, because ClientSession::RunAheadArmed is package ra's; it is inert until the integration commit publishes the caps bit, and ra collapses this body into that latch. --- MobileGL/MG_Remote/Client/EmitTables.cpp | 95 +++++++++++++++++++++++- MobileGL/MG_Remote/Client/EmitTables.h | 10 +++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index ac5dc9e4..1d28e23c 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -382,8 +382,39 @@ namespace MobileGL::MG_Remote::Client { nullptr, 0, nullptr, 0, nullptr); } + // P5e (vi), ID-82: declared here because the REDUCED DrawArrays path below predates the + // nineteen-slot block that defines them, and it needs the same two answers. One + // definition each, further down, beside the rest of the draw plan. + [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier); + Bool RunAheadWouldSkipTheWait(ClientSession& session); + + // Does the bound VAO fetch any ENABLED attribute out of the application's own memory. + // The test is EmitVertexBuffers' own (`attrib.Enabled && !attrib.Buffer` is exactly what + // it publishes as Res == kMGPipeNullHandle), so the flag on the wire and the record the + // server applies agree by construction. + Bool BoundVaoHasClientVertexArrays(MG_State::GLState::GLContext* ctx) { + if (ctx == nullptr) return false; + const auto& vao = ctx->GetBoundVertexArray(); + if (!vao) return false; + const auto& attributes = vao->GetAllAttributes(); + for (SizeT i = 0; i < attributes.size(); ++i) { + if (attributes[i].Enabled && !attributes[i].Buffer) return true; + } + return false; + } + void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) { ClientSession& session = RequireSession("DrawArrays"); + + // P5e (vi): the same refusal the nineteen-slot block raises in EmitDrawRecord, and + // this is the entry point the contract names by name - "DrawArrays+CLIENT_ARRAYS". + // The probe runs only on this path's own draw, before the hooks, and it is inert + // under lockstep (see RunAheadWouldSkipTheWait). + const Bool clientArrays = BoundVaoHasClientVertexArrays(MG_State::pGLContext.get()); + if (clientArrays && RunAheadWouldSkipTheWait(session)) { + RefuseDrawByName("DrawArrays", "CLIENT_ARRAYS"); + } + BeforeDrawVerb(); if (g_dropDrawEmission) { @@ -399,7 +430,10 @@ namespace MobileGL::MG_Remote::Client { MG_Pipe::MGPDrawInfo info{}; info.Mode = static_cast(mode); info.IndexSize = 0; // arrays - info.Flags = 0; // NO kDrawHasUserIndices: the reduced path draws from a VBO + // NO kDrawHasUserIndices: the reduced path draws from a VBO. kDrawClientArrays IS + // set when one is present, because the wait rule is computed from the record on + // both sides and this path publishes its own record rather than PlanDrawInfo's. + info.Flags = clientArrays ? static_cast(MG_Pipe::kDrawClientArrays) : 0; info.InstanceCount = 1; info.StartInstance = 0; info.RestartIndex = 0; @@ -446,6 +480,27 @@ namespace MobileGL::MG_Remote::Client { // indices (or past 2^32 of them): the record spells Start in // indices, and rounding would draw from the wrong element. + // P5e (vi), ID-82 / CONTRACT-P5E §5.1: "would this session publish a draw and move on". + // + // IT IS THE CONJUNCTION MGPipeTypes.h STATES FOR RunAheadArmed(), spelled here rather + // than called, and that is a SEAM, not a duplication: ClientSession::RunAheadArmed() is + // package ra's to land (it latches the same three at the first caps adoption after Start + // and may afterwards only turn OFF). Until it exists this is the same answer computed + // from the same three inputs, and it is inert for the whole phase because the third one + // - the server's kCapRunAheadApply - is not published until the integration commit + // (kMGPipeP5eRunAheadReady). WHEN ra LANDS, THIS BODY BECOMES `session.RunAheadArmed()`: + // one latch, one answer, and no way for the refusal to disagree with the wait rule about + // which arm the session is on. + Bool RunAheadWouldSkipTheWait(ClientSession& session) { +#if MOBILEGL_BUILD_DISAGGREGATED + return session.BarrierArmed() && MG_Config::Ipc.RunAhead != 0 && + session.Caps().HasCap(MG_Pipe::kCapRunAheadApply); +#else + (void)session; + return false; +#endif + } + [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) { char name[96]; std::snprintf(name, sizeof(name), "%s+%s", slot, qualifier); @@ -465,6 +520,19 @@ namespace MobileGL::MG_Remote::Client { b.ElementBufferBound = true; b.ElementBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*bound); } + // P5e (vi): the client-array probe, in the SAME single read of the bindings the + // rest of the plan is made from rather than in a second walk at the refusal - + // this runs on the GL thread once per draw and the refusal must not add its own + // frontend pass. The test is the emitter's own: EmitVertexBuffers publishes + // Res == kMGPipeNullHandle for exactly `attrib.Enabled && !attrib.Buffer`, so + // the flag and the record agree by construction instead of by inspection. + const auto& attributes = vao->GetAllAttributes(); + for (SizeT i = 0; i < attributes.size(); ++i) { + if (attributes[i].Enabled && !attributes[i].Buffer) { + b.ClientVertexArrays = true; + break; + } + } } if (const auto& di = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::DrawIndirect).GetBoundObject()) { b.DrawIndirectBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*di); @@ -486,6 +554,24 @@ namespace MobileGL::MG_Remote::Client { const void* clientIndices, Uint64 clientIndexBytes, const MG_Pipe::MGPDrawIndirect* indirect) { ClientSession& session = RequireSession(slot); + + // P5e (vi), ID-82 / ruling 2, BEFORE the pre-verb hooks and before the record: a + // draw that fetches vertices out of the application's own memory has no wire form, + // and under run-ahead the server would dereference a client pointer into memory + // this thread is already past. It is refused BY NAME on the GL thread, in the + // shape ID-57 gives every unmigrated draw shape, rather than rendered wrong (R-4). + // + // Under LOCKSTEP it is not refused and nothing here changes: the client is parked + // in WaitForApplied for exactly this record, so the server's upload reads memory + // that is not moving - which is ruling 4's "barriered records keep P5C's + // semantics", and is why this is a run-ahead gate and not a transport gate. + // Staging the bytes as a per-attribute {BindingIndex, MGHostSpan} tail, shaped like + // kDrawHasUserIndices, is P8's and retires the refusal with it. + if ((info.Flags & static_cast(MG_Pipe::kDrawClientArrays)) != 0 && + RunAheadWouldSkipTheWait(session)) { + RefuseDrawByName(slot, "CLIENT_ARRAYS"); + } + BeforeDrawVerb(); if (g_dropDrawEmission) { @@ -1853,6 +1939,13 @@ namespace MobileGL::MG_Remote::Client { // on its side). kDrawHasUserIndices / kDrawIsIndirect are added by the emission itself, // kDrawHasIndexRange by the two DrawRangeElements* callers. info.Flags = bindings.PrimitiveRestart ? static_cast(MG_Pipe::kDrawPrimitiveRestart) : 0; + // P5e (vi), CONTRACT-P5E §2.1 (ii) / §5.1. On the wire BECAUSE the server needs it: the + // barriered predicate is computed identically by both roles from the record alone, and + // "does this draw fetch from client memory" is not derivable from anything else in it - + // a null Res inside the vertex-buffer window is also what a DISABLED attribute below + // the high-water mark publishes. Set for every draw shape, not just the array ones: a + // glDrawElements can fetch its VERTICES from client memory too. + if (bindings.ClientVertexArrays) info.Flags |= static_cast(MG_Pipe::kDrawClientArrays); // A negative count has already been refused by the frontend (INVALID_VALUE); 0 crosses // as 0 and draws nothing, which is what the driver does with it. info.InstanceCount = instanceCount > 0 ? static_cast(instanceCount) : 0u; diff --git a/MobileGL/MG_Remote/Client/EmitTables.h b/MobileGL/MG_Remote/Client/EmitTables.h index f076c2aa..45707409 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.h +++ b/MobileGL/MG_Remote/Client/EmitTables.h @@ -187,6 +187,16 @@ namespace MobileGL::MG_Remote::Client { // carried verbatim (informational in P5b: the backend reads its own barrier-pulled copy). Bool PrimitiveRestart = false; Uint32 RestartIndex = 0; + // P5e (vi), ID-82 / CONTRACT-P5E §5.1: TRUE when the bound VAO has at least one ENABLED + // attribute with no buffer object behind it, i.e. an array whose vertices live in the + // application's own memory. The server has no such memory: today it dereferences + // `attrib.Offset` as a raw client pointer from the apply thread + // (Managers.cpp's SyncClientSideAttributesForDrawArrays), which is legal only while the + // client is parked behind the record. It rides in MGPDrawInfo::Flags as kDrawClientArrays + // so BOTH roles can decide from the wire - the client refuses such a draw under + // run-ahead, and MGPipeBarriered's escalation (ii) keeps it barriered if one ever + // arrives anyway. Staging the bytes is P8's. + Bool ClientVertexArrays = false; }; // 1 / 2 / 4 for the three GL index types, 0 for anything else (the frontend has already