From 734287ac56e4b0f9f5fec74f167d64418671aee0 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 12:09:55 -0400 Subject: [PATCH 1/3] [Feat] (MG_Remote, P5b): emit i1 seven image/compute/barrier/copy-image/storage-block slots as class B and give each ServerVerbSink body the backend call the contract names --- MobileGL/MG_Remote/Client/EmitTables.cpp | 302 +++++++++++++++++++--- MobileGL/MG_Remote/Server/PipeApplier.cpp | 135 +++++++++- MobileGL/MG_Remote/Server/PipeApplier.h | 15 ++ 3 files changed, 404 insertions(+), 48 deletions(-) diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 6af52e8e..1fdf4425 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -10,8 +10,8 @@ // // 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 5 slots emitted -// class C 64 slots Fatal{UnmigratedVerb, ""} +// class B 5 slots emitted (+ 7 flipped by P5b package i1, below) +// class C 64 slots Fatal{UnmigratedVerb, ""} (- the same 7) // 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. @@ -34,6 +34,13 @@ #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 #include #include @@ -428,6 +435,231 @@ namespace MobileGL::MG_Remote::Client { session.PumpControlPlane(); } + // ============================================================================= + // 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 A - answered locally from the caps mirror (R-15). NO RECORD, EVER. // ============================================================================= @@ -467,7 +699,8 @@ namespace MobileGL::MG_Remote::Client { } // ============================================================================= - // CLASS C - Fatal{UnmigratedVerb}. 64 slots: 63 in GLFunctionsTable + SetSwapInterval. + // CLASS C - Fatal{UnmigratedVerb}. 57 slots: 56 in GLFunctionsTable + SetSwapInterval + // (64 at the P5b contract commit, less the seven package i1 flipped to class B). // ============================================================================= // // PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md, @@ -518,17 +751,12 @@ namespace MobileGL::MG_Remote::Client { X(DrawElementsIndirect, void, (GLenum, GLenum, const void*)) \ X(DrawArraysIndirect, void, (GLenum, const void*)) - // DispatchCompute and DispatchComputeIndirect are i1's too; they are hand-written below - // because they carry b1's dispatch hook before the Fatal. -#define MGR_UNMIGRATED_I1_SLOTS(X) \ - X(BindImageTexture, void, (GLuint, GLuint, GLint, GLboolean, GLint, GLenum, GLenum)) \ - X(CopyImageSubData, void, \ - (const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, \ - const MG_Backend::CopyImageEndpoint&, GLenum, GLint, GLint, GLint, GLint, GLsizei, GLsizei, \ - GLsizei)) \ - X(MemoryBarrier, void, (GLbitfield)) \ - X(MemoryBarrierByRegion, void, (GLbitfield)) \ - X(ShaderStorageBlockBinding, void, (GLuint, const GLchar*, GLuint)) + // 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) #define MGR_UNMIGRATED_T2_SLOTS(X) \ X(PatchParameteri, void, (GLenum, GLint)) \ @@ -607,31 +835,18 @@ namespace MobileGL::MG_Remote::Client { MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_DEFINE_UNMIGRATED) #undef MGR_DEFINE_UNMIGRATED - // THE TWO COMPUTE SLOTS CARRY b1's DISPATCH HOOK BEFORE THE FATAL, and this is stated - // rather than hidden. MarkGpuWritesForDispatch() belongs immediately before the - // dispatch record, and the dispatch record is class C until i1 lands - so the call site - // is here, in the right place, and is UNREACHABLE-IN-EFFECT: the abort follows it. The - // package that moves DispatchCompute into class B (i1: launch_grid, opcode 60) replaces - // the Fatal and inherits a call site that is already correct rather than discovering - // that the mark walk was never wired. - void DispatchCompute_Unmigrated(GLuint, GLuint, GLuint) { - PushPersistentMapsBeforeVerb(); - MarkGpuWritesForDispatch(); - UnmigratedVerbFatal("DispatchCompute"); - } - void DispatchComputeIndirect_Unmigrated(GLintptr) { - PushPersistentMapsBeforeVerb(); - MarkGpuWritesForDispatch(); - UnmigratedVerbFatal("DispatchComputeIndirect"); - } + // 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); - // + DispatchCompute, DispatchComputeIndirect, written out by hand. - constexpr Uint32 kUnmigratedI1 = 0 MGR_UNMIGRATED_I1_SLOTS(MGR_COUNT_ONE) + 2; + // 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. @@ -644,7 +859,9 @@ namespace MobileGL::MG_Remote::Client { // 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 = 0; - constexpr Uint32 kEmittedSlotsI1 = 0; + // P5b i1: BindImageTexture, DispatchCompute, DispatchComputeIndirect, MemoryBarrier, + // MemoryBarrierByRegion, CopyImageSubData, ShaderStorageBlockBinding. + constexpr Uint32 kEmittedSlotsI1 = 7; constexpr Uint32 kEmittedSlotsT2 = 0; constexpr Uint32 kEmittedSlotsF1 = 0; constexpr Uint32 kEmittedSlots = @@ -660,7 +877,11 @@ namespace MobileGL::MG_Remote::Client { 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 == 20, "the wave-3 tail is 20 slots and no P5b package owns one"); - static_assert(kUnmigratedSlots == 64, "CONTRACT-P5.md §7 class C is 64 slots at the P5b contract commit"); + // 64 at the P5b contract commit, MINUS the seven i1 flipped. Each landing package lowers + // this line by its own kEmittedSlots*; the invariant that never moves is the partition + // below, which stays 71 whoever lands next. + static_assert(kUnmigratedSlots == 64 - kEmittedSlotsI1, + "class C is 64 slots at the P5b contract commit less the seven i1 flipped"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, "the three classes no longer partition the 71 slots"); @@ -677,8 +898,6 @@ namespace MobileGL::MG_Remote::Client { MGR_UNMIGRATED_GL_SLOTS(MGR_ASSIGN_UNMIGRATED) MGR_UNMIGRATED_GL_VALUE_SLOTS(MGR_ASSIGN_UNMIGRATED) #undef MGR_ASSIGN_UNMIGRATED - table.GL.DispatchCompute = &DispatchCompute_Unmigrated; - table.GL.DispatchComputeIndirect = &DispatchComputeIndirect_Unmigrated; table.SetSwapInterval = &SetSwapInterval_Unmigrated; // ---- class A @@ -698,6 +917,15 @@ namespace MobileGL::MG_Remote::Client { table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.Present = &EmitPresent; + // ---- 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; } diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index cae264b6..320a4149 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -355,31 +355,144 @@ namespace MobileGL::MG_Remote::Server { // in the same words), the call, and a tally the lane can assert moved. // ----------------------------------------------------------------------------------- - // ---- i1 ---- + // ---- i1 ---- (MG_Remote/CONTRACT-P5B.md §2 i1; landed by package p5b/i1) + // + // RULE D IN FIVE BODIES. Each reproduces the backend call the monolith makes, from the + // record and from server state, and NOTHING ELSE: the backend goes on reading the frontend + // fields it reads today through the BARRIER-PULLED entries of its verb class, which the + // verb stamp PipeApplier::ApplyOne put up before this sink ran is exactly what makes legal. + // That is why none of these touches a backend file and why the monolith path is byte + // identical - and it is also the honest statement of the debt, which `rsp` counts. + Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) { - (void)grid; - ServerUnmigratedVerbFatal(grid.IsIndirect ? "DispatchComputeIndirect" : "DispatchCompute"); + const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid"); + if (table == nullptr) return false; + const MG_Backend::GLFunctionsTable& gl = table->GL; + // The compute program is NOT named by this record and must not be: it is + // GetProgramForDispatch, GetProgramForDraw's twin, which the backend pulls inside its + // own PrepareForCompute (DirectGLES.cpp:5779). i1 is what puts compute on the path, so + // the field moves FATAL -> BARRIER_PULLED in FieldOwnership.def (contract §6.9, the + // one row this package is granted). Block* are 0 on the wire for the same reason: the + // local size is a link artifact the backend reads from its own program. + if (grid.IsIndirect != 0) { + if (gl.DispatchComputeIndirect == nullptr) return false; + // IndirectBuffer travels for P7's sake; the BINDING is server state, put there by + // the set_buffer_bindings record that preceded this one, exactly as OnClear's Fbo + // is not re-resolved here. glDispatchComputeIndirect takes only the offset. + gl.DispatchComputeIndirect(static_cast(grid.IndirectOffset)); + } else { + if (gl.DispatchCompute == nullptr) return false; + gl.DispatchCompute(static_cast(grid.GridX), static_cast(grid.GridY), + static_cast(grid.GridZ)); + } + ++m_dispatches; + return true; } Bool ServerVerbSink::OnMemoryBarrier(const MG_Pipe::MGPMemoryBarrier& barrier) { - ServerUnmigratedVerbFatal(barrier.ByRegion ? "MemoryBarrierByRegion" : "MemoryBarrier"); + const MG_Backend::GlobalBackendFunctionsTable* table = Table("memory_barrier"); + if (table == nullptr) return false; + const MG_Backend::GLFunctionsTable& gl = table->GL; + // THE BITS GO OVER VERBATIM AND ARE LOWERED HERE BY NOBODY. Espryt's atomic-counter + // lowering - the counter bit implying the storage bit, because glslang lowers every + // atomic_uint onto a storage block - lives inside its own MemoryBarrier + // (DirectGLES.cpp:8837) and is a statement about the DRIVER. Repeating it on this side + // would make the split arm and the monolith arm two different calls. + if (barrier.ByRegion != 0) { + if (gl.MemoryBarrierByRegion == nullptr) return false; + gl.MemoryBarrierByRegion(static_cast(barrier.Bits)); + } else { + if (gl.MemoryBarrier == nullptr) return false; + gl.MemoryBarrier(static_cast(barrier.Bits)); + } + ++m_memoryBarriers; + return true; } Bool ServerVerbSink::OnResourceCopyRegion(const MG_Pipe::MGPCopyRegion& copy) { - (void)copy; - ServerUnmigratedVerbFatal("CopyImageSubData"); + const MG_Backend::GlobalBackendFunctionsTable* table = Table("resource_copy_region"); + if (table == nullptr) return false; + if (table->GL.CopyImageSubData == nullptr) return false; + + // REFUSED BY NAME ON BOTH SIDES OF ONE WIRE. The client refuses a renderbuffer endpoint + // before it emits (ID-57's shape, EmitTables.cpp), and this is the same refusal for a + // record that reached here anyway: no sticky forward hands out a RenderbufferObject, so + // there is no honest way to build the endpoint, and guessing an empty one would copy + // nothing and say it copied. + if (copy.SrcTarget == GL_RENDERBUFFER || copy.DstTarget == GL_RENDERBUFFER) { + ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER"); + } + + // THE TWO ENDPOINTS ARE REBUILT FROM THE GL NAMES, through the BARRIER-PULLED sticky + // forward GetTextureObject(name) - `rsp` counts every one of these and P7 is what + // retires them by making the backend take the handles that travel beside the names. + MG_Backend::CopyImageEndpoint src{}; + MG_Backend::CopyImageEndpoint dst{}; + src.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast(copy.SrcGlName)); + dst.Texture = MG_Pipe::gPipeInputs.GetTextureObject(static_cast(copy.DstGlName)); + if (!src.Exists() || !dst.Exists()) { + // The monolith's own answer to this, in its own words (DirectGLES.cpp:9067 + // "source or destination image failed to sync; declining the copy"): the frontend + // validator is what keeps it unreachable and what reports the INVALID_VALUE the + // application is owed. A decline here is a real answer, not a silent success. + MGLOG_E_ONCE("MG_Remote server: resource_copy_region named texture(s) %u -> %u that " + "the frontend no longer holds; declining the copy", + static_cast(copy.SrcGlName), + static_cast(copy.DstGlName)); + return false; + } + + // SrcBox is {origin, extent} and the extent is the copy's, spelled once by GL for both + // endpoints; the destination contributes only its origin. + table->GL.CopyImageSubData(src, static_cast(copy.SrcTarget), + static_cast(copy.SrcLevel), copy.SrcBox.X, copy.SrcBox.Y, + copy.SrcBox.Z, dst, static_cast(copy.DstTarget), + static_cast(copy.DstLevel), copy.DstX, copy.DstY, + copy.DstZ, static_cast(copy.SrcBox.W), + static_cast(copy.SrcBox.H), + static_cast(copy.SrcBox.D)); + ++m_imageCopies; + return true; } Bool ServerVerbSink::OnBindShaderImage(const MG_Pipe::MGPImageBind& bind) { - (void)bind; - ServerUnmigratedVerbFatal("BindImageTexture"); + const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_shader_image"); + if (table == nullptr) return false; + if (table->GL.BindImageTexture == nullptr) return false; + // THE SAME CALL IS RIGHT FOR BOTH BACKENDS, which is why the record carries the whole + // argument list although neither reads all of it today: Espryt ignores everything but + // Unit and syncs that unit from the barrier-pulled GetImageTextureBinding + // (DirectGLES.cpp:9154, :2471), and Magma's slot is a no-op (DirectVulkan.cpp:665). The + // arguments travel because rule D says a verb crosses as the CALL, and because P7 is + // what makes the backend read them instead of pulling. + table->GL.BindImageTexture(static_cast(bind.Unit), static_cast(bind.GlName), + static_cast(bind.Level), + bind.Layered != 0 ? GL_TRUE : GL_FALSE, + static_cast(bind.Layer), + static_cast(bind.Access), + static_cast(bind.Format)); + ++m_imageBinds; + return true; } Bool ServerVerbSink::OnSetStorageBlockBinding(const MG_Pipe::MGPStorageBlockBinding& binding, const char* name) { - (void)binding; - (void)name; - ServerUnmigratedVerbFatal("ShaderStorageBlockBinding"); + const MG_Backend::GlobalBackendFunctionsTable* table = Table("set_storage_block_binding"); + if (table == nullptr) return false; + if (table->GL.ShaderStorageBlockBinding == nullptr) return false; + if (name == nullptr) return false; + // The NAME is the one coordinate the application, the frontend and both backends agree + // on (BackendObject.h:216-221), which is why the row carries a blob rather than the + // application's block INDEX. `name` points into the decoder's bounded local and is + // valid for this call only (rule C); the backend slot copies what it needs. + // + // Both backends resolve the PROGRAM through the barrier-pulled GetProgramObject(GlName) + // / TryGetDirectVulkanProgram - `rsp` again, retired by P9. ShaderCso travels beside the + // name for the phase that dispatches on it. + table->GL.ShaderStorageBlockBinding(static_cast(binding.GlName), name, + static_cast(binding.Binding)); + ++m_storageBlockBindings; + return true; } // ---- t2 ---- diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 43b09c1b..3b7af121 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -153,6 +153,16 @@ namespace MobileGL::MG_Remote::Server { Uint64 Presents() const { return m_presents; } Uint64 LastPresentSerial() const { return m_lastPresentSerial; } Uint64 ReadbackBytes() const { return m_readbackBytes; } + // ---- P5b package i1's tallies. R-16: a probe may not arm against a stub, and on a + // split build "the scenario passed" is also what a scenario that never left the + // monolith path looks like - so the lane asserts the number that only this sink can + // move. One per wire ROW, not per GL slot: launch_grid carries both dispatch entry + // points and memory_barrier both barrier ones, and the sink is where they separate. + Uint64 ImageBinds() const { return m_imageBinds; } + Uint64 Dispatches() const { return m_dispatches; } + Uint64 MemoryBarriers() const { return m_memoryBarriers; } + Uint64 ImageCopies() const { return m_imageCopies; } + Uint64 StorageBlockBindings() const { return m_storageBlockBindings; } // ID-49's tight-size control reads this: the scratch a read_pixels grew to. It must equal // the tight w*h*bpp extent of the read, never the client's DstSize - a scratch sized from // DstSize is exactly the heap overflow codex 1 found, one field over. @@ -169,6 +179,11 @@ namespace MobileGL::MG_Remote::Server { Uint64 m_presents = 0; Uint64 m_lastPresentSerial = 0; Uint64 m_readbackBytes = 0; + Uint64 m_imageBinds = 0; + Uint64 m_dispatches = 0; + Uint64 m_memoryBarriers = 0; + Uint64 m_imageCopies = 0; + Uint64 m_storageBlockBindings = 0; // ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable:: // ReadPixels writes into a caller buffer, so one staging vector per session sits // between them. Grown, never shrunk, and never handed out past the call. From b82c63a9ec0704a4e6f153cbb8d2f18c5b146417 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 12:10:01 -0400 Subject: [PATCH 2/3] [Feat] (MG_Pipe, DirectGLES, P5b): move GetProgramForDispatch FATAL to BARRIER_PULLED now that compute crosses, and skip the copy-image shadow mirror under a non-monolith transport --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 21 +++++++++++++++++++ MobileGL/MG_Pipe/FieldOwnership.def | 14 ++++++++++--- .../MG_Pipe/generated/PipeFieldOwnership.inc | 8 +++---- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5225c987..101adccd 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -8988,6 +8988,27 @@ namespace MobileGL::MG_Backend::DirectGLES { auto* srcMipmap = MG_State::GLState::AsMipmapTexture(srcEndpoint.Texture.get()); auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get()); if (!srcMipmap || !dstMipmap) return; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5b package i1's ONE backend edit, and the contract names this site + // (MG_Remote/CONTRACT-P5B.md §2 i1 "the copy-image-shadow-mirror emulation", ruling + // §6.7). Migrating glCopyImageSubData moves the first blocker off the client's + // Fatal{UnmigratedVerb} and onto the Fatal below, on every Espryt copy between two + // textures with CPU shadows - so the ruling is: UNDER A REAL TRANSPORT THE SERVER SKIPS + // THE MIRROR, and the client-side mirror ROADMAP P8 names ("CopyImage 镜像搬到 client") + // stays P8's. + // + // WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed + // to be a skip rather than a port: a later glGetTexImage of the destination served from + // the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9) and a texture + // re-mint that re-uploads the level is Fatal{UnmigratedEmulation, "texture-remint-pull"} + // (Managers.cpp:5634). Neither can silently read the un-mirrored shadow. + // + // BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1), + // and the arm is the TRANSPORT and not the build - build-split runs its unit and + // integration-gpu lanes under MOBILEGL_TRANSPORT=monolith, where this mirror is on an + // ordinary correct path and must still run. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return; +#endif #if MOBILEGL_PIPE_PUSH // P4a (D-M). glCopyImageSubData's CPU-shadow mirror copies the source level's shadow // rows into the DESTINATION's shadow so a later readback of the destination sees what diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def index beeca7c4..9cccb3ec 100644 --- a/MobileGL/MG_Pipe/FieldOwnership.def +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -105,6 +105,14 @@ /* DirectGLES.cpp:4497, PrepareForDraw, the second unconditional pointer read of every draw. */ \ X(GetProgramForDraw, BARRIER_PULLED, "P8 (Espryt), P7 (Magma)", \ "frontend SharedPtr; the record carries a handle") \ + /* P5b package i1 (CONTRACT-P5B.md §6.9, the one row the contract grants a package). This */ \ + /* was FATAL "reachable only from kDispatch; there is no compute on the reduced path" - and */ \ + /* i1 IS what puts compute on the path: launch_grid (60) now crosses and the backend's */ \ + /* PrepareForCompute pulls this inside it (DirectGLES.cpp:5779, VulkanRenderer.cpp:7327). */ \ + /* It is GetProgramForDraw's twin in every respect, so it takes its class and its retiring */ \ + /* phases. Overturned by: nothing in P5b; P7/P8 retire both rows together. */ \ + X(GetProgramForDispatch, BARRIER_PULLED, "P7 (Magma), P8 (Espryt)", \ + "frontend SharedPtr; the record carries a handle") \ /* The XFB six. XFB itself is off the reduced path, but kDraw's may-read mask carries all */ \ /* six and the draw walk reads them regardless - which is exactly the case a field census */ \ /* taken from "what the scenario does" rather than from the mask would miss. */ \ @@ -132,13 +140,13 @@ X(GetPixelStoreParameters, APPLIER_DERIVED, "-", \ "set_pixel_pack_state; the applier writes m_pixelStore[0] (PipeApply.cpp:1373)") \ \ - /* ---- FATAL: three non-sticky fields, each off the reduced path for a checkable reason --- */ \ + /* ---- FATAL: two non-sticky fields, each off the reduced path for a checkable reason ----- */ \ + /* Three until P5b: GetProgramForDispatch moved up to BARRIER_PULLED when package i1 put */ \ + /* compute on the path (CONTRACT-P5B.md §6.9). */ \ X(GetBoundTransformFeedbackName, FATAL, "-", \ "DEAD: read by no backend since the D21 rekey (PipeInputs.h:232-234)") \ X(GetTransformFeedbackPausedPrimitiveCounter, FATAL, "-", \ "reachable only from class kQuery, which the reduced path never enters") \ - X(GetProgramForDispatch, FATAL, "-", \ - "reachable only from kDispatch; there is no compute on the reduced path") \ \ /* ---- the seven sticky forwards, as FIELD rows ---- */ \ /* They have no storage, so a read of the FIELD is a call of the FORWARD; the field row and */ \ diff --git a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc index 8e2e6faa..44cdf75b 100644 --- a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc +++ b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc @@ -69,7 +69,7 @@ inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCou MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetFactor MGPipeFieldOwnership::kRecordSupplied, // GetPolygonOffsetUnits MGPipeFieldOwnership::kRecordSupplied, // GetPrimitiveRestartIndex - MGPipeFieldOwnership::kFatal, // GetProgramForDispatch + MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDispatch MGPipeFieldOwnership::kBarrierPulled, // GetProgramForDraw MGPipeFieldOwnership::kBarrierPulled, // GetProgramObject MGPipeFieldOwnership::kRecordSupplied, // GetProvokingVertexMode @@ -136,7 +136,7 @@ inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] = "-", // GetPolygonOffsetFactor "-", // GetPolygonOffsetUnits "-", // GetPrimitiveRestartIndex - "-", // GetProgramForDispatch + "P7 (Magma), P8 (Espryt)", // GetProgramForDispatch "P8 (Espryt), P7 (Magma)", // GetProgramForDraw "P9", // GetProgramObject "-", // GetProvokingVertexMode @@ -298,6 +298,6 @@ inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3; // The class sizes, as constants a test can pin without recounting the table. inline constexpr SizeT kMGPipeRecordSuppliedFieldCount = 32; inline constexpr SizeT kMGPipeApplierDerivedFieldCount = 1; -inline constexpr SizeT kMGPipeBarrierPulledFieldCount = 27; -inline constexpr SizeT kMGPipeFatalFieldCount = 3; +inline constexpr SizeT kMGPipeBarrierPulledFieldCount = 28; +inline constexpr SizeT kMGPipeFatalFieldCount = 2; static_assert(kMGPipeRecordSuppliedFieldCount + kMGPipeApplierDerivedFieldCount + kMGPipeBarrierPulledFieldCount + kMGPipeFatalFieldCount == kMGPipeInputFieldCount, "the four class sizes do not partition the field set"); From a72a96d85f88b31768b0390ae9588b85eb342784 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 12:10:09 -0400 Subject: [PATCH 3/3] [Test] (MG_Test, MG_IntegrationTest, P5b): pin i1 seven class-B slots and the served compute program, and arm 24 inproc lane entries whose results only exist if the records crossed --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 49 ++++++++ .../Harness/SplitLogPaths.cmake.in | 9 ++ MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp | 44 ++++++- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 109 +++++++++++++++++- 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index af59f21a..9fa27748 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -1935,6 +1935,55 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" ) + # ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) -------------------------------- + # + # THE LANE CASE THAT PROVES THE RECORDS CROSSED, and it is a RESULT and not a probe (R-16: + # a probe may not arm against a stub). Each of these four scenarios asserts a value that + # only exists if the verb ran on the apply thread - the texel a compute shader stored + # through an image unit, the word glCopyImageSubData moved, the counter an atomic add left + # behind, the block a program pipeline rebound - so a sink that DECLINED, or a client that + # fell through to the driver, is red by the number it reads back and not by a tally. + # + # Before i1 every one of these aborted at the client's Fatal{UnmigratedVerb} on the first + # glBindImageTexture / glDispatchCompute / glCopyImageSubData / glShaderStorageBlockBinding; + # all four are green under inproc now (the census: 13 + 6 + 11 + 3 DirectGLES entries). + # RED ONCE BY DOING X: return false from ServerVerbSink::OnLaunchGrid before the + # gl.DispatchCompute call and UnboundImageDescriptor / AtomicCounter / ProgramPipeline all + # read back their initial values. + # + # THE FILTERS ARE NARROWED TO THE CASES THAT ACTUALLY EMIT, and that is ScenarioFixture's + # rule rather than a convenience: an armed `DirectGLES.Split.` case must move the client + # encoder's record ordinal (ScenarioFixture.h:86), because a lane whose workload produced no + # record is the "resolved the transport and then fell through to the driver" shape that + # every pixel assertion is blind to. ProgramPipelineScenario's other nine cases are pure + # name/state cases that draw nothing, so the whole-scenario filter would arm them and they + # would be red for having nothing to say. The two named here are the ones that were + # Fatal{UnmigratedVerb, "ShaderStorageBlockBinding"} on the c0b head. + # + # One block per scenario, following the three above: a `:`-separated multi-pattern + # TEST_FILTER is unproven through gtest_discover_tests' flat PROPERTIES forwarding. + set(MGL_SPLIT_I1_SCENARIOS + UnboundImageDescriptorScenario # bind_shader_image (72) + launch_grid (60), 13 cases + CopyImageLayeredScenario # resource_copy_region (53), 6 cases + AtomicCounterScenario # launch_grid (60) + memory_barrier (61), 3 cases + ProgramPipelineScenario) # set_storage_block_binding (75), the 2 storage-block cases + set(MGL_SPLIT_I1_FILTER_UnboundImageDescriptorScenario "UnboundImageDescriptorScenario.*") + set(MGL_SPLIT_I1_FILTER_CopyImageLayeredScenario "CopyImageLayeredScenario.*") + set(MGL_SPLIT_I1_FILTER_AtomicCounterScenario "AtomicCounterScenario.*") + set(MGL_SPLIT_I1_FILTER_ProgramPipelineScenario "ProgramPipelineScenario.*StorageBlock*") + foreach(mglItestI1Scenario IN LISTS MGL_SPLIT_I1_SCENARIOS) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Split." + TEST_LIST "MGL_SPLIT_I1_${mglItestI1Scenario}_TESTS" + TEST_FILTER "${MGL_SPLIT_I1_FILTER_${mglItestI1Scenario}}" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-gpu\;integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}" + ) + endforeach() + # The split half of the counting pair. MGITEST_PERSISTENT_MAP_ARM=emulated is R-6: under split # the adopt tier is pinned at T2, the resource owner declines every acquisition and the client # pushes the mapping's dirty blocks - so pmap must be non-zero and mpr must be the monolith diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in index 532ba453..5ae4a1ce 100644 --- a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -11,4 +11,13 @@ foreach(scenario @MGL_SPLIT_SMALL_RING_SCENARIOS@) "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") endforeach() endforeach() +# P5b package i1's four scenarios, the same shape as the small-ring loop above: every +# DirectGLES.Split. entry owns exactly one absolute log path and shares it with nobody, which +# is what SplitLogPaths.PrivateAndDistinct checks over the discovered set. +foreach(scenario @MGL_SPLIT_I1_SCENARIOS@) + foreach(entry IN LISTS MGL_SPLIT_I1_${scenario}_TESTS) + set_tests_properties("${entry}" PROPERTIES ENVIRONMENT + "MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log") + endforeach() +endforeach() # PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it. diff --git a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp index 068e9080..039a3b10 100644 --- a/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp +++ b/MobileGL/MG_Test/Pipe/FieldOwnershipTest.cpp @@ -254,13 +254,23 @@ TEST_F(FieldOwnershipTest, TheReducedPathsUnmigratedFieldsAreAllAccountedFor) { EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetPixelStoreParameters, 1u), MGPipeFieldOwnership::kFatal); - // The three off the reduced path, each for its own checkable reason. + // The two off the reduced path, each for its own checkable reason. THREE UNTIL P5b: the + // third was GetProgramForDispatch, FATAL because "there is no compute on the reduced path", + // and package i1 is what put compute on the path (CONTRACT-P5B.md §6.9). It is asserted + // below in its new class rather than deleted from this case, because a field that quietly + // left the FATAL list is exactly what this case exists to catch. EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetBoundTransformFeedbackName), MGPipeFieldOwnership::kFatal); EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter), MGPipeFieldOwnership::kFatal); + // P5b i1: launch_grid (60) crosses, the backend's PrepareForCompute pulls the compute + // program inside it (DirectGLES.cpp:5779), and the field takes GetProgramForDraw's class + // and its retiring phases - so it is a measured DEBT now, not a defect. EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch), - MGPipeFieldOwnership::kFatal); + MGPipeFieldOwnership::kBarrierPulled); + EXPECT_EQ(MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDispatch), + MGPipeFieldOwnershipOf(MGPipeInputField::GetProgramForDraw)) + << "GetProgramForDispatch is GetProgramForDraw's twin and must share its class"; } TEST_F(FieldOwnershipTest, TheSevenStickyForwardsAgreeWithTheirFieldRows) { @@ -516,17 +526,43 @@ TEST_F(FieldOwnershipTest, StrictErrorsAlsoPromotesTheStickyForwards) { // A FATAL-class read aborts whatever the knob says: no carrier, and the reduced path never // reads it, so it is a real defect rather than a debt. +// P5b i1: the exemplar MOVED. This case used GetProgramForDispatch, which is BARRIER-PULLED +// from i1 on (a debt the server serves, not an abort), so it would now assert that a served +// read aborts - green for the wrong reason at best. GetTransformFeedbackPausedPrimitiveCounter +// is the same statement with a field that is still FATAL: reachable only from class kQuery, +// which the reduced path never enters. +// RED ONCE BY DOING X: put GetProgramForDispatch back in the FATAL block of FieldOwnership.def +// and TheFieldOwnershipTableIsTheContractsTableRow's new kBarrierPulled expectation goes red by +// name; swap the field below for GetProgramForDispatch and THIS case goes red instead, because +// a barrier-pulled read under a stamp does not abort. TEST_F(FieldOwnershipTest, AFatalClassReadAbortsEvenWithoutStrictErrors) { const ChildResult r = RunInChild([] { MGPipeServerStampVerbBoundary(MGPipeVerb::DrawArrays); - (void)gPipeInputs.GetProgramForDispatch(); + (void)gPipeInputs.GetTransformFeedbackPausedPrimitiveCounter(); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch@DrawArrays\"}"), + EXPECT_NE(r.Log.find("Fatal{UnmigratedPipeInput, " + "\"GetTransformFeedbackPausedPrimitiveCounter@DrawArrays\"}"), std::string::npos) << r.Log; } +// And the field that LEFT the FATAL class is served rather than fatal, under the verb that made +// it reachable. This is i1's half of the §6.9 grant made checkable: a dispatch stamp plus a read +// of the compute program must NOT abort, which is precisely the statement "compute is on the +// path now". RED ONCE BY DOING X: revert the FieldOwnership.def row to FATAL and this child +// aborts with Fatal{UnmigratedPipeInput, "GetProgramForDispatch@DispatchCompute"}. +TEST_F(FieldOwnershipTest, TheComputeProgramIsServedUnderADispatchStampFromP5bOn) { + const ChildResult r = RunInChild([] { + MGPipeServerStampVerbBoundary(MGPipeVerb::DispatchCompute); + (void)gPipeInputs.GetProgramForDispatch(); + MGPipeServerClearVerbBoundary(); + }); + EXPECT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_EQ(r.Log.find("Fatal{UnmigratedPipeInput, \"GetProgramForDispatch"), std::string::npos) + << r.Log; +} + // The argument-keyed row: the pack half is answerable and the unpack half is not, and the // difference is the accessor's own argument rather than a second field id. Splitting the field // into two ids would have made the unpack half BARRIER-PULLED - silently served - which is diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index acef2348..238807fe 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -171,12 +171,21 @@ namespace { // ===================================================================================== TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { - // CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal. Read from the functions the - // table itself reports with - which is also what t1's arming condition reads - rather than - // recomputed here, so a table that lost an emitter cannot look like one that never had it. + // CONTRACT-P5.md §7: 2 answered locally + 5 emitted + 64 Fatal at the P5b contract commit. + // Read from the functions the table itself reports with - which is also what t1's arming + // condition reads - rather than recomputed here, so a table that lost an emitter cannot look + // like one that never had it. + // + // P5b package i1 (CONTRACT-P5B.md §2 i1) flipped SEVEN slots C -> B: BindImageTexture, + // DispatchCompute, DispatchComputeIndirect, MemoryBarrier, MemoryBarrierByRegion, + // CopyImageSubData, ShaderStorageBlockBinding. So the two numbers that move are 5 -> 12 and + // 64 -> 57, and the SUM below is the invariant that does not move whichever package lands + // next. RED ONCE BY DOING X: comment out `table.GL.MemoryBarrier = &EmitMemoryBarrier;` in + // BuildRemoteEmitTable and this case stays green while NoSlotIsNull goes red - which is why + // the per-package count is asserted here and the null walk is a separate case. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 5u); - EXPECT_EQ(UnmigratedSlotCount(), 64u); + EXPECT_EQ(ImplementedVerbCount(), 12u); + EXPECT_EQ(UnmigratedSlotCount(), 57u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); } @@ -257,6 +266,49 @@ TEST(RemoteEmitTable, SetSwapIntervalIsClassCAndSaysSo) { EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log; } +#endif // MGTEST_HAVE_FORK + +// ---- P5b package i1 (MG_Remote/CONTRACT-P5B.md §2 i1) ------------------------------------- + +TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) { + // The census's five measured slots plus the two companions that share their rows. Named + // rather than counted, so a table that flipped a DIFFERENT seven is red here and not only + // in the arithmetic. The comparison is against a slot that is still class C: a flipped slot + // and an unflipped one cannot be the same pointer, which is what a forgotten class-B + // assignment would look like (class C is assigned FIRST in BuildRemoteEmitTable precisely so + // that the mistake is loud rather than null). + const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable(); + const void* fatal = reinterpret_cast(table.GL.GetTexImage); // wave-3 tail, class C + ASSERT_NE(fatal, nullptr); + const void* const i1[] = { + reinterpret_cast(table.GL.BindImageTexture), + reinterpret_cast(table.GL.DispatchCompute), + reinterpret_cast(table.GL.DispatchComputeIndirect), + reinterpret_cast(table.GL.MemoryBarrier), + reinterpret_cast(table.GL.MemoryBarrierByRegion), + reinterpret_cast(table.GL.CopyImageSubData), + reinterpret_cast(table.GL.ShaderStorageBlockBinding), + }; + static const char* const kNames[] = {"BindImageTexture", "DispatchCompute", + "DispatchComputeIndirect", "MemoryBarrier", + "MemoryBarrierByRegion", "CopyImageSubData", + "ShaderStorageBlockBinding"}; + for (SizeT i = 0; i < sizeof(i1) / sizeof(i1[0]); ++i) { + EXPECT_NE(i1[i], nullptr) << kNames[i] << " is null"; + EXPECT_NE(i1[i], fatal) << kNames[i] + << " still points at an UnmigratedVerbFatal thunk; i1 flipped it " + "to class B"; + } + // The two barrier slots and the two dispatch slots share a WIRE ROW but not an emitter: the + // discriminant (ByRegion / IsIndirect) is set by the emitter, so one thunk for both would + // carry the wrong one. + EXPECT_NE(i1[3], i1[4]) << "MemoryBarrier and MemoryBarrierByRegion share memory_barrier (61) " + "but must set opposite ByRegion values"; + EXPECT_NE(i1[1], i1[2]) << "DispatchCompute and DispatchComputeIndirect share launch_grid (60) " + "but must set opposite IsIndirect values"; +} + +#if MGTEST_HAVE_FORK TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) { // The other half of "no slot may fall through to the driver". With no ClientSession the // emitter has nowhere to put the record, and the one thing it may not do is return quietly: @@ -265,6 +317,53 @@ TEST(RemoteEmitTable, AClassBSlotWithNoSessionAbortsRatherThanFallingThrough) { ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"Clear\"}"), std::string::npos) << r.Log; } + +TEST(RemoteEmitTable, EachI1SlotReachesRequireSessionUnderItsOwnName) { + // The behavioural half: an i1 slot is class B, so with no ClientSession it reaches + // RequireSession and aborts Fatal{NoClientSession, ""} - NOT Fatal{UnmigratedVerb} + // (which would mean the flip never happened) and NOT quietly (which is the split lane + // running monolith and going green, R-4). The name in the message is the slot's own, which + // is the half a single case could not state. + // + // RED ONCE BY DOING X: put `X(MemoryBarrier, void, (GLbitfield))` back in + // MGR_UNMIGRATED_I1_SLOTS and drop `table.GL.MemoryBarrier = &EmitMemoryBarrier;` - the + // MemoryBarrier arm below then finds Fatal{UnmigratedVerb, "MemoryBarrier"} instead. + const ChildResult barrier = RunInChild([] { RemoteEmitTable().GL.MemoryBarrier(0x2000); }); + ASSERT_TRUE(DiedOfAbort(barrier)) << DescribeStatus(barrier) << "\n" << barrier.Log; + EXPECT_NE(barrier.Log.find("Fatal{NoClientSession, \"MemoryBarrier\"}"), std::string::npos) + << barrier.Log; + EXPECT_EQ(barrier.Log.find("Fatal{UnmigratedVerb"), std::string::npos) + << "MemoryBarrier is class B from P5b i1 on:\n" + << barrier.Log; + + const ChildResult dispatch = RunInChild([] { RemoteEmitTable().GL.DispatchCompute(1, 1, 1); }); + ASSERT_TRUE(DiedOfAbort(dispatch)) << DescribeStatus(dispatch) << "\n" << dispatch.Log; + EXPECT_NE(dispatch.Log.find("Fatal{NoClientSession, \"DispatchCompute\"}"), std::string::npos) + << dispatch.Log; + + const ChildResult bind = RunInChild( + [] { RemoteEmitTable().GL.BindImageTexture(0, 1, 0, GL_FALSE, 0, 0x88BA, 0x8058); }); + ASSERT_TRUE(DiedOfAbort(bind)) << DescribeStatus(bind) << "\n" << bind.Log; + EXPECT_NE(bind.Log.find("Fatal{NoClientSession, \"BindImageTexture\"}"), std::string::npos) + << bind.Log; + + const ChildResult ssbo = RunInChild( + [] { RemoteEmitTable().GL.ShaderStorageBlockBinding(1, "Blk", 2); }); + ASSERT_TRUE(DiedOfAbort(ssbo)) << DescribeStatus(ssbo) << "\n" << ssbo.Log; + EXPECT_NE(ssbo.Log.find("Fatal{NoClientSession, \"ShaderStorageBlockBinding\"}"), + std::string::npos) + << ssbo.Log; + + const ChildResult copy = RunInChild([] { + const MG_Backend::CopyImageEndpoint src{}; + const MG_Backend::CopyImageEndpoint dst{}; + RemoteEmitTable().GL.CopyImageSubData(src, 0x0DE1, 0, 0, 0, 0, dst, 0x0DE1, 0, 0, 0, 0, 1, + 1, 1); + }); + ASSERT_TRUE(DiedOfAbort(copy)) << DescribeStatus(copy) << "\n" << copy.Log; + EXPECT_NE(copy.Log.find("Fatal{NoClientSession, \"CopyImageSubData\"}"), std::string::npos) + << copy.Log; +} #endif // MGTEST_HAVE_FORK // =====================================================================================