// MobileGL - MobileGL/MG_Remote/Client/EmitTables.cpp // Copyright (c) 2025-2026 MobileGL-Dev // Licensed under the GNU Lesser General Public License v3.0: // https://www.gnu.org/licenses/gpl-3.0.txt // https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only // End of Source File Header // P5 package c1: the 71-slot emit table. // // THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12): // class A 2 slots answered locally from the caps mirror, never emitted, never Fatal // class B 54 slots emitted; class C 15 slots name their unmigrated verb. // The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that // changes class without changing the arithmetic is a build break rather than a behaviour // change nobody reviewed. // // P5b MOVES SLOTS FROM C TO B, ONE PACKAGE AT A TIME (MG_Remote/CONTRACT-P5B.md §7). The three // numbers above are the partition AT THE P5b CONTRACT COMMIT and they are the ones the contract // states; the arithmetic below is what the tree currently has, and the per-package ownership // assertions say which package moved which slot. On this head t2 has landed: class B is 5 + 6 // and class C is 58. // // THE PRE-VERB HOOKS RUN BEFORE THE RECORD, NEVER AFTER (b1, ID-18). PushPersistentMapsBeforeVerb // publishes the bytes an application wrote through a coherent map with no API call at all, and // MarkGpuWritesForDraw builds the conservative GPU-write set the client now owns. Both describe // the work the record is ABOUT TO START, so a hook deferred past its own record is the C-1 // regression re-committed at the transport layer. #include "EmitTables.h" #include "ClientSession.h" #include "GpuWritePending.h" #include "PersistentMapTracker.h" #include #include #include #include #include #include #include // P5b i1: the emitters below name a texture's, a buffer's and a program's HANDLE beside the GL // arguments (rule D). The handle comes from the client's own slot allocator, which is // MG_Impl/Pipe's - the same table MG_Impl/Pipe/ImageEmit.h's set_shader_images reads, so the // two records name one identity rather than two. #include #include #include // P5b d1: the handle a bound buffer already has (never minted here - the validate-time // set_index_buffer / the buffer's own constructor did that), for MGPDrawInfo::IndexResource and // the two indirect-buffer handles. #include #include #include #include #include "WireTables.h" #include #include #include namespace MobileGL::MG_Remote::Client { // The slot arithmetic, asserted rather than commented. GlobalBackendFunctionsTable is // GLFunctionsTable plus Present plus SetSwapInterval; GLFunctionsTable is 69 function // pointers plus one Bool (PrefersCpuXfbPrimitiveAccounting, BackendObject.h:274). A slot // added to either without a decision here is a build break, which is the point: R-4 forbids // a null slot, so a new slot needs an owner on the day it appears. static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) == sizeof(MG_Backend::GLFunctionsTable) + 2 * sizeof(void (*)()), "GlobalBackendFunctionsTable is no longer GLFunctionsTable + Present + SetSwapInterval"); static_assert(sizeof(MG_Backend::GlobalBackendFunctionsTable) == kRemoteEmitSlotCount * sizeof(void (*)()) + sizeof(void (*)()), "the emit table's 71 slots plus the packed Bool no longer describe the table"); [[noreturn]] void UnmigratedVerbFatal(const char* slot) { // The same shape as MGPipeInputPoisonFatal (generated/PipeFilled.inc:407-413): names the // slot, live at every log level, aborts. Deliberately NOT MOBILEGL_ASSERT, which is // inert in an INFO build - and INFO is what every device lane runs. MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); std::abort(); } namespace { Bool g_dropClearEmission = false; Uint64 g_droppedClearEmissions = 0; Bool g_dropDrawEmission = false; Uint64 g_droppedDrawEmissions = 0; Uint64 g_presentOrdinal = 0; Uint64 g_publishedMaxRecordBytes = 0; // E2's control has to be armable from OUTSIDE the process that runs the replay, because // the statement it makes is about a trace lane and not about a unit case: "drop an // emission and OpenRA's SSIM falls below 0.99". A recompile would make the control // arm against source text, which is ID-22(a)'s defect. // // READ WITH getenv RATHER THAN THROUGH MG_Config, DELIBERATELY AND TEMPORARILY. Config.h // is c0's and a new MOBILEGL_IPC_* knob goes through the integrator; these are // NEGATIVE-CONTROL switches no operator may ever set, and they announce themselves at // warning level every time they arm so they cannot be on by accident. Flagged for // adoption into IpcTable if the integrator wants them there. Bool ReadControlKnob(const char* name) { const char* value = std::getenv(name); return value != nullptr && value[0] == '1' && value[1] == '\0'; } // ---- WHY THERE ARE TWO OF THESE KNOBS, measured rather than assumed ----------------- // // MOBILEGL_IPC_E2_DROP_CLEAR came first and it does exactly what it says: every glClear // stops at the client and no Clear record reaches the ring. It STILL COULD NOT TURN THE // E2 RETRACE RED (joint-v1.md 3: SSIM 1.000000, mismatchPixels=0 with the knob armed and // the arming WARN in the library's own log). That is not a broken knob, it is OpenRA: // `apitrace dump` of openra.trace over the 31249 replayed calls counts 30 glClear, 30 // glXSwapBuffers and 788 glDrawArrays, and the final frame issues its clear at call // 30197 and then covers the surface four times over with a terrain layer // (`glDrawArrays(GL_TRIANGLES, first=56064, count=16128)` x4, under a scissor of // -24,-24,688x528 over a 640x480 surface) before the snapshot at 31249. A frame that // overdraws every pixel it clears has a picture that does not depend on the clear, so // "drop the clear" is a control whose observable is invisible to THIS trace - R-16's // exact defect, a gate that cannot go red for its own reason. // // MOBILEGL_IPC_E2_DROP_DRAW is the honest form of the same statement for a trace lane: // drop every DrawVbo record and the picture can only be the clear colour. It is the // control that makes "the wire carried the frame" falsifiable, because the thing it // removes is the thing the golden is made of. // // DROP_CLEAR IS KEPT rather than retired: it drops a real record, it now publishes the // count it dropped (below), and a scenario whose picture DOES depend on its clear - // ClearThenReadPixelsScenario is the reduced path's target A - is where it is // observable. What it is no longer allowed to be is E2's retrace control. void ArmControlKnobs() { g_dropClearEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_CLEAR"); g_dropDrawEmission = ReadControlKnob("MOBILEGL_IPC_E2_DROP_DRAW"); if (g_dropClearEmission) { MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_CLEAR=1 - a NEGATIVE CONTROL is " "armed and every glClear will be DROPPED on the wire. It is observable " "only where the picture depends on the clear: OpenRA overdraws its whole " "surface every frame, so this knob does NOT redden the E2 retrace " "(measured, joint-v1.md 3) - MOBILEGL_IPC_E2_DROP_DRAW is the one that " "does. The dropped count is published on the 'E2 control armed' line"); } if (g_dropDrawEmission) { MGLOG_W("MG_Remote client: MOBILEGL_IPC_E2_DROP_DRAW=1 - E2's NEGATIVE CONTROL is " "armed and every DrawVbo record will be DROPPED on the wire. The surface " "can then only carry the clear colour, so this arm is expected to fail its " "SSIM threshold; a lane that stays green with it set is not going through " "the wire at all"); } } // THE CONTROL'S OWN EVIDENCE LINE, and it is emitted per frame rather than at teardown // on purpose: a retrace that is killed by its own timeout, or whose library never runs // MobileGL::Destroy, would leave a teardown-only line absent and the control would then // have to accept a bare threshold failure - which is the thing R-16 forbids. One line // per Present, only while a knob is armed, is bounded by the frame count and present // whatever happens afterwards. void LogE2ControlLine(Uint64 frameOrdinal) { if (!g_dropClearEmission && !g_dropDrawEmission) return; MGLOG_W("MGPipe: E2 control armed - drop-draw=%d drop-clear=%d, %llu records dropped " "on the wire (draw=%llu clear=%llu), frame %llu", g_dropDrawEmission ? 1 : 0, g_dropClearEmission ? 1 : 0, static_cast(g_droppedDrawEmissions + g_droppedClearEmissions), static_cast(g_droppedDrawEmissions), static_cast(g_droppedClearEmissions), static_cast(frameOrdinal)); } // ID-49's two halves, in one place so the emitter and its control read the same // arithmetic. // // GL 4.6 8.4.4, pack side: the destination row stride is ROW_LENGTH (or the width) // pixels rounded UP to PACK_ALIGNMENT, the first written byte is offset by SKIP_ROWS // whole strides plus SKIP_PIXELS pixels, and only `width * bytesPerPixel` bytes of each // stride are written - the gaps belong to the application and are never touched. That // last clause is what the control checks with a sentinel. Bool ReadbackPackStateIsTight(GLsizei width, Uint64 bytesPerPixel, const PixelStoreParameters& pack) { // SKIP_IMAGES (and IMAGE_HEIGHT) are NOT consulted: glReadPixels is a 2-D read and GL // ignores the image-level pack parameters for it, exactly as the monolith conversion // path does at DirectGLES.cpp:10905 (honorPackImageParams=false). A non-zero // SkipImages therefore does not make the layout non-tight (codex 6). if (pack.SkipRows != 0 || pack.SkipPixels != 0) return false; if (pack.RowLength != 0 && pack.RowLength != width) return false; const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; const Uint64 rowBytes = static_cast(width) * bytesPerPixel; return (rowBytes % alignment) == 0; } // ---- the session, demanded rather than assumed -------------------------------- // // Every class-B slot needs one. A null session here is NOT the monolith answer - the // monolith answer is that this table was never installed at all, because // MG_Backend::Init() only reaches BackendObject_Remote when the transport resolved. So // a null one is a Fatal by name and not a fall-through to the driver: a pass-through // slot is the "split lane ran monolith and went green" shape that every gate in this // phase exists to prevent (R-4). ClientSession& RequireSession(const char* slot) { RequireClientTablesInstalled(slot); ClientSession* session = ClientSession::Active(); if (session == nullptr) { MGLOG_F("MGPipe: Fatal{NoClientSession, \"%s\"} - the remote emit table is " "installed but no ClientSession is active. A slot may not fall through " "to a driver this role does not have", slot); std::abort(); } return *session; } // The two hooks b1 wrote and deliberately left with no caller, because the call site is // this file's. ORDER: the push first (it produces resource_subdata records that must // precede the verb on SEG_CMD), then the mark walk, then the verb record. void BeforeDrawVerb() { PushPersistentMapsBeforeVerb(); MarkGpuWritesForDraw(); } // A verb that reads buffers but starts no shader: clear, blit, readback, present. The // push still has to run - a coherent map is read by the GPU on any of them - but there // is no shader that could write one, so no mark walk. void BeforeReadOnlyVerb() { PushPersistentMapsBeforeVerb(); } // ============================================================================= // CLASS B - the five slots the verb census measured (CONTRACT-P5.md §7) // ============================================================================= void EmitClear(GLbitfield mask) { ClientSession& session = RequireSession("Clear"); BeforeReadOnlyVerb(); if (g_dropClearEmission) { // A negative control. Everything above still ran, so the only difference // between this arm and the live one is the record - which is exactly the // statement "the picture comes from the wire" that E2 exists to prove. Its // OBSERVABILITY is a property of the workload, not of this branch: see // ArmControlKnobs for the measurement that took E2's retrace off this knob. ++g_droppedClearEmissions; return; } MG_Pipe::MGPClear record{}; // The DRAW framebuffer is whatever the server's own SyncRenderState resolves from // gPipeInputs, which the client's MGP_FILL(Clear) at GL_Drawing.cpp:534 has just // written and the verb barrier keeps still (R-1). Naming a handle here would be a // SECOND statement of the binding, and the second one is the one that goes stale. record.Fbo = MG_Pipe::kMGPipeNullHandle; record.Kind = kRemoteClearWhole; record.DrawBufferIndex = -1; record.BufferMask = static_cast(mask); record.ValueClass = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // ---- f1: verbatim clear/copy/mipmap records (CONTRACT-P5B §2) ---- void EmitF1Clear(const char* slot, MG_Pipe::MGPipeHandle fbo, GLenum buffer, GLint drawbuffer, Uint8 valueClass, const void* value, GLfloat depth = 0, GLint stencil = 0) { auto& session = RequireSession(slot); BeforeReadOnlyVerb(); MG_Pipe::MGPClear record{}; record.Fbo = fbo; record.DrawBufferIndex = drawbuffer; record.ValueClass = valueClass; switch (buffer) { case GL_COLOR: record.Kind = MG_Pipe::kMGPipeClearKindColor; std::memcpy(record.ColorValue, value, sizeof(record.ColorValue)); break; case GL_DEPTH: record.Kind = MG_Pipe::kMGPipeClearKindDepth; std::memcpy(&record.DepthValue, value, sizeof(record.DepthValue)); break; case GL_STENCIL: record.Kind = MG_Pipe::kMGPipeClearKindStencil; std::memcpy(&record.StencilValue, value, sizeof(record.StencilValue)); break; case GL_DEPTH_STENCIL: record.Kind = MG_Pipe::kMGPipeClearKindDepthStencil; record.DepthValue = depth; record.StencilValue = stencil; break; default: UnmigratedVerbFatal(slot); } session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { EmitF1Clear("ClearBufferfv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value); } void EmitClearNamedFramebufferfv(const SharedPtr& fbo, GLenum buffer, GLint drawbuffer, const GLfloat* value) { EmitF1Clear("ClearNamedFramebufferfv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value); } void EmitClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { EmitF1Clear("ClearBufferiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value); } void EmitClearNamedFramebufferiv(const SharedPtr& fbo, GLenum buffer, GLint drawbuffer, const GLint* value) { EmitF1Clear("ClearNamedFramebufferiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value); } void EmitClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { EmitF1Clear("ClearBufferuiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value); } void EmitClearNamedFramebufferuiv(const SharedPtr& fbo, GLenum buffer, GLint drawbuffer, const GLuint* value) { EmitF1Clear("ClearNamedFramebufferuiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value); } void EmitClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { EmitF1Clear("ClearBufferfi", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); } void EmitClearNamedFramebufferfi(const SharedPtr& fbo, GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { EmitF1Clear("ClearNamedFramebufferfi", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); } const SharedPtr& F1BoundTexture(GLenum target) { auto& ctx = *MG_State::pGLContext; return ctx.GetTextureUnitObject(ctx.GetActiveTextureUnit()) .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject(); } void EmitF1Copy(GLenum target, GLint level, GLenum format, GLint x, GLint y, GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, Bool subImage) { auto& session = RequireSession(subImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); BeforeReadOnlyVerb(); MG_Pipe::MGPCopyFromFramebuffer record{}; record.Dst = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*F1BoundTexture(target)); record.Target = static_cast(target); record.Level = level; record.InternalFormat = format; record.X = x; record.Y = y; record.Width = width; record.Height = height; record.XOffset = xoffset; record.YOffset = yoffset; record.SubImage = subImage; session.EmitAndWait(MG_Pipe::MGPWireOp::CopyFramebufferToTexture, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitCopyTexImage2D(GLenum target, GLint level, GLenum format, GLint x, GLint y, GLsizei width, GLsizei height, GLint) { EmitF1Copy(target, level, format, x, y, width, height, 0, 0, false); } void EmitCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { EmitF1Copy(target, level, 0, x, y, width, height, xoffset, yoffset, true); } void EmitGenerateMipmap(GLenum target) { auto& session = RequireSession("GenerateMipmap"); BeforeReadOnlyVerb(); const auto& texture = F1BoundTexture(target); MG_Pipe::MGPMipPlan record{}; record.Res = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*texture); record.Target = static_cast(target); record.BaseLevel = texture->GetLevelRange().x(); const auto* mipmap = dynamic_cast(texture.get()); record.LevelCount = mipmap ? mipmap->GetMipmapLevelCount() : 0; session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) { ClientSession& session = RequireSession("DrawArrays"); BeforeDrawVerb(); if (g_dropDrawEmission) { // E2's LOAD-BEARING negative control. Same shape as the clear drop and the same // rule: everything above still ran - the persistent-map push and the GPU-write // mark walk both happened - so the ONLY difference from the live arm is that // this frame's geometry never crossed the ring. A lane that still matches its // golden with this armed did not get its picture from the wire. ++g_droppedDrawEmissions; return; } 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 info.InstanceCount = 1; info.StartInstance = 0; info.RestartIndex = 0; info.DrawIdOffset = 0; info.IndexResource = MG_Pipe::kMGPipeNullHandle; info.MinIndex = ~0u; // "unknown", MGPipeTypes.h:1330 info.MaxIndex = ~0u; info.XfbCpuCapturedVertices = 0; info.NumDraws = 1; const MG_Pipe::MGPDrawRange range{static_cast(first), static_cast(count), 0}; // One tail of exactly NumDraws entries. w1's encoder recomputes that from the // payload and Fatals on a disagreement, on THIS side - so a NumDraws that drifted // from the tail is a producer-side abort rather than a corrupt stream a peer has to // diagnose. session.EmitAndWait(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), &range, sizeof(range), nullptr, 0, nullptr); } // ============================================================================= // P5b d1 - the nineteen indexed / instanced / multi-draw / indirect draw slots // (MG_Remote/CONTRACT-P5B.md §2 d1). ONE ROW, draw_vbo (59): the record carries the GL // call verbatim (rule D) beside the handle the P8 form will dispatch on, and the sink // reproduces the backend call the monolith makes. // ============================================================================= // // Every entry point below is: read the bindings, plan the head and the ranges (the pure // functions the unit cases drive), then EmitDrawRecord - which runs the SAME pre-verb // hooks in the SAME order as EmitDrawArrays (push, then mark walk, then the record; // ID-18), honours the E2 draw-drop control for every draw record and not only the P5 // one, stages a client index array into SEG_STAGE when there is one, and emits. // // WHAT IS REFUSED BY NAME, ID-57's shape (Fatal{UnmigratedVerb, "+"}), // never rendered wrong and never fallen through to the driver (R-4): // +CLIENT_INDICES a glMultiDrawElements* with no element buffer bound: `indices[i]` // are drawcount separate client pointers and one span names one run; // P8's HostResolve.cpp flattens it. No measured workload has one. // +CLIENT_COMMANDS an indirect draw with no GL_DRAW_INDIRECT_BUFFER bound: `indirect` // would be a host pointer, which rule B forbids on the wire. // +UNBOUND_PARAMETER an *IndirectCount with no GL_PARAMETER_BUFFER (the frontend has // already raised INVALID_OPERATION for it; stated so the emitter // cannot send a null handle where the sink dereferences one). // +INDEX_OFFSET an element-buffer byte offset that is not a whole number of // indices (or past 2^32 of them): the record spells Start in // indices, and rounding would draw from the wrong element. [[noreturn]] void RefuseDrawByName(const char* slot, const char* qualifier) { char name[96]; std::snprintf(name, sizeof(name), "%s+%s", slot, qualifier); UnmigratedVerbFatal(name); } // The bindings the plan is made from, read ONCE per draw from the frontend context on // the GL thread. The element buffer is the VAO's (the same slot EmitIndexBuffer read at // validate), the two indirect buffers are the context's, and the handles are LOOKED UP, // never minted: a push build mints in the BufferObject constructor. RemoteDrawBindings ReadDrawBindings() { RemoteDrawBindings b{}; MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get(); if (ctx == nullptr) return b; if (const auto& vao = ctx->GetBoundVertexArray()) { if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) { b.ElementBufferBound = true; b.ElementBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*bound); } } if (const auto& di = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::DrawIndirect).GetBoundObject()) { b.DrawIndirectBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*di); } if (const auto& pb = ctx->GetBufferBindingSlot(::MobileGL::BufferTarget::Parameter).GetBoundObject()) { b.ParameterBuffer = MG_Pipe::MGPipeResourceTrackerInstance().Find(*pb); } b.PrimitiveRestart = ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || ctx->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); b.RestartIndex = ctx->GetPrimitiveRestartIndex(); return b; } // The one emission for all nineteen. `clientIndices`/`clientIndexBytes` name a client // index array to stage (no element buffer bound); `indirect` is the kDrawIsIndirect // block. The two are exclusive by construction here and by the layout on both sides. void EmitDrawRecord(const char* slot, MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, Uint32 numDraws, const void* clientIndices, Uint64 clientIndexBytes, const MG_Pipe::MGPDrawIndirect* indirect) { ClientSession& session = RequireSession(slot); BeforeDrawVerb(); if (g_dropDrawEmission) { // E2's negative control covers EVERY draw record, not only DrawArrays: the // Minecraft traces are DrawElements frames, and a control that dropped only the // one P5 entry point would leave those lanes green with the wire disarmed. ++g_droppedDrawEmissions; return; } info.NumDraws = numDraws; Wire::WireTail tails[2] = {{ranges, static_cast(numDraws) * sizeof(MG_Pipe::MGPDrawRange)}, {nullptr, 0}}; Uint32 tailCount = 1; MG_Pipe::MGHostSpan span{}; if (clientIndices != nullptr && clientIndexBytes != 0) { // The P8 resolve-on-client rule, applied: the bytes exist on the client only, // so the client stages them whole and the record names the run - Ptr = nullptr, // Seg = SEG_STAGE (rule B). The encoder runs all four honesty arms on the span // before it is published, and the sink resolves it through MGPipeHostBytes. const MG_Pipe::MGPBlobRef staged = session.Encoder().StageBytes(clientIndices, clientIndexBytes); span.Ptr = nullptr; span.Seg = staged.Seg; span.Offset = staged.Offset; span.Size = staged.Size; info.Flags |= MG_Pipe::kDrawHasUserIndices; tails[1] = {&span, sizeof(span)}; tailCount = 2; } else if (indirect != nullptr) { info.Flags |= MG_Pipe::kDrawIsIndirect; tails[1] = {indirect, sizeof(*indirect)}; tailCount = 2; } session.EmitAndWaitTails(MG_Pipe::MGPWireOp::DrawVbo, &info, sizeof(info), tails, tailCount, nullptr, 0, nullptr); } // ---- the single-draw indexed family: DrawElements, DrawElementsBaseVertex, the two // DrawRangeElements*, the four DrawElementsInstanced* ------------------------------- void EmitIndexedDraw(const char* slot, GLenum mode, GLsizei count, GLenum type, const void* indices, GLint baseVertex, GLsizei instanceCount, GLuint baseInstance, Bool hasRange, GLuint start, GLuint end) { const RemoteDrawBindings bindings = ReadDrawBindings(); const Uint8 indexSize = RemoteIndexSizeFor(type); if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, instanceCount, baseInstance, 1, bindings); if (hasRange) { info.Flags |= MG_Pipe::kDrawHasIndexRange; info.MinIndex = start; info.MaxIndex = end; } MG_Pipe::MGPDrawRange range{}; if (!PlanDrawRange(bindings, indexSize, indices, count, baseVertex, range)) { RefuseDrawByName(slot, "INDEX_OFFSET"); } // No element buffer: `indices` is the application's array and the draw's own // range is exactly what the driver would read. A null pointer or an empty draw // stages nothing and crosses as a zero-length range, which is what the monolith // hands the driver too. const Bool clientArray = !bindings.ElementBufferBound && indices != nullptr && count > 0; EmitDrawRecord(slot, info, &range, 1, clientArray ? indices : nullptr, clientArray ? static_cast(count) * indexSize : 0, nullptr); } void EmitDrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { EmitIndexedDraw("DrawElements", mode, count, type, indices, 0, 1, 0, false, 0, 0); } void EmitDrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLint basevertex) { EmitIndexedDraw("DrawElementsBaseVertex", mode, count, type, indices, basevertex, 1, 0, false, 0, 0); } void EmitDrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { EmitIndexedDraw("DrawRangeElements", mode, count, type, indices, 0, 1, 0, true, start, end); } void EmitDrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { EmitIndexedDraw("DrawRangeElementsBaseVertex", mode, count, type, indices, basevertex, 1, 0, true, start, end); } void EmitDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { EmitIndexedDraw("DrawElementsInstanced", mode, count, type, indices, 0, instancecount, 0, false, 0, 0); } void EmitDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) { EmitIndexedDraw("DrawElementsInstancedBaseVertex", mode, count, type, indices, basevertex, instancecount, 0, false, 0, 0); } void EmitDrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { EmitIndexedDraw("DrawElementsInstancedBaseInstance", mode, count, type, indices, 0, instancecount, baseinstance, false, 0, 0); } void EmitDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { EmitIndexedDraw("DrawElementsInstancedBaseVertexBaseInstance", mode, count, type, indices, basevertex, instancecount, baseinstance, false, 0, 0); } // ---- the instanced array draws ------------------------------------------------ void EmitArraysDraw(const char* slot, GLenum mode, GLint first, GLsizei count, GLsizei instanceCount, GLuint baseInstance) { const RemoteDrawBindings bindings = ReadDrawBindings(); MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, instanceCount, baseInstance, 1, bindings); MG_Pipe::MGPDrawRange range{}; PlanDrawRange(bindings, 0, reinterpret_cast(static_cast(first)), count, 0, range); EmitDrawRecord(slot, info, &range, 1, nullptr, 0, nullptr); } void EmitDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) { EmitArraysDraw("DrawArraysInstanced", mode, first, count, instancecount, 0); } void EmitDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance) { // The vertex-FETCH base instance already crossed in set_vertex_buffers::BaseInstance // at validate (D-H1: MGP_SET_BASE_INSTANCE runs before MGP_FILL); StartInstance is // gl_BaseInstance's value and feeds the GL call. EmitArraysDraw("DrawArraysInstancedBaseInstance", mode, first, count, instancecount, baseinstance); } // ---- the multi-draws: MGPDrawRange[drawcount] is exactly their shape ------------- // // The range array is built in a scratch vector owned by the GL thread (the emitter is // single-threaded by the ring's own SPSC contract), sized by drawcount, never held past // the emission. A drawcount of 0 is a call that draws nothing and emits nothing: the // frontend has already refused a negative one. Vector& MultiDrawScratch(Uint32 count) { static Vector& scratch = *new Vector(); scratch.resize(count); return scratch; } void EmitMultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) { if (drawcount <= 0 || first == nullptr || count == nullptr) return; const RemoteDrawBindings bindings = ReadDrawBindings(); const auto n = static_cast(drawcount); MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, 0, 1, 0, n, bindings); Vector& ranges = MultiDrawScratch(n); for (Uint32 i = 0; i < n; ++i) { PlanDrawRange(bindings, 0, reinterpret_cast(static_cast(first[i])), count[i], 0, ranges[i]); } EmitDrawRecord("MultiDrawArrays", info, ranges.data(), n, nullptr, 0, nullptr); } void EmitMultiIndexedDraw(const char* slot, GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount, const GLint* basevertex) { if (drawcount <= 0 || count == nullptr || indices == nullptr) return; const RemoteDrawBindings bindings = ReadDrawBindings(); const Uint8 indexSize = RemoteIndexSizeFor(type); if (indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); if (!bindings.ElementBufferBound) RefuseDrawByName(slot, "CLIENT_INDICES"); const auto n = static_cast(drawcount); MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, n, bindings); Vector& ranges = MultiDrawScratch(n); for (Uint32 i = 0; i < n; ++i) { if (!PlanDrawRange(bindings, indexSize, indices[i], count[i], basevertex != nullptr ? basevertex[i] : 0, ranges[i])) { RefuseDrawByName(slot, "INDEX_OFFSET"); } } EmitDrawRecord(slot, info, ranges.data(), n, nullptr, 0, nullptr); } void EmitMultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount) { EmitMultiIndexedDraw("MultiDrawElements", mode, count, type, indices, drawcount, nullptr); } void EmitMultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices, GLsizei drawcount, const GLint* basevertex) { EmitMultiIndexedDraw("MultiDrawElementsBaseVertex", mode, count, type, indices, drawcount, basevertex); } // ---- the indirect family: the block is the second tail, NumDraws is 0 ------------- void EmitIndirectDraw(const char* slot, GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride, GLintptr parameterOffset, Bool hasParameterBuffer) { const RemoteDrawBindings bindings = ReadDrawBindings(); const Uint8 indexSize = type != 0 ? RemoteIndexSizeFor(type) : 0; if (type != 0 && indexSize == 0) RefuseDrawByName(slot, "INDEX_TYPE"); if (MG_Pipe::MGPipeHandleIsNull(bindings.DrawIndirectBuffer)) { RefuseDrawByName(slot, "CLIENT_COMMANDS"); } if (hasParameterBuffer && MG_Pipe::MGPipeHandleIsNull(bindings.ParameterBuffer)) { RefuseDrawByName(slot, "UNBOUND_PARAMETER"); } MG_Pipe::MGPDrawInfo info = PlanDrawInfo(mode, indexSize, 1, 0, 0, bindings); const MG_Pipe::MGPDrawIndirect block = PlanDrawIndirect(bindings, indirect, drawcount, stride, parameterOffset, hasParameterBuffer); EmitDrawRecord(slot, info, nullptr, 0, nullptr, 0, &block); } void EmitDrawArraysIndirect(GLenum mode, const void* indirect) { EmitIndirectDraw("DrawArraysIndirect", mode, 0, indirect, 1, 0, 0, false); } void EmitDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) { EmitIndirectDraw("DrawElementsIndirect", mode, type, indirect, 1, 0, 0, false); } void EmitMultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) { EmitIndirectDraw("MultiDrawArraysIndirect", mode, 0, indirect, drawcount, stride, 0, false); } void EmitMultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount, GLsizei stride) { EmitIndirectDraw("MultiDrawElementsIndirect", mode, type, indirect, drawcount, stride, 0, false); } void EmitMultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { EmitIndirectDraw("MultiDrawArraysIndirectCount", mode, 0, indirect, maxdrawcount, stride, drawcount, true); } void EmitMultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) { EmitIndirectDraw("MultiDrawElementsIndirectCount", mode, type, indirect, maxdrawcount, stride, drawcount, true); } void EmitBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { ClientSession& session = RequireSession("BlitFramebuffer"); BeforeReadOnlyVerb(); MG_Pipe::MGPBlit record{}; // Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set // by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the // barrier. The named form below carries both handles after a scoped binding override. record.ReadFbo = MG_Pipe::kMGPipeNullHandle; record.DrawFbo = MG_Pipe::kMGPipeNullHandle; record.SrcX0 = srcX0; record.SrcY0 = srcY0; record.SrcX1 = srcX1; record.SrcY1 = srcY1; record.DstX0 = dstX0; record.DstY0 = dstY0; record.DstX1 = dstX1; record.DstY1 = dstY1; record.Mask = static_cast(mask); record.Filter = static_cast(filter); session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // DSA blits use the existing bound-form backend while the verb barrier holds. // Only client shadow bindings change here; no driver call or frontend pointer crosses // the wire. Bind() bumps their versions on entry and restore, so the next ordinary // verb republishes the application's original bindings even if no GL bind intervenes. class ScopedBlitBindings { public: using Fbo = MG_State::GLState::FramebufferObject; ScopedBlitBindings(const SharedPtr& read, const SharedPtr& draw) : m_read(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read)), m_draw(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw)), m_savedRead(m_read.GetBoundObject()), m_savedDraw(m_draw.GetBoundObject()) { m_read.Bind(read); m_draw.Bind(draw); } ~ScopedBlitBindings() { m_read.Bind(m_savedRead); m_draw.Bind(m_savedDraw); } ScopedBlitBindings(const ScopedBlitBindings&) = delete; ScopedBlitBindings& operator=(const ScopedBlitBindings&) = delete; private: BindingSlot& m_read; BindingSlot& m_draw; SharedPtr m_savedRead; SharedPtr m_savedDraw; }; void EmitBlitNamedFramebuffer( const SharedPtr& read, const SharedPtr& draw, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { ClientSession& session = RequireSession("BlitNamedFramebuffer"); const ScopedBlitBindings bindings(read, draw); // The frontend's first validate preceded this temporary lowering. Refresh both // emitted framebuffer state and the existing BARRIER-PULLED binding fields now. MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer); BeforeReadOnlyVerb(); MG_Pipe::MGPBlit record{}; record.ReadFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*read); record.DrawFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*draw); record.SrcX0 = srcX0; record.SrcY0 = srcY0; record.SrcX1 = srcX1; record.SrcY1 = srcY1; record.DstX0 = dstX0; record.DstY0 = dstY0; record.DstX1 = dstX1; record.DstY1 = dstY1; record.Mask = static_cast(mask); record.Filter = static_cast(filter); session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // How many bytes glReadPixels will pack for this rectangle, from the PACK half of the // pixel-store state. // // ID-49: THE PACK STATE NEVER CROSSES FOR A READ, AND DstSize IS THE TIGHT EXTENT. // The first version of this computed GL 4.6 8.4.4's PACKED size - row length, skips, // alignment - and handed it over as DstSize. v1's server allocates exactly DstSize and // the real backend honours the live pack state, so a 4x3 RGBA8 read with // PACK_ROW_LENGTH=8, SKIP_ROWS=1, SKIP_PIXELS=2 allocated 80 bytes and the driver wrote // to byte 120. That is the shape of the joint inproc lane's two // DepthReadbackHonoursThePackPixelStoreParameters SEGFAULTs, on both backends. // // So the wire carries a RECTANGLE and not a layout: the server reads with NEUTRAL pack // state into a tight w*h*bytesPerPixel run that IS the reply payload, and the CLIENT - // which is the side that holds the application's pack state, and the only side that // can - scatters those rows into the application's pointer. "OnReadPixels writes // exactly DstSize bytes" still holds; DstSize is now a number both sides derive from // the same three values instead of one side deriving it from state the other cannot // see. // // IT IS FORMAT-AGNOSTIC ON PURPOSE. The depth and depth-stencil reads the 21 split // entries touch take the same rule with no special case, because the rule is about the // LAYOUT and not about the component: bytesPerPixel is whatever the format sizes to. Uint64 ReadbackBytesPerPixel(GLenum format, GLenum type) { const TextureInputFormat inputFormat = MG_Util::ConvertGLEnumToTextureInputFormat(format); const TexturePixelDataType dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); const SizeT bytesPerPixel = MG_Util::GetInputBytesPerPixel(inputFormat, dataType); if (bytesPerPixel == 0) { // NOT a guess and not a zero-length reply. A format this build cannot size is a // readback whose answer would be silently truncated, which is the one failure a // picture comparison cannot see. MGLOG_F("MGPipe: Fatal{UnsizedReadback, \"read_pixels\"} format=0x%04x type=0x%04x " "- the client must declare MGPReadbackInfo::DstSize and cannot size this " "pair; P5's reduced path reads RGBA/UNSIGNED_BYTE", static_cast(format), static_cast(type)); std::abort(); } return static_cast(bytesPerPixel); } Uint64 TightReadbackBytes(GLsizei width, GLsizei height, GLenum format, GLenum type) { if (width <= 0 || height <= 0) return 0; return static_cast(width) * static_cast(height) * ReadbackBytesPerPixel(format, type); } void ApplyReadbackByteSwap(void* pixels, Uint64 bytes, GLenum type, const PixelStoreParameters& pack) { if (!pack.SwapBytes || pixels == nullptr) return; const auto dataType = MG_Util::ConvertGLEnumToTexturePixelDataType(type); SizeT group = MG_Util::GetSizedTexturePixelDataTypeSize(dataType); if (group == 0) group = MG_Util::GetBaseTexturePixelDataTypeSize(dataType); // This packed depth/stencil type contains two independent 32-bit words. if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) group = 4; if (group <= 1) return; auto* data = static_cast(pixels); for (Uint64 at = 0; at + group <= bytes; at += group) { for (SizeT i = 0; i < group / 2; ++i) { std::swap(data[at + i], data[at + group - 1 - i]); } } } // M2 / codex 11: the reply the server posted is COMPLETE and OK. A short OK reply, and a // DECLINED or ERROR reply with a zero payload, both leave the destination full of stale // bytes; scattering or returning it is the silently truncated picture ID-47's own comment // says an SSIM comparison cannot see, arriving through the status field rather than // through truncation. `expected` is the record's own DstSize (CONTRACT-P5 row 23: the // reply's exact extent). The predicate is at namespace scope (below) and exposed for the // control, so R-16's "drive the production predicate" holds rather than a second copy of // the rule in the test. // The same predicate at the call site, with the named Fatal each failure mode owns. // status > expected cannot reach here: EmitAndWait already aborts Fatal{ReplyTooLarge} on // an oversize reply, so the only failures left are a wrong status or a SHORT one. void RequireReadbackReplyComplete(Int32 status, Uint64 replySize, Uint64 expected) { if (status == Wire::ReplySink::kStatusError) { MGLOG_F("MGPipe: Fatal{ReplyError, \"ReadPixels\"} - the readback answered ERROR; " "the destination is left untouched rather than filled with stale bytes"); std::abort(); } if (status == Wire::ReplySink::kStatusDeclined) { MGLOG_F("MGPipe: Fatal{ReadbackDeclined, \"ReadPixels\"} - the server has no " "GL.ReadPixels and DECLINED; a decline is a real answer for an acceptance " "row (R-5) but a blocking readback has no pixels to return, so it is a " "Fatal here rather than a buffer of stale bytes"); std::abort(); } if (replySize != expected) { MGLOG_F("MGPipe: Fatal{ReadbackReplyShort, \"ReadPixels %llu < %llu\"} - the OK " "reply carried fewer bytes than the read's own DstSize (CONTRACT-P5 row " "23's exact extent); the missing rows would otherwise be scattered as " "whatever the destination held", static_cast(replySize), static_cast(expected)); std::abort(); } } void EmitReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { ClientSession& session = RequireSession("ReadPixels"); // ID-57 / M8: A PACK-PBO DESTINATION IS REFUSED BY NAME, BEFORE ANY EMISSION AND // BEFORE `pixels` IS TOUCHED. With GL_PIXEL_PACK_BUFFER bound, the frontend permits // `pixels` to be a byte OFFSET into that buffer, not an address (GL_Framebuffer.cpp: // 3055 aligns it to the type size, one byte for UNSIGNED_BYTE) - and this emitter has // no PBO branch: it sets DstOffset = 0, hands the offset to EmitAndWait as a host // buffer, and the reply is memcpy'd to CPU address . Under monolith the // backend maps the PBO and writes the reply into the buffer (unchanged). The real // split form - the server writes the reply into the buffer resource and the client // marks it GPU-written (b1's MarkReadPixelsPackBuffer becoming the producer contract // §3 names) - is a P6 ROADMAP item. In P5 it is class C's shape (R-4), refused here. if (MG_State::pGLContext != nullptr && MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack) .GetBoundObject()) { UnmigratedVerbFatal("ReadPixels+PACK_BUFFER"); } BeforeReadOnlyVerb(); // THE PBO HALF IS b1's DESIGN AND b1 ALREADY WIRED ITS MARK, at // GL_Framebuffer.cpp:3109 - immediately after this table call returns, inside // ReadPixels_Backend itself. So this emitter deliberately does NOT call // MarkReadPixelsPackBuffer(): a second call there would be the "wire it twice" // shape, and the per-row counter b1's unit cases assert on would then count one // read as two. (In P5 the refusal above means no PBO read reaches here at all; the // note stays for the P6 form.) if (width <= 0 || height <= 0) return; const Uint64 bytesPerPixel = ReadbackBytesPerPixel(format, type); // ONE tight-size function for production AND the control (M3 / codex 10a). The first // cut computed this inline here while the test drove TightReadbackByteCount, so a // `+16` on the production line stayed green - the test observed a different number. // Now the number the server allocates and the number the test asserts come from the // same body. const Uint64 tight = TightReadbackBytes(width, height, format, type); MG_Pipe::MGPReadbackInfo info{}; info.Res = MG_Pipe::kMGPipeNullHandle; // "the bound read surface answers" info.Box = MG_Pipe::MGPBox{x, y, 0, static_cast(width), static_cast(height), 1}; info.Format = static_cast(format); info.Type = static_cast(type); info.Target = 0; info.Level = 0; info.DstOffset = 0; info.DstSize = tight; // CHECKED BEFORE THE EMISSION, not after the answer (ID-47), and through S1's // HELPER rather than a copy of it here. A reply bigger than a slot is Fatal on the // SERVER too, and s1 keeps that as the last line of defence - but it fires on the // apply thread with the record already on the wire, where all the client sees is a // hang. This one names the read, at the call site that knows what the read was. // // THE NUMBER IS ID-49's TIGHT EXTENT and not the packed one: the pack state never // crosses, so the answer that has to fit a slot is w*h*bytesPerPixel. The cap is // the pool's own, read live, so s1's growth of SEG_REPLY to 16 MiB / eight 2 MiB // slots needed no edit in this file - only the merge. session.RequireReadPixelsReplyFits(static_cast(width), static_cast(height), static_cast(format), static_cast(type), tight); PixelStoreParameters pack{}; if (MG_State::pGLContext != nullptr) { pack = MG_State::pGLContext->GetPixelStoreParameters(/*isUnpack=*/false); } Int32 status = 0; Uint64 replySize = 0; if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) { // THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the // destination layout IS the tight layout, so the reply lands straight in the // application's buffer and there is no bounce at all. It is a fast path for the // SAME bytes, not a second rule: ScatterTightReadback below is a memcpy of the // whole run in exactly this case, and the control drives that function. session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0, pixels, tight, &status, &replySize); // M2 / codex 11: an OK reply that arrived short, or a DECLINE/ERROR, must not be // handed back as pixels. The Fatal aborts before the application reads the buffer, // so the bytes EmitAndWait already copied into `pixels` are never observed. RequireReadbackReplyComplete(status, replySize, tight); ApplyReadbackByteSwap(pixels, tight, type, pack); return; } // The bounce is the price of the application having asked for a layout. It is the // tight size and never more, and it is freed before this returns - R-11's rule one // level out: nothing here outlives the call. Vector bounce(static_cast(tight)); session.EmitAndWait(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info), nullptr, 0, bounce.data(), tight, &status, &replySize); // BEFORE THE SCATTER, so a short or non-OK reply never reaches the application's // pointer at all (the bounce is the only thing that held the partial bytes). RequireReadbackReplyComplete(status, replySize, tight); ApplyReadbackByteSwap(bounce.data(), tight, type, pack); ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel, pack); } void EmitPresent() { ClientSession& session = RequireSession("Present"); BeforeReadOnlyVerb(); // ---- R-10's and R-9's readings, published BEFORE the present record ------------ // // THE FRAME BOUNDARY IS THE RIGHT PLACE and the per-record path is the wrong one: // the encoder keeps all five as run totals, so this is five relaxed stores per // frame rather than five per record. Guarded by Enabled() like every other counting // site in the tree, so the cost with MOBILEGL_PIPE_STATS unset is a global load and // a predicted branch. // // AND IT IS BEFORE THE EmitAndWait BELOW, WHICH IS NOT A DETAIL. PipeStats::OnPresent // is called by the SERVER's Present - i.e. from inside the apply of the very record // this function is about to emit - so a publish placed after it lands one frame // late, and the FIRST summary line of every run then reads `maxrec=0 maxcap=0`. // Measured that way once: a zero that means "not published yet" is printed in the // same shape as a zero that means "nothing crossed", and the second one is a real // defect (an emit table that fell through to the driver). The Present record is 24 // bytes and cannot be the maximum, so nothing is lost by reading one record early. if (MG_Util::PipeStats::Enabled()) { const Wire::PipeWireEncoder& encoder = session.Encoder(); using MG_Util::PipeStats::Gauge; MG_Util::PipeStats::PublishGauge(Gauge::MaxRecordBytes, encoder.MaxRecordBytesSeen()); MG_Util::PipeStats::PublishGauge(Gauge::MaxRecordBytesCap, encoder.MaxRecordBytesCap()); MG_Util::PipeStats::PublishGauge(Gauge::RingWraps, encoder.CmdWraps()); MG_Util::PipeStats::PublishGauge(Gauge::RingWrapPads, encoder.CmdWrapPads()); MG_Util::PipeStats::PublishGauge(Gauge::RingWaits, encoder.StageReclaimWaits()); // AND THE ROW, WHENEVER THE MAXIMUM MOVES. The summary line can carry the // number but not the name - MG_Util is below MG_Remote and has no WireOpName - // and the name is the actionable half: R-10 makes the integrator choose between // early chunking and a bigger ring, and that is a decision about a record // FAMILY. ClientSession::Stop prints the same pair at teardown, but a trace // replay never reaches it (measured: the OpenRA lane's library log ends mid-run // with no teardown line at all), so a stats-enabled run would otherwise publish // a size with no row. Emitted only when the maximum actually grows, so it is // bounded by the number of distinct maxima - five or six in a whole replay. if (encoder.MaxRecordBytesSeen() > g_publishedMaxRecordBytes) { g_publishedMaxRecordBytes = encoder.MaxRecordBytesSeen(); MGLOG_I("MGPipe: wire ledger: new maximum record - maxrec=%llu " "maxrecop=%s cap=%llu (R-10's proof obligation; P5 does not chunk)", static_cast(g_publishedMaxRecordBytes), encoder.MaxRecordOpName(), static_cast(encoder.MaxRecordBytesCap())); } } MG_Pipe::MGPPresent record{}; // FrameSerial 0 = "the server stamps its own". P5 has no client-side present credit // (MOBILEGL_IPC_PRESENT_CREDIT is P6's), so a client-minted serial would be a second // id space with no consumer. record.FrameSerial = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::Present, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); // R-12's invalidation edge, drained at the one boundary every target crosses. A // second CapsSnapshot IS the invalidation; nothing else on the client can see that // the server re-ran InitCapabilities, because GLContext::GetCompileEnv()'s memo is // keyed on pActiveBackendObject.get() (Core.cpp:34) and that pointer never changes // under split. session.PumpControlPlane(); ++g_presentOrdinal; LogE2ControlLine(g_presentOrdinal); } // ============================================================================= // CLASS B - P5b package i1: image bind, compute, barriers, copy-image, SSBO block // (MG_Remote/CONTRACT-P5B.md §2 i1). Seven slots, five wire rows. // ============================================================================= // // RULE D, WHICH IS WHY THESE ARE SHORT. A P5b verb crosses AS THE CALL: the record // carries the GL arguments verbatim - the enums as tokens, the GL names the backend // keys on - beside the handle the P7/P8 form will dispatch on instead, and the server's // ServerVerbSink reproduces the backend call the monolith makes. The backend keeps // reading the frontend state it reads today through the BARRIER-PULLED fields of its // verb class, which the record's own verb stamp is what makes legal. So a migration is // one emitter here plus one sink body there, and nothing in the backend moves. // // THE HANDLES ARE LOOKED UP, NEVER MINTED, and that is a ruling (i1-v1 §4). The sinks // below dispatch on the GL NAME - that is the whole point of carrying it - so a handle // is carried for P7's sake only. FindByLifetimeId answers the handle the resource // subsystem has already published for this object and kMGPipeNullHandle when it has // published none; Acquire would MINT one here instead, at a call site that emits no // create record, and the server would then be handed an identity it has never seen. // A null Res/Src/Dst/ShaderCso therefore means "no handle published yet", which is a // true statement, rather than a slot nobody allocated. MG_Pipe::MGPipeHandle PublishedTextureHandle( const SharedPtr& texture) { if (!texture) return MG_Pipe::kMGPipeNullHandle; return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId()); } // glBindImageTexture. Emitted AT THE CALL, after the frontend has written the unit's // ImageTextureBinding and MGP_FILL(BindImageTexture) has run - the record's verb // boundary is what makes the server's read of that binding legal. set_shader_images // (the draw-prep set) still travels at the next validate, untouched: that record // describes a resolved unit for the draw, this one reproduces a call. // // NO PRE-VERB HOOK, AND THAT IS DELIBERATE. b1's two hooks describe "the work the // record is ABOUT TO START" - PushPersistentMapsBeforeVerb publishes bytes an // application wrote through a coherent map, MarkGpuWrites* builds the GPU-write set. // A bind starts no shader and reads no buffer; the dispatch that later reads this image // is the verb that carries both hooks, and running them here as well would push the // same maps twice per dispatch and inflate b1's per-row counters. void EmitBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) { ClientSession& session = RequireSession("BindImageTexture"); MG_Pipe::MGPImageBind record{}; // The unit's binding is the frontend's and has just been written by the caller, so // the texture this record names is the one the server's SyncImageTextureBinding // will pull for the same unit. Read from MG_State::pGLContext and NOT through // MGB_CTX: on this side of a split MGB_CTX is gPipeInputs, which is the SERVER's // view, and the client asking it a question is how the two halves come to disagree. if (MG_State::pGLContext != nullptr) { record.Res = PublishedTextureHandle( MG_State::pGLContext->GetImageTextureBinding(static_cast(unit)).Texture); } record.Unit = static_cast(unit); // The GL name the application passed, verbatim - what the ES slot is handed as // `texture` and currently ignores. Never an identity (ARCHITECTURE 4.2.1). record.GlName = static_cast(texture); record.Level = static_cast(level); record.Layer = static_cast(layer); // THE GL ACCESS TOKEN, not MGPImageView::Access's three-value encoding (table 0's // MGPImageBind::Access row). Two records, two jobs. record.Access = static_cast(access); record.Format = static_cast(format); record.Layered = layered != GL_FALSE ? 1 : 0; session.EmitAndWait(MG_Pipe::MGPWireOp::BindShaderImage, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // glDispatchCompute. THE HOOK ORDER IS b1's AND IS INHERITED FROM THE CLASS-C STUB // VERBATIM: push the persistent maps (they produce resource_subdata records that must // precede the verb on SEG_CMD), then the dispatch mark walk, then the record. The stub // carried both calls before its Fatal precisely so that the package which flipped this // slot would inherit a call site that was already correct. void EmitDispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { ClientSession& session = RequireSession("DispatchCompute"); PushPersistentMapsBeforeVerb(); MarkGpuWritesForDispatch(); MG_Pipe::MGPGridInfo record{}; record.GridX = static_cast(numGroupsX); record.GridY = static_cast(numGroupsY); record.GridZ = static_cast(numGroupsZ); // Block* STAY 0 IN P5b (contract i1): the local size is a link artifact the backend // reads from its own program, and a client-minted copy would be a second statement // of it. P7's Magma may fill it from the reflection archive. record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; record.IndirectOffset = 0; record.IsIndirect = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitDispatchComputeIndirect(GLintptr indirect) { ClientSession& session = RequireSession("DispatchComputeIndirect"); PushPersistentMapsBeforeVerb(); MarkGpuWritesForDispatch(); MG_Pipe::MGPGridInfo record{}; // The counts come from the GL_DISPATCH_INDIRECT_BUFFER, so the three grid fields are // 0 and IsIndirect is what says so; the sink dispatches on it and calls // glDispatchComputeIndirect with the offset the application spelled. record.IsIndirect = 1; record.IndirectOffset = static_cast(indirect); record.IndirectBuffer = MG_Pipe::kMGPipeNullHandle; if (MG_State::pGLContext != nullptr) { const auto& bound = MG_State::pGLContext ->GetBufferBindingSlot(::MobileGL::BufferTarget::DispatchIndirect) .GetBoundObject(); if (bound) { record.IndirectBuffer = MG_Pipe::MGPipeSlots().FindByLifetimeId( MG_Pipe::MGPipeKind::Buffer, bound->GetLifetimeId()); } } session.EmitAndWait(MG_Pipe::MGPWireOp::LaunchGrid, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // glMemoryBarrier / glMemoryBarrierByRegion. The bits cross VERBATIM: the frontend has // already validated them and already folds glTextureBarrier onto the same field // (GL_Drawing.cpp:920-937), and Espryt's atomic-counter lowering - the counter bit // implying the storage bit - stays inside the backend where the reason for it lives // (DirectGLES.cpp:8837). A client that pre-lowered would be answering a driver question // from the wrong side and the two arms would stop being byte-identical. // // No pre-verb hook: a barrier orders memory the GPU already holds. It starts no shader // and reads no mapped buffer. void EmitMemoryBarrier(GLbitfield barriers) { ClientSession& session = RequireSession("MemoryBarrier"); MG_Pipe::MGPMemoryBarrier record{}; record.Bits = static_cast(barriers); record.ByRegion = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitMemoryBarrierByRegion(GLbitfield barriers) { ClientSession& session = RequireSession("MemoryBarrierByRegion"); MG_Pipe::MGPMemoryBarrier record{}; record.Bits = static_cast(barriers); record.ByRegion = 1; session.EmitAndWait(MG_Pipe::MGPWireOp::MemoryBarrier, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // glCopyImageSubData -> resource_copy_region (53), which P5b rules is glCopyImageSubData // ONLY (contract §6.4; the framebuffer-sourced copies are f1's row 76). void EmitCopyImageSubData(const MG_Backend::CopyImageEndpoint& src, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, const MG_Backend::CopyImageEndpoint& dst, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) { ClientSession& session = RequireSession("CopyImageSubData"); // ID-57's SHAPE: REFUSED BY NAME, BEFORE ANY EMISSION. GL 4.6 core 18.3.2 accepts // GL_RENDERBUFFER as either endpoint, and an endpoint is a sum type for exactly that // reason - but no sticky forward hands out a renderbuffer object, so the sink has no // way to rebuild one from a name, and none was measured. P7 is where the backend // takes handles and this arm becomes ordinary. The sink refuses the same shape by // the same name if a record ever reaches it (defence on both sides of one wire). if (src.IsRenderbuffer() || dst.IsRenderbuffer()) { UnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER"); } MG_Pipe::MGPCopyRegion record{}; record.Src = PublishedTextureHandle(src.Texture); record.Dst = PublishedTextureHandle(dst.Texture); // The GL names beside the handles: the key MGB_CTX->GetTextureObject(name) takes on // the far side (a BARRIER-PULLED sticky forward, counted in `rsp`, retired by P7). record.SrcGlName = src.Texture ? static_cast(src.Texture->GetExternalIndex()) : 0u; record.DstGlName = dst.Texture ? static_cast(dst.Texture->GetExternalIndex()) : 0u; // The GL targets verbatim, in a Uint16 - every GL texture target fits one. NOT // MGPipeResourceTarget: the sink only ever forwards these to a slot that takes GL // enums, and the tree has no resource-target -> GL-enum inverse to spend on them. record.SrcTarget = static_cast(srcTarget); record.DstTarget = static_cast(dstTarget); record.SrcLevel = static_cast(srcLevel); record.DstLevel = static_cast(dstLevel); // SrcBox is {srcX, srcY, srcZ, w, h, d}: the source origin AND the extent, which is // one extent for both endpoints (GL spells the copy's size once). record.SrcBox = MG_Pipe::MGPBox{srcX, srcY, srcZ, static_cast(srcWidth), static_cast(srcHeight), static_cast(srcDepth)}; record.DstX = dstX; record.DstY = dstY; record.DstZ = dstZ; session.EmitAndWait(MG_Pipe::MGPWireOp::ResourceCopyRegion, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // glShaderStorageBlockBinding -> set_storage_block_binding (75), the ONE content-carrying // row P5b adds. The block is NAMED, not indexed, because the application's index is the // frontend interface-query enumeration's and no backend shares that index space // (BackendObject.h:216-221) - the name is the one coordinate all three agree on. void EmitShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { ClientSession& session = RequireSession("ShaderStorageBlockBinding"); // The backend slot's own first line (DirectGLES.cpp:9201), kept here so a null name // never becomes a zero-size blob - which rule A forbids spelling at all. if (storageBlockName == nullptr) return; MG_Pipe::MGPStorageBlockBinding record{}; record.GlName = static_cast(program); record.Binding = static_cast(storageBlockBinding); record.ShaderCso = MG_Pipe::kMGPipeNullHandle; if (MG_State::pGLContext != nullptr) { const auto& programObject = MG_State::pGLContext->GetProgramObject(program); if (programObject) { record.ShaderCso = MG_Pipe::MGPipeSlots().FindByLifetimeId( MG_Pipe::MGPipeKind::ShaderCso, programObject->GetLifetimeId()); } } // Size = strlen + 1: THE NUL TRAVELS (contract table 0's block-name row). The // decoder re-terminates into a bounded local and refuses a run whose last byte is // not NUL, so the two sides agree on where the name ends. const Uint64 nameBytes = static_cast(std::strlen(storageBlockName)) + 1ull; record.Name = session.Encoder().StageBytes(storageBlockName, nameBytes); session.EmitAndWait(MG_Pipe::MGPWireOp::SetStorageBlockBinding, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // CLASS B - P5b package t2: the transform-feedback spans, the XFB object bind and the // tessellation patch parameter (MG_Remote/CONTRACT-P5B.md §2 t2). // ============================================================================= // // SIX SLOTS, SIX ROWS, AND NOT ONE OF THEM STARTS A SHADER. Every one of the six is a // control call - it opens, closes, pauses, resumes or re-targets a capture span, or sets // the patch size the next tessellation draw uses - so each takes BeforeReadOnlyVerb(), // which is CONTRACT-P5B.md §4's rule for "a verb that reads buffers but starts no // shader" naming the XFB and patch controls by hand. The GPU-WRITE MARK FOR THE CAPTURE // TARGETS IS NOT TAKEN HERE and that is deliberate: it belongs at the END of the span, // after the record and before GLContext::EndTransformFeedback clears the live bindings, // which is exactly where b1 already put it (GL_Drawing.cpp's // MarkEndTransformFeedbackCaptureTargets). Taking it here as well would mark the same // buffers twice and taking it INSTEAD of there would mark nothing. // // RULE D (CONTRACT-P5B.md §0): each record carries the GL arguments the frontend handed // the backend slot and nothing that is a READING of them. The capture program, the // capture-buffer bindings, the patch state and the bound XFB object all stay // BARRIER-PULLED - the server's backend reads the client's gPipeInputs fill of the // moment, which MGP_FILL at each call site has just written and the verb barrier holds // still (R-1). That is why these are six two-line emitters and not an XFB protocol. // // WHAT MUST HAVE CROSSED BEFORE begin_stream_output, since it is the ordering question // this package was asked: the capture buffers' own resource records (emitted at their // own call sites through the resource family, long before this point), the program // (the CSO/program family, likewise), and the buffer BINDINGS - which do not cross as a // record at all, because set_stream_output_targets (39) has no producer and no consumer // and CONTRACT-P5B.md §2 rules it NOT required for t2: under the barrier the server's // StartPendingTransformFeedback reads them through the kXfbSpan/kDraw pulls // (GetTransformFeedbackProgram, GetBufferBindingPoint). Producing that row is P9's. void EmitBeginTransformFeedback(GLenum primitiveMode) { ClientSession& session = RequireSession("BeginTransformFeedback"); BeforeReadOnlyVerb(); MG_Pipe::MGPStreamOutputBegin record{}; // The GL token verbatim (contract table 0's "GL enums on the wire"): the sink hands // it to the backend slot that takes it, and nothing between here and there reads it. record.PrimitiveMode = static_cast(primitiveMode); session.EmitAndWait(MG_Pipe::MGPWireOp::BeginStreamOutput, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitEndTransformFeedback() { ClientSession& session = RequireSession("EndTransformFeedback"); BeforeReadOnlyVerb(); MG_Pipe::MGPXfbAccounting record{}; // THE ACCOUNTING IS THE CLIENT'S OWN AND IT IS INFORMATIONAL ON THIS SIDE OF THE // WIRE: end_stream_output's backend call takes no arguments, and the three numbers // are what the frontend has counted over this span (CONTRACT-P5B.md §2 t2, the // companions row). They travel because the row has carried them since P4a and // because they are what a server-side scatter would need when P9 lands one; the // sink today calls GL.EndTransformFeedback() and reads none of them. Read here, // BEFORE GLContext::EndTransformFeedback resets the counters at the call site. const auto& context = *MG_State::pGLContext; record.CapturedVertices = context.GetTransformFeedbackCapturedVertices(); record.PrimitivesWritten = context.GetTransformFeedbackPrimitiveCounter(); record.PrimitiveMode = static_cast(context.GetTransformFeedbackPrimitiveMode()); session.EmitAndWait(MG_Pipe::MGPWireOp::EndStreamOutput, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitPauseTransformFeedback() { ClientSession& session = RequireSession("PauseTransformFeedback"); BeforeReadOnlyVerb(); // Reserved IS zero and the contract says so (MGPStreamOutputControl{Reserved = 0}). // The row exists to BE the verb boundary - the stamp the server puts up before the // sink runs - not to carry anything. MG_Pipe::MGPStreamOutputControl record{}; record.Reserved = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::PauseStreamOutput, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitResumeTransformFeedback() { ClientSession& session = RequireSession("ResumeTransformFeedback"); BeforeReadOnlyVerb(); MG_Pipe::MGPStreamOutputControl record{}; record.Reserved = 0; session.EmitAndWait(MG_Pipe::MGPWireOp::ResumeStreamOutput, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitBindTransformFeedback(GLuint name) { ClientSession& session = RequireSession("BindTransformFeedback"); BeforeReadOnlyVerb(); MG_Pipe::MGPStreamOutputBind record{}; // THE GL NAME IS NOT AN IDENTITY (ARCHITECTURE 4.2.1) and is carried anyway, because // it is the key the backend has always used: Espryt indexes its driver objects by it // (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and generates the ES object on // first bind. Name 0 is the default object, which is why the field is not a handle. record.GlName = static_cast(name); // Beside it, the identity that WILL dispatch: the frontend's per-object lifetime id, // process-wide and never reused, which is what survives glGenTransformFeedbacks // recycling a name. Read AFTER GLContext::BindTransformFeedbackObject at the call // site, so it is the id of the object being bound and not of the previous one. record.LifetimeId = MG_State::pGLContext->GetBoundTransformFeedbackLifetimeId(); session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } void EmitPatchParameteri(GLenum pname, GLint value) { ClientSession& session = RequireSession("PatchParameteri"); BeforeReadOnlyVerb(); MG_Pipe::MGPPatchParameter record{}; // GL_PATCH_VERTICES is the only pname that reaches a backend slot - the frontend // answers GL_PATCH_DEFAULT_*_LEVEL itself and bakes those into the synthesized // control stage - and the frontend has already rejected every other pname with // INVALID_ENUM before this call (GL_Drawing.cpp's PatchParameteri). Carried verbatim // so the sink reproduces the call rather than a reading of it. record.Pname = static_cast(pname); record.Value = static_cast(value); // set_patch_state (43) STILL TRAVELS, at the next validate, and that is not a // duplicate: it is the applier's working-block copy, this is the driver push Espryt // does AT THE CALL (DirectGLES.cpp:8760), and both pushes happen today on the // monolith path too (CONTRACT-P5B.md §2 t2). session.EmitAndWait(MG_Pipe::MGPWireOp::PatchParameter, &record, sizeof(record), nullptr, 0, nullptr, 0, nullptr); } // ============================================================================= // CLASS A - answered locally from the caps mirror (R-15). NO RECORD, EVER. // ============================================================================= void AnswerGetIntegeri_v(GLenum target, GLuint index, GLint* data) { if (data == nullptr) return; const MG_Backend::DynamicBackendParameters& dynamic = CapsMirrorInstance().Dynamic(); // The ONLY two indexed pnames the device owns; every other indexed pname names // frontend state and is answered in GL_Getter::GetIntegeri_v before any table is // consulted (BackendObject.h:196-205). The existing gate is // AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer, which pins // that this answer and the caps copy are ONE number. switch (target) { case GL_MAX_COMPUTE_WORK_GROUP_COUNT: if (index < 3) *data = static_cast(dynamic.MaxComputeWorkGroupCount[index]); return; case GL_MAX_COMPUTE_WORK_GROUP_SIZE: if (index < 3) *data = static_cast(dynamic.MaxComputeWorkGroupSize[index]); return; default: // Not a Fatal: the slot's own contract is "whatever pname the frontend has no // case for at all", and the monolith backends answer such a pname by leaving // the driver's own default in place. Answering a wrong number would be worse // than answering none. MGLOG_W_ONCE("MG_Remote client: GetIntegeri_v(0x%04x, %u) is not one of the two " "device-owned indexed pnames and has no caps-mirror answer", static_cast(target), static_cast(index)); return; } } Bool AnswerIsTimerQuerySupported() { // A capability predicate, not a call. Today a null slot means COUNTER_BITS == 0 // (GL_Query.cpp:792) - which is precisely the null check R-4 forbids, so it moves // here, to the bit the server published. return CapsMirrorInstance().HasCap(MG_Pipe::kCapTimerQuery); } // ============================================================================= // The frontend sees a local opaque token; neither this address nor the driver's // BackendSyncHandle is serialized. The Fence-kind slot allocator supplies wire identity. struct RemoteFenceProxy { MG_Pipe::MGPipeHandle Handle; }; std::mutex g_fenceMutex; UnorderedMap> g_fenceProxies; MG_Pipe::MGPipeHandle FenceHandle(MG_Backend::BackendSyncHandle proxy) { const auto it = g_fenceProxies.find(proxy); if (it == g_fenceProxies.end()) Wire::WireProtocolFatal("Fence.proxy", "unknown client fence proxy"); return it->second->Handle; } MG_Backend::BackendSyncHandle EmitFenceSync() { ClientSession& session = RequireSession("FenceSync"); const std::lock_guard lock(g_fenceMutex); BeforeReadOnlyVerb(); auto proxy = MakeUnique(); proxy->Handle = MG_Pipe::MGPipeSlots().Allocate(MG_Pipe::MGPipeKind::Fence); const MG_Pipe::MGPHandleOnly desc{proxy->Handle, static_cast(MG_Pipe::MGPipeKind::Fence), 0}; session.EmitAndWait(MG_Pipe::MGPWireOp::FenceCreate, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr); auto* local = proxy.get(); g_fenceProxies.emplace(local, std::move(proxy)); return local; } Uint32 ReadFenceReply(ClientSession& session, MG_Pipe::MGPWireOp op, const void* payload, Uint64 bytes) { Uint32 result = 0; Int32 status = Wire::ReplySink::kStatusError; Uint64 replyBytes = 0; session.EmitAndWait(op, payload, bytes, nullptr, 0, &result, sizeof(result), &status, &replyBytes); if (status != Wire::ReplySink::kStatusOk || replyBytes != sizeof(result)) Wire::WireProtocolFatal("Fence.reply", "missing or malformed sync result"); return result; } GLenum EmitClientWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) { ClientSession& session = RequireSession("ClientWaitSync"); const std::lock_guard lock(g_fenceMutex); const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0}; return ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceWait, &request, sizeof(request)); } Bool EmitGetSyncStatus(MG_Backend::BackendSyncHandle proxy) { ClientSession& session = RequireSession("GetSyncStatus"); const std::lock_guard lock(g_fenceMutex); const MG_Pipe::MGPHandleOnly desc{FenceHandle(proxy), static_cast(MG_Pipe::MGPipeKind::Fence), 0}; const Uint32 result = ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceStatus, &desc, sizeof(desc)); if (result > 1) Wire::WireProtocolFatal("FenceStatus.reply", "status must be boolean"); return result != 0; } void EmitWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) { ClientSession& session = RequireSession("WaitSync"); const std::lock_guard lock(g_fenceMutex); const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0}; session.EmitAndWait(MG_Pipe::MGPWireOp::FenceWaitServer, &request, sizeof(request), nullptr, 0, nullptr, 0, nullptr); } void EmitDeleteSync(MG_Backend::BackendSyncHandle proxy) { const std::lock_guard lock(g_fenceMutex); const auto handle = FenceHandle(proxy); // MobileGL::Destroy stops the server BEFORE DestroyAllSyncObjects. Detach already // released those native objects on the apply thread; only the local proxy remains. if (auto* session = ClientSession::Active(); session != nullptr && session->Started()) { const MG_Pipe::MGPHandleOnly desc{handle, static_cast(MG_Pipe::MGPipeKind::Fence), 0}; session->EmitAndWait(MG_Pipe::MGPWireOp::FenceDestroy, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr); } MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Fence, handle); g_fenceProxies.erase(proxy); } // CLASS C - 16 slots remain after d1/i1/t2/f1; each names its first blocker. // ============================================================================= // // PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md, // ~/w7/notes/p5b/BRIEF-P5B.md), so that four packages migrating in parallel edit four // DISJOINT lists and four DISJOINT counts rather than one list and one number. To flip a // slot a package (1) removes its X row from ITS list, (2) assigns the real emitter in // BuildRemoteEmitTable's class-B block, (3) raises ITS kEmittedSlots* by one. The // per-package ownership assertions below then still hold, the totals stay arithmetic, // and a slot that changes class without changing the arithmetic is a build break. The // X-macro shape is kept so the DEFINITION and the ASSIGNMENT cannot drift apart. Three // slots carry a body the macro cannot (DispatchCompute, DispatchComputeIndirect, // SetSwapInterval) and are written out by hand below. // // d1 indexed / instanced / multi-draw / indirect draws -> draw_vbo (59), its // kDrawIsIndirect tail and its kDrawHasUserIndices span // i1 image bind, compute, barriers, copy-image, storage block -> bind_shader_image // (72), launch_grid (60), memory_barrier (61), resource_copy_region (53), // set_storage_block_binding (75) // t2 the XFB spans and object bind, the patch parameter -> begin/end/pause/resume_ // stream_output (62..65), bind_stream_output (74), patch_parameter (73) // f1 the clear family, the framebuffer-sourced copies, mips -> clear (57), // copy_framebuffer_to_texture (76), generate_mipmap (54) // tail the wave-3 remainder nothing measured: queries, syncs, the texture readbacks, // the DSA blit, the swap interval (census-classC.md "static cross") // d1 v1 flipped all nineteen (DrawElements, DrawElementsBaseVertex, MultiDrawArrays, // MultiDrawElements, MultiDrawElementsBaseVertex, MultiDrawElementsIndirect, // MultiDrawArraysIndirect, MultiDrawElementsIndirectCount, MultiDrawArraysIndirectCount, // DrawRangeElementsBaseVertex, DrawRangeElements, the four DrawElementsInstanced*, // DrawArraysInstancedBaseInstance, DrawArraysInstanced, DrawElementsIndirect, // DrawArraysIndirect) to class B; the list is kept, empty, so the ownership assertion // below still reads 0 + 19 = 19 and a slot that fell back in would have to be added here. #define MGR_UNMIGRATED_D1_SLOTS(X) // i1 HAS LANDED: the list is EMPTY and all seven slots are class B (the five emitters // above plus the two compute ones). It is kept as an empty macro rather than deleted so // that MGR_UNMIGRATED_GL_SLOTS' union, kUnmigratedI1's arithmetic and the ownership // static_assert below all keep their shape - and so the next package to need a row here // (a P5b wave-3 image/compute slot) has the partition to put it in. #define MGR_UNMIGRATED_I1_SLOTS(X) // t2 LANDED (CONTRACT-P5B.md §2 t2): the three measured slots - BeginTransformFeedback // (95 lane entries), PatchParameteri (43), BindTransformFeedback (2) - and the three // companions that share their rows are class B now and live in the block above. // DeleteTransformFeedback is the one that stays: it has NO ROW in P5b, by ruling and // not by omission (unmeasured; the driver object leaks on the server until P9's XFB // namespace work, and a bind of name 0 is what the backend does on delete of the bound // one, DirectGLES.cpp:1422). It therefore still aborts by its own name. #define MGR_UNMIGRATED_T2_SLOTS(X) \ X(DeleteTransformFeedback, void, (GLuint)) #define MGR_UNMIGRATED_F1_SLOTS(X) // The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot). #define MGR_UNMIGRATED_TAIL_SLOTS(X) \ X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \ X(GetTextureImage, void, \ (const SharedPtr&, TextureUploadTarget, GLint, GLenum, \ GLenum, GLsizei, GLvoid*)) \ X(EndTimeElapsedQuery, void, (MG_Backend::BackendQueryHandle)) \ X(DeleteBackendQuery, void, (MG_Backend::BackendQueryHandle)) \ X(EndOcclusionQuery, void, (MG_Backend::BackendQueryHandle)) \ X(EndXfbPrimitivesQuery, void, (MG_Backend::BackendQueryHandle)) // The non-void ones, kept apart only because the macro body differs: a [[noreturn]] // call is a complete body for a void slot and for a value-returning one alike, but a // compiler that does not see UnmigratedVerbFatal's attribute through the macro would // warn on the second. It does see it; they are split for readability. All ten are the // wave-3 tail. #define MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X) \ X(BeginTimeElapsedQuery, MG_Backend::BackendQueryHandle, ()) \ X(QueryCounterTimestamp, MG_Backend::BackendQueryHandle, ()) \ X(IsQueryResultAvailable, Bool, (MG_Backend::BackendQueryHandle)) \ X(GetQueryResult64, Bool, (MG_Backend::BackendQueryHandle, Bool, Uint64*)) \ X(BeginOcclusionQuery, MG_Backend::BackendQueryHandle, ()) \ X(BeginXfbPrimitivesQuery, MG_Backend::BackendQueryHandle, (Bool)) \ X(GetGpuTimestampNs, Int64, ()) // The union, for the places that want every class-C row at once (the definitions and // the assignments). A package never edits THIS; it edits its own list above. #define MGR_UNMIGRATED_GL_SLOTS(X) \ MGR_UNMIGRATED_D1_SLOTS(X) \ MGR_UNMIGRATED_I1_SLOTS(X) \ MGR_UNMIGRATED_T2_SLOTS(X) \ MGR_UNMIGRATED_F1_SLOTS(X) \ MGR_UNMIGRATED_TAIL_SLOTS(X) #define MGR_UNMIGRATED_GL_VALUE_SLOTS(X) MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X) #define MGR_DEFINE_UNMIGRATED(Name, Ret, Sig) \ Ret Name##_Unmigrated Sig { UnmigratedVerbFatal(#Name); } MGR_UNMIGRATED_GL_SLOTS(MGR_DEFINE_UNMIGRATED) MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED) #undef MGR_DEFINE_UNMIGRATED // THE TWO COMPUTE SLOTS' class-C stubs are GONE (P5b i1): they carried b1's dispatch // hook before their Fatal so that the package flipping them would inherit a call site // that was already correct, and EmitDispatchCompute / EmitDispatchComputeIndirect above // are that inheritance - same two calls, same order, the record where the Fatal was. void SetSwapInterval_Unmigrated(Int) { UnmigratedVerbFatal("SetSwapInterval"); } // The counts, as arithmetic. MGR_COUNT_ONE expands to `+ 1` per row. #define MGR_COUNT_ONE(Name, Ret, Sig) +1 constexpr Uint32 kUnmigratedD1 = 0 MGR_UNMIGRATED_D1_SLOTS(MGR_COUNT_ONE); // i1 landed: the list is empty and the two hand-written compute stubs are gone with it. constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE); constexpr Uint32 kUnmigratedT2 = 0 MGR_UNMIGRATED_T2_SLOTS(MGR_COUNT_ONE); constexpr Uint32 kUnmigratedF1 = 0 MGR_UNMIGRATED_F1_SLOTS(MGR_COUNT_ONE); // + SetSwapInterval, written out by hand. constexpr Uint32 kUnmigratedTail = 0 MGR_UNMIGRATED_TAIL_SLOTS(MGR_COUNT_ONE) MGR_UNMIGRATED_TAIL_VALUE_SLOTS(MGR_COUNT_ONE) + 1; #undef MGR_COUNT_ONE constexpr Uint32 kUnmigratedSlots = kUnmigratedD1 + kUnmigratedI1 + kUnmigratedT2 + kUnmigratedF1 + kUnmigratedTail; // The emitted counts, PER OWNER. P5's five are c1's; each P5b package raises its own. constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present constexpr Uint32 kEmittedSlotsD1 = 19; constexpr Uint32 kEmittedSlotsI1 = 7; constexpr Uint32 kEmittedSlotsT2 = 6; constexpr Uint32 kEmittedSlotsF1 = 11; constexpr Uint32 kEmittedSlotsTail = 1; // BlitNamedFramebuffer constexpr Uint32 kEmittedSlotsSync = 5; constexpr Uint32 kEmittedSlots = kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1 + kEmittedSlotsTail + kEmittedSlotsSync; constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported // EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and // adds one to its emitted count; a package that touches another's list breaks the // other's line, not its own. The four numbers are the census's package tables plus the // unmeasured companions that share a wire row (BRIEF-P5B.md file-ownership table). static_assert(kUnmigratedD1 + kEmittedSlotsD1 == 19, "d1 owns the 19 draw slots"); static_assert(kUnmigratedI1 + kEmittedSlotsI1 == 7, "i1 owns the 7 image/compute/barrier/copy/SSBO slots"); static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots"); static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots"); static_assert(kUnmigratedTail + kEmittedSlotsTail + kEmittedSlotsSync == 20, "the original wave-3 tail owns 20 slots"); static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, "the three classes no longer partition the 71 slots"); MG_Backend::GlobalBackendFunctionsTable BuildRemoteEmitTable() { ArmControlKnobs(); MG_Backend::GlobalBackendFunctionsTable table{}; // ---- class C first, so that a slot forgotten below stays Fatal rather than null. // Order matters for exactly this reason: if class B's assignment were first, a // typo in class C would leave a NULL slot, and a null slot is 91 potential null // calls with no diagnostic. This way the worst a mistake can do is name a verb // that was supposed to be emitted, loudly. #define MGR_ASSIGN_UNMIGRATED(Name, Ret, Sig) table.GL.Name = &Name##_Unmigrated; MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED) MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED) #undef MGR_ASSIGN_UNMIGRATED table.SetSwapInterval = &SetSwapInterval_Unmigrated; table.GL.FenceSync = &EmitFenceSync; table.GL.ClientWaitSync = &EmitClientWaitSync; table.GL.GetSyncStatus = &EmitGetSyncStatus; table.GL.WaitSync = &EmitWaitSync; table.GL.DeleteSync = &EmitDeleteSync; // ---- class A table.GL.GetIntegeri_v = &AnswerGetIntegeri_v; table.GL.IsTimerQuerySupported = &AnswerIsTimerQuerySupported; // NOT A SLOT and not a verb: a Bool member of the table, whose one non-test client // reader is GL_Query.cpp:221. It does NOT ride inside MGPCaps::Dynamic - it is a // member of GLFunctionsTable, which is exactly the thing a split client never // receives - so it is answered from kCapCpuXfbPrimitiveAccounting. table.GL.PrefersCpuXfbPrimitiveAccounting = CapsMirrorInstance().PrefersCpuXfbPrimitiveAccounting(); // ---- class B table.GL.Clear = &EmitClear; table.GL.DrawArrays = &EmitDrawArrays; // ---- P5b d1: the nineteen draw slots, all on draw_vbo (CONTRACT-P5B.md §2 d1) table.GL.DrawElements = &EmitDrawElements; table.GL.DrawElementsBaseVertex = &EmitDrawElementsBaseVertex; table.GL.DrawRangeElements = &EmitDrawRangeElements; table.GL.DrawRangeElementsBaseVertex = &EmitDrawRangeElementsBaseVertex; table.GL.DrawElementsInstanced = &EmitDrawElementsInstanced; table.GL.DrawElementsInstancedBaseVertex = &EmitDrawElementsInstancedBaseVertex; table.GL.DrawElementsInstancedBaseInstance = &EmitDrawElementsInstancedBaseInstance; table.GL.DrawElementsInstancedBaseVertexBaseInstance = &EmitDrawElementsInstancedBaseVertexBaseInstance; table.GL.DrawArraysInstanced = &EmitDrawArraysInstanced; table.GL.DrawArraysInstancedBaseInstance = &EmitDrawArraysInstancedBaseInstance; table.GL.MultiDrawArrays = &EmitMultiDrawArrays; table.GL.MultiDrawElements = &EmitMultiDrawElements; table.GL.MultiDrawElementsBaseVertex = &EmitMultiDrawElementsBaseVertex; table.GL.DrawArraysIndirect = &EmitDrawArraysIndirect; table.GL.DrawElementsIndirect = &EmitDrawElementsIndirect; table.GL.MultiDrawArraysIndirect = &EmitMultiDrawArraysIndirect; table.GL.MultiDrawElementsIndirect = &EmitMultiDrawElementsIndirect; table.GL.MultiDrawArraysIndirectCount = &EmitMultiDrawArraysIndirectCount; table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; table.GL.ReadPixels = &EmitReadPixels; table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer; table.Present = &EmitPresent; // ---- class B, P5b t2. Assigned AFTER the class-C block above, which is what makes // the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot // whose row is removed from MGR_UNMIGRATED_T2_SLOTS but not assigned here would be // NULL and caught by RemoteEmitTable.NoSlotIsNull rather than silently skipped. table.GL.BeginTransformFeedback = &EmitBeginTransformFeedback; table.GL.EndTransformFeedback = &EmitEndTransformFeedback; table.GL.PauseTransformFeedback = &EmitPauseTransformFeedback; table.GL.ResumeTransformFeedback = &EmitResumeTransformFeedback; table.GL.BindTransformFeedback = &EmitBindTransformFeedback; table.GL.PatchParameteri = &EmitPatchParameteri; // ---- f1 ---- table.GL.ClearBufferfi = &EmitClearBufferfi; table.GL.ClearBufferfv = &EmitClearBufferfv; table.GL.ClearBufferiv = &EmitClearBufferiv; table.GL.ClearBufferuiv = &EmitClearBufferuiv; table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; table.GL.CopyTexImage2D = &EmitCopyTexImage2D; table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; table.GL.GenerateMipmap = &EmitGenerateMipmap; // ---- class B, P5b package i1 (kEmittedSlotsI1 = 7) table.GL.BindImageTexture = &EmitBindImageTexture; table.GL.DispatchCompute = &EmitDispatchCompute; table.GL.DispatchComputeIndirect = &EmitDispatchComputeIndirect; table.GL.MemoryBarrier = &EmitMemoryBarrier; table.GL.MemoryBarrierByRegion = &EmitMemoryBarrierByRegion; table.GL.CopyImageSubData = &EmitCopyImageSubData; table.GL.ShaderStorageBlockBinding = &EmitShaderStorageBlockBinding; return table; } } // namespace const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() { // Leaked at exit like every other MG_Remote singleton (ID-8): MG_Backend::Init() // copies it into gBackendFunctionsTable and MobileGL::Destroy() clears that copy from // an exit handler, by which point a static destructor here would already have run. static const MG_Backend::GlobalBackendFunctionsTable& table = *new MG_Backend::GlobalBackendFunctionsTable{BuildRemoteEmitTable()}; return table; } Uint32 ImplementedVerbCount() { return kEmittedSlots; } Uint32 LocallyAnsweredSlotCount() { return kLocallyAnsweredSlots; } Uint32 UnmigratedSlotCount() { return kUnmigratedSlots; } void SetDropClearEmissionForNegativeControl(Bool drop) { g_dropClearEmission = drop; } Uint64 DroppedClearEmissions() { return g_droppedClearEmissions; } void SetDropDrawEmissionForNegativeControl(Bool drop) { g_dropDrawEmission = drop; } Uint64 DroppedDrawEmissions() { return g_droppedDrawEmissions; } Bool ReadbackPackStateIsTightForTest(GLsizei width, Uint64 bytesPerPixel, const PixelStoreParameters& pack) { return ReadbackPackStateIsTight(width, bytesPerPixel, pack); } Uint64 TightReadbackByteCount(GLsizei width, GLsizei height, GLenum format, GLenum type) { return TightReadbackBytes(width, height, format, type); } // M2 / codex 11's predicate at namespace scope: the one RequireReadbackReplyComplete decides // on and the one the control drives. 0=OK / 1=DECLINED / 2=ERROR. Bool ReadbackReplyIsComplete(Int32 status, Uint64 replySize, Uint64 expected) { return status == Wire::ReplySink::kStatusOk && replySize == expected; } // ID-49's scatter. Exported for the same reason as the refusal above: the control drives // THIS, which is what the emitter calls, rather than a second copy of 8.4.4's arithmetic. void ScatterTightReadbackIntoPackState(const void* tight, void* destination, GLsizei width, GLsizei height, Uint64 bytesPerPixel, const PixelStoreParameters& pack) { if (tight == nullptr || destination == nullptr || width <= 0 || height <= 0) return; const Uint64 rowPixels = pack.RowLength > 0 ? static_cast(pack.RowLength) : static_cast(width); const Uint64 alignment = pack.Alignment > 0 ? static_cast(pack.Alignment) : 1ull; const Uint64 strideBytes = ((rowPixels * bytesPerPixel + alignment - 1) / alignment) * alignment; // m6: the client re-derives GL 4.6 8.4.4's pack layout, so it honours GL_PACK_ROW_LENGTH // VERBATIM - including the ill-formed 0 < ROW_LENGTH < width, where the row stride is // narrower than a written row and consecutive rows overlap. GL leaves that case to the // implementation; the client reproduces exactly what the monolith backend's own scatter // would do with the same state rather than clamping, so the two arms stay byte-identical. // The fast path (ReadbackPackStateIsTight) already rejects any ROW_LENGTH != width, so // this only runs on the scatter path the application asked for. const Uint64 writtenPerRow = static_cast(width) * bytesPerPixel; // SKIP_IMAGES and IMAGE_HEIGHT ARE IGNORED (codex 6). glReadPixels is a 2-D read; GL // does not apply the image-level pack parameters to it, and the monolith conversion path // says so explicitly with honorPackImageParams=false (DirectGLES.cpp:10905). Applying // SKIP_IMAGES here shifted a read with SKIP_IMAGES=1 by a whole image and overran an // application buffer sized for exactly `height` rows. Only SKIP_ROWS and SKIP_PIXELS - // the 2-D skips - offset the first written byte. auto* out = static_cast(destination) + static_cast(pack.SkipRows) * strideBytes + static_cast(pack.SkipPixels) * bytesPerPixel; const auto* in = static_cast(tight); for (Uint64 row = 0; row < static_cast(height); ++row) { std::memcpy(out + row * strideBytes, in + row * writtenPerRow, static_cast(writtenPerRow)); } } // ============================================================================= // P5b d1 - the draw family's record plan, at namespace scope so the unit cases drive // exactly what the emitters above call (R-16). // ============================================================================= Uint8 RemoteIndexSizeFor(GLenum indexType) { switch (indexType) { case GL_UNSIGNED_BYTE: return 1; case GL_UNSIGNED_SHORT: return 2; case GL_UNSIGNED_INT: return 4; default: return 0; } } MG_Pipe::MGPDrawInfo PlanDrawInfo(GLenum mode, Uint8 indexSize, GLsizei instanceCount, GLuint baseInstance, Uint32 numDraws, const RemoteDrawBindings& bindings) { MG_Pipe::MGPDrawInfo info{}; info.Mode = static_cast(mode); info.IndexSize = indexSize; // The restart state rides verbatim (informational in P5b: the backend's // ScopedRestartIndexSubstitution reads its own barrier-pulled copy and the index bytes // 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; // 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; info.StartInstance = baseInstance; info.RestartIndex = bindings.RestartIndex; info.DrawIdOffset = 0; // The handle beside the call (rule D): the VAO's element buffer for an indexed draw, // the same handle set_index_buffer (32) carried at validate; null for arrays and for a // client index array. info.IndexResource = (indexSize != 0 && bindings.ElementBufferBound) ? bindings.ElementBuffer : MG_Pipe::kMGPipeNullHandle; info.MinIndex = ~0u; // "unknown" (MGPipeTypes.h); the DrawRangeElements* callers fill it info.MaxIndex = ~0u; info.XfbCpuCapturedVertices = 0; info.NumDraws = numDraws; return info; } Bool PlanDrawRange(const RemoteDrawBindings& bindings, Uint8 indexSize, const void* indicesOrFirst, GLsizei count, GLint baseVertex, MG_Pipe::MGPDrawRange& out) { out = MG_Pipe::MGPDrawRange{}; out.Count = count > 0 ? static_cast(count) : 0u; if (indexSize == 0) { // Arrays: `first`, spelled through the pointer parameter so one function serves // both shapes. A negative first has already been refused by the frontend. const auto first = static_cast(reinterpret_cast(indicesOrFirst)); out.Start = first > 0 ? static_cast(first) : 0u; out.IndexBias = 0; return true; } out.IndexBias = baseVertex; if (!bindings.ElementBufferBound) { // A client index array: the emitter stages the bytes and the run starts at its // first index. out.Start = 0; return true; } // An element buffer: `indices` is a byte offset into it, and Start is that offset in // INDICES - the same arithmetic OnDrawVbo inverts (offset = Start * IndexSize). An // offset that is not a whole number of indices cannot be spelled and is the caller's // refusal, not a rounding. const auto offset = reinterpret_cast(indicesOrFirst); if (offset % indexSize != 0) return false; const std::uintptr_t start = offset / indexSize; if (start > 0xFFFFFFFFull) return false; out.Start = static_cast(start); return true; } MG_Pipe::MGPDrawIndirect PlanDrawIndirect(const RemoteDrawBindings& bindings, const void* indirect, GLsizei drawCount, GLsizei stride, GLintptr parameterOffset, Bool hasParameterBuffer) { MG_Pipe::MGPDrawIndirect block{}; block.Buffer = bindings.DrawIndirectBuffer; block.ParameterBuffer = hasParameterBuffer ? bindings.ParameterBuffer : MG_Pipe::kMGPipeNullHandle; // The command byte offset the call passed as `indirect` (a bound GL_DRAW_INDIRECT_BUFFER // makes it an offset, never an address - the emitter refused the other case by name). block.Offset = static_cast(reinterpret_cast(indirect)); block.ParameterOffset = hasParameterBuffer ? static_cast(parameterOffset) : 0u; // 0 = tightly packed, as GL spells it; the backend normalises. Negative counts and // strides have already been refused by the frontend. block.Stride = stride > 0 ? static_cast(stride) : 0u; block.DrawCount = drawCount > 0 ? static_cast(drawCount) : 0u; return block; } } // namespace MobileGL::MG_Remote::Client