diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d589a76f..8d429390 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -356,6 +356,42 @@ namespace MobileGL::MG_Backend::DirectGLES { } const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer resolves + // from the handle the verb record carried, and its CPU bytes are the SERVER's staged + // shadow (rule E) - the client's GL_DRAW_INDIRECT_BUFFER binding slot, the frontend + // object's MappedData()/GetSize() (B3) and the client slot allocator (T2) are never + // read. Coverage is asserted: commands read from bytes no record staged would be + // zero-filled, which is a missing record, not a valid command stream. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = BufferImpl::FindBufferResourceForHandle(handle); + const Uint8* const base = resource ? resource->hostBytes : nullptr; + const SizeT commandOffset = reinterpret_cast(indirect); + if (base == nullptr) { + MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged " + "shadow on this side (GPU-written indirect commands are P8's)", + label, handle.Slot, handle.Gen); + return nullptr; + } + if (commandOffset + requiredBytes > BufferImpl::ResourceWidthForHandle(handle)) { + MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label); + return nullptr; + } + BufferImpl::RequireStagedCoverage(*resource, base, commandOffset, + commandOffset + requiredBytes, "indirect_command_bytes"); + return base + commandOffset; } + // No buffer was bound: the frontend already raised the GL error; keep the old + // fall-through shape (a null indirect pointer is the same skip it was). + if (!indirect) { + MGLOG_E_ONCE("%s skipped: indirect pointer is null", label); + return nullptr; + } + MGLOG_E_ONCE("%s skipped: the verb carried no indirect buffer handle", label); + return nullptr; + } +#endif auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); if (drawBuffer) { drawBuffer->SyncPersistentMappedRange(); @@ -901,10 +937,26 @@ namespace MobileGL::MG_Backend::DirectGLES { // Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES // context since indirect draws now execute natively on the GPU. if (includeIndirectBuffer) { - auto& possibleIndirectBuffer = - MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - if (possibleIndirectBuffer) { - SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb + // record's handle; the frontend binding slot and the client allocator are + // never read (T2). The twin's ensure is what SyncBoundBuffer's did. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = EnsureBufferResourceForHandle(nullptr, handle); + if (resource && resource->id != 0) { + BindBufferId(GL_DRAW_INDIRECT_BUFFER, resource->id); + } + } + } else +#endif + { + auto& possibleIndirectBuffer = + MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + if (possibleIndirectBuffer) { + SyncBoundBuffer(BufferTarget::DrawIndirect, GL_DRAW_INDIRECT_BUFFER); + } } } @@ -931,6 +983,19 @@ namespace MobileGL::MG_Backend::DirectGLES { SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER); MarkShaderStorageBuffersGpuWritten(); if (includeDispatchIndirectBuffer) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb + // record's handle, never the frontend binding slot or the client allocator. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer; + if (!MG_Pipe::MGPipeHandleIsNull(handle)) { + auto* resource = EnsureBufferResourceForHandle(nullptr, handle); + if (resource && resource->id != 0) { + BindBufferId(GL_DISPATCH_INDIRECT_BUFFER, resource->id); + } + } + } else +#endif SyncBoundBuffer(BufferTarget::DispatchIndirect, GL_DISPATCH_INDIRECT_BUFFER); } } @@ -2846,6 +2911,14 @@ namespace MobileGL::MG_Backend::DirectGLES { !bound || bound == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO; if ((record.IsDefault != 0) != boundIsDefault) return false; if (boundIsDefault) return true; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the object-handle half of the identity check is a client-allocator + // probe (T2). Under an active transport the record is the only statement of + // identity (rule E) and the check does not run - the record was resolved from the + // applier's OWN bound handle, so it is this binding's by construction (ID-19). + // Monolith keeps the corroboration verbatim. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return true; +#endif return record.Fbo == g_backendFramebufferObjects.HandleOf(bound.get()); } @@ -3035,6 +3108,16 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!backendObj) { backendObj = MakeShared(); } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): with an active transport the twin's sync keys its applier-record + // lookup on the record's own handle (m_pushedSyncHandle), never on the client + // allocator (T2), and the table remembers which frontend object the twin is + // synced from so a later handle-only resolution (the named blit) can reach it. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + backendObj->m_pushedSyncHandle = record.Fbo; + g_backendFramebufferObjects.NoteStateForHandle(record.Fbo, currentFBO); + } +#endif // THE "SAME FBO AS DRAW" SKIP IS NOW A FIELD, not a pointer comparison against // the object the previous iteration happened to sync. Target = Both is the @@ -4440,12 +4523,80 @@ namespace MobileGL::MG_Backend::DirectGLES { backendObj->Bind(target); } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.2/§3.3): the handle-keyed form of SyncAndBindFramebufferObject. + // With an active transport a framebuffer reaches the server as the handle a record + // carried - the named blit's ReadFbo/DrawFbo, or the applier's own bound handle for a + // binding restore - and the client's binding slots and slot allocator are never probed + // (T2/T3). The twin is resolved (or minted) BY HANDLE; its sync is keyed on THIS handle's + // applier record through m_pushedSyncHandle; and the frontend object the sync body still + // walks comes from the table's own state note (the object-class channel P3b/P4b retires), + // never from a client binding slot. A twin with no noted object was never synced through + // any binding - the DSA-on-a-never-bound-framebuffer case, which the split path already + // refuses for the named clears - and is loud rather than silently unconfigured. + void SyncAndBindFramebufferByHandle(MG_Pipe::MGPipeHandle fbo, FramebufferTarget target, + Bool forceSync = false) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + if (MG_Pipe::MGPipeHandleIsNull(fbo) || fbo == MG_Pipe::kMGPipeDefaultFramebuffer) { + // Same reset as the object form's default branch, for its reason. + if (target == FramebufferTarget::Draw) { + FramebufferImpl::g_alphaWidenedDrawBufferMask = 0; + FramebufferImpl::g_integerColorDrawBufferMask = 0; + } + FramebufferImpl::BindFramebufferId( + target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0); + return; + } + + auto& registry = FramebufferImpl::g_backendFramebufferObjects; + auto* twinSlot = registry.GetOrCreateByHandle(fbo); + if (twinSlot == nullptr) { + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} is refused by the twin slot table " + "(live generation %u); nothing is bound for the %s target", + fbo.Slot, fbo.Gen, registry.LiveGenAt(fbo.Slot), + target == FramebufferTarget::Read ? "READ" : "DRAW"); + return; + } + auto& backendObj = *twinSlot; + if (!backendObj) { + backendObj = MakeShared(); + } + backendObj->m_pushedSyncHandle = fbo; + const auto stateObj = registry.StateForHandle(fbo); + if (stateObj) { + if (forceSync) { + backendObj->InvalidateSyncedState(); + } + backendObj->SyncToBackend(stateObj, target); + } else { + MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no frontend object noted on " + "this side - it was never synced through a binding, so its twin is " + "bound unconfigured", + fbo.Slot, fbo.Gen); + } + backendObj->Bind(target); + } +#endif // MOBILEGL_BUILD_DISAGGREGATED + void ForceBindCurrentFBO(FramebufferTarget target) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif auto& slot = GetFramebufferBindingSlotChecked(target); const auto& fbo = slot.GetBoundObject(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): with an active transport the bound framebuffer's handle is the applier's + // own working state; the object form's registry probe never runs (T2). + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && FramebufferSubsystemEnabled()) { + SyncAndBindFramebufferByHandle( + MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast( + target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read + : MG_Pipe::MGPipeFramebufferTarget::Draw)], + target); + } else +#endif SyncAndBindFramebufferObject(fbo, target); FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion(); FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0; @@ -5650,14 +5801,36 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!restart.DrawIsValid()) return; type = restart.IndexType(); indexSize = MG_Util::GetGLTypeSize(type); - const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): under an active transport the command buffer is the + // handle the verb record carried (MGPipeApplier's verb stash) and the SSBO view's + // resource resolves from it by handle - the frontend binding slot and the client slot + // allocator are never read (T2). + const MG_Pipe::MGPipeHandle verbBufferHandle = + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? MG_Pipe::MGPipeApplier().VerbIndirectBuffer + : MG_Pipe::kMGPipeNullHandle; +#endif + const Bool useNative = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws() + : +#endif + drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); if (useNative) { // gl_BaseInstance must observe GPU-written command fields; expose the indirect // buffer to the program's mg_IndirectParams SSBO view and address it per draw. const auto backendProgram = GetCurrentBackendProgram(); const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; if (paramsBinding >= 0) { - auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer); + auto* resource = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle) + : +#endif + BufferImpl::EnsureBufferResource(drawIndirectBuffer); if (resource && resource->id != 0) { BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), resource->id); @@ -5722,12 +5895,32 @@ namespace MobileGL::MG_Backend::DirectGLES { // draw is what leaves a stale value, and restoring afterwards would only protect the // NEXT draw while these commands ran with the stale one. SetCurrentBaseVertex(0); - const Bool useNative = drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): see ExecuteIndexedIndirectCommands - the verb's handle, + // never the frontend binding slot or the client allocator. + const MG_Pipe::MGPipeHandle verbBufferHandle = + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? MG_Pipe::MGPipeApplier().VerbIndirectBuffer + : MG_Pipe::kMGPipeNullHandle; +#endif + const Bool useNative = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? !MG_Pipe::MGPipeHandleIsNull(verbBufferHandle) && SupportsNativeIndirectDraws() + : +#endif + drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws(); if (useNative) { const auto backendProgram = GetCurrentBackendProgram(); const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1; if (paramsBinding >= 0) { - auto* resource = BufferImpl::EnsureBufferResource(drawIndirectBuffer); + auto* resource = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? BufferImpl::EnsureBufferResourceForHandle(nullptr, verbBufferHandle) + : +#endif + BufferImpl::EnsureBufferResource(drawIndirectBuffer); if (resource && resource->id != 0) { BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(paramsBinding), resource->id); @@ -6394,6 +6587,14 @@ namespace MobileGL::MG_Backend::DirectGLES { } const auto& drawIndirectBuffer = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? SharedPtr(nullptr) + : +#endif MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, drawcount, stride, "MultiDrawElementsIndirect"); @@ -6425,6 +6626,53 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport both buffers resolve from the + // verb record's handles (MGPDrawIndirect::Buffer / ParameterBuffer) and the count + // reads from the SERVER's staged shadow - the frontend binding slots, the frontend + // accessors (B3) and the client allocator (T2) are never read. Monolith takes the + // original body below, verbatim. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto& applierState = MG_Pipe::MGPipeApplier(); + const SizeT commandOffset = reinterpret_cast(indirect); + const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + + sizeof(DrawElementsIndirectCommand); + auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); + auto* paramResource = + BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); + const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr; + const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr; + if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); + return; + } + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > + BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); + return; + } + // No staged shadow means no count to read, not a wrong one - the monolith body's + // own rule for a buffer with no CPU shadow. + if (parameterBytes == nullptr || drawBytes == nullptr) { + MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, + "multidraw_elements_indirect_count_commands"); + BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), + static_cast(drawcount) + sizeof(Uint32), + "multidraw_elements_indirect_count_parameter"); + Uint32 actualDrawCount = 0; + std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); + actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); + ExecuteIndexedIndirectCommands(mode, type, indexSize, drawBytes + commandOffset, commandOffset, + nullptr, static_cast(actualDrawCount), stride, + "MultiDrawElementsIndirectCount"); + return; + } +#endif + auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); if (!drawBuffer) { @@ -6496,6 +6744,14 @@ namespace MobileGL::MG_Backend::DirectGLES { } const auto& drawIndirectBuffer = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? SharedPtr(nullptr) + : +#endif MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, drawcount, stride, "MultiDrawArraysIndirect"); @@ -6527,6 +6783,48 @@ namespace MobileGL::MG_Backend::DirectGLES { DrawSyncFlags syncBit = DrawSyncBit::IndirectBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): see the indexed twin - the verb record's handles and + // the SERVER's staged shadow, never the frontend bindings or the client allocator. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto& applierState = MG_Pipe::MGPipeApplier(); + const SizeT commandOffset = reinterpret_cast(indirect); + const SizeT commandBytes = commandOffset + static_cast(stride) * static_cast(maxdrawcount - 1) + + sizeof(DrawArraysIndirectCommand); + auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer); + auto* paramResource = + BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer); + const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr; + const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr; + if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range"); + return; + } + if (drawcount < 0 || static_cast(drawcount) + sizeof(Uint32) > + BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectParameterBuffer)) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_PARAMETER_BUFFER binding or range"); + return; + } + if (parameterBytes == nullptr || drawBytes == nullptr) { + MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: CPU fallback cannot read the parameter or " + "draw-indirect buffer"); + return; + } + BufferImpl::RequireStagedCoverage(*drawResource, drawBytes, commandOffset, commandBytes, + "multidraw_arrays_indirect_count_commands"); + BufferImpl::RequireStagedCoverage(*paramResource, parameterBytes, static_cast(drawcount), + static_cast(drawcount) + sizeof(Uint32), + "multidraw_arrays_indirect_count_parameter"); + Uint32 actualDrawCount = 0; + std::memcpy(&actualDrawCount, parameterBytes + drawcount, sizeof(actualDrawCount)); + actualDrawCount = std::min(actualDrawCount, static_cast(maxdrawcount)); + ExecuteArraysIndirectCommands(mode, drawBytes + commandOffset, commandOffset, nullptr, + static_cast(actualDrawCount), stride, + "MultiDrawArraysIndirectCount"); + return; + } +#endif + auto drawBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); auto parameterBuffer = MGB_CTX->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); if (!drawBuffer) { @@ -6700,6 +6998,14 @@ namespace MobileGL::MG_Backend::DirectGLES { } const auto& drawIndirectBuffer = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? SharedPtr(nullptr) + : +#endif MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteIndexedIndirectCommands(mode, type, indexSize, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, 1, sizeof(DrawElementsIndirectCommand), @@ -6741,6 +7047,14 @@ namespace MobileGL::MG_Backend::DirectGLES { } const auto& drawIndirectBuffer = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport the frontend binding slot + // is never read - the Execute* helpers resolve the buffer from the verb record's + // handle (T2). Monolith reads the slot as it always did. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? SharedPtr(nullptr) + : +#endif MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); ExecuteArraysIndirectCommands(mode, commandBytes, reinterpret_cast(indirect), drawIndirectBuffer, 1, sizeof(DrawArraysIndirectCommand), "DrawArraysIndirect"); @@ -7757,6 +8071,63 @@ namespace MobileGL::MG_Backend::DirectGLES { DebugImpl::OpenGLScopeMarker marker(__func__); #endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.3): THE NAMED ARM. A blit record whose ReadFbo/DrawFbo are + // non-null is a glBlitNamedFramebuffer: both framebuffers resolve from the record's + // handles through the FBO twin table (the client's ScopedBlitBindings staging and this + // function's binding-slot read at the bottom are gone from the split path), and the + // sink is told the pair was consumed - a backend that reaches here without consuming + // it has no named arm and the verb declines there. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + auto& applierState = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle readFbo = applierState.VerbBlitReadFbo; + const MG_Pipe::MGPipeHandle drawFbo = applierState.VerbBlitDrawFbo; + if (!MG_Pipe::MGPipeHandleIsNull(readFbo) || !MG_Pipe::MGPipeHandleIsNull(drawFbo)) { + applierState.VerbBlitNamedConsumed = true; + TextureImpl::SyncNeccessaryTextures(); + RenderStateImpl::SyncRenderState(); + + SyncAndBindFramebufferByHandle(readFbo, FramebufferTarget::Read, /*forceSync=*/true); + SyncAndBindFramebufferByHandle(drawFbo, FramebufferTarget::Draw, /*forceSync=*/true); + + MGLOG_D("ES BlitNamedFramebuffer({%u,%u} -> {%u,%u}, %d, %d, %d, %d, %d, %d, %d, %d, 0x%x, %s)", + readFbo.Slot, readFbo.Gen, drawFbo.Slot, drawFbo.Gen, srcX0, srcY0, srcX1, + srcY1, dstX0, dstY0, dstX1, dstY1, mask, + MG_Util::ConvertGLEnumToString(filter).c_str()); + // See the bound arm below: only the probed defect makes this do anything. It + // reads the two frontend objects' attachments (the object-class channel P3b/P4b + // retires), reached through the twins' state notes - and for a default endpoint + // the default framebuffer object itself, exactly as the bound arm's binding-slot + // read hands it. A missing object only skips the workaround. + const auto objectFor = [](MG_Pipe::MGPipeHandle handle) { + if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) { + return SharedPtr( + MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO); + } + return FramebufferImpl::g_backendFramebufferObjects.StateForHandle(handle); + }; + const auto readObj = objectFor(readFbo); + const auto drawObj = objectFor(drawFbo); + if (readObj && drawObj) { + mask &= ~BlitLayeredDestinationAspects(readObj, drawObj, srcX0, srcY0, srcX1, srcY1, + dstX0, dstY0, dstX1, dstY1, mask); + } + if (mask != 0) { + IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, + mask, filter); + } + // The driver's bindings are now the two NAMED framebuffers; put the bound ones + // back exactly as the monolith named entry point does. + ForceBindCurrentFBO(FramebufferTarget::Read); + ForceBindCurrentFBO(FramebufferTarget::Draw); + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { + MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); + }); + return; + } + } +#endif + TextureImpl::SyncNeccessaryTextures(); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -7853,6 +8224,29 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.4): under an active transport the destination is the handle + // the copy_framebuffer_to_texture record carried; the unit binding slot and the client + // allocator are never read (T2/T4). + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(dstHandle); + if (backendTextureSlot == nullptr) { + MGLOG_E_ONCE("%s: the verb's destination texture handle {%u, %u} is refused by " + "the twin slot table (live generation %u)", + __func__, dstHandle.Slot, dstHandle.Gen, + TextureImpl::g_backendTextureObjects.LiveGenAt(dstHandle.Slot)); + return false; + } + auto& backendObj = *backendTextureSlot; + if (!backendObj) { + backendObj = MakeShared(); + } + backendObj->Bind(TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget), unit); + return true; + } +#endif + const auto& bindingSlot = textureUnit.GetBindingSlot(textureTarget); { const auto& textureObject = bindingSlot.GetBoundObject(); @@ -8083,8 +8477,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // The frontend has already defined the generated chain before emitting this verb. // Its resource_respecify carries that shape, so the server only verifies the // descriptor; it must not allocate or dirty the client's level shadows again. - // Identity resolution is the same existing registry lookup used by texture sync. - const auto handle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get()); + // P5c (hd): identity is the handle the generate_mipmap record carried + // (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2). + const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes; const auto* record = PipeTextureRecordForHandle(handle); if (record == nullptr || record->Desc.Width == 0 || record->Desc.Levels == 0) { MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); @@ -8558,18 +8953,42 @@ namespace MobileGL::MG_Backend::DirectGLES { // Bind necessary FBO and texture BindCurrentFBO(FramebufferTarget::Read); Uint activeTextureUnit = MGB_CTX->GetActiveTextureUnit(); - const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit) - .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) - .GetBoundObject(); - auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); - if (!backendTextureSlot || !*backendTextureSlot) { - MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", - textureObject ? textureObject->GetExternalIndex() : 0); - return; + TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; + TextureInternalFormat mgInternalFormat{}; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.4): with an active transport the destination resolves from + // the handle the copy_framebuffer_to_texture record carried (MGPCopyFromFramebuffer:: + // Dst, the verb stash) and its format is the applier descriptor's - the client's + // texture-unit binding slot and the client slot allocator are never read (T2/T4). + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + const auto* dstRecord = PipeTextureRecordForHandle(dstHandle); + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle); + if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr || dstRecord == nullptr) { + MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin " + "or no applier record on this side", + dstHandle.Slot, dstHandle.Gen); + return; + } + dstBackendTexture = backendTextureSlot->get(); + mgInternalFormat = static_cast(dstRecord->Desc.InternalFormat); + } else +#endif + { + const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit) + .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) + .GetBoundObject(); + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); + if (!backendTextureSlot || !*backendTextureSlot) { + MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", + textureObject ? textureObject->GetExternalIndex() : 0); + return; + } + dstBackendTexture = backendTextureSlot->get(); + mgInternalFormat = textureObject->GetFormat(); } - (*backendTextureSlot)->Bind(target, activeTextureUnit); + dstBackendTexture->Bind(target, activeTextureUnit); - auto mgInternalFormat = textureObject->GetFormat(); GLenum format = GL_DEPTH_COMPONENT; GLenum type = GL_UNSIGNED_INT; TextureImpl::GenerateTextureFormatInfo(mgInternalFormat, &internalformat, &format, &type, @@ -8603,7 +9022,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); - auto currentTex = (GLint)(*backendTextureSlot)->GetBackendTextureId(); + auto currentTex = (GLint)dstBackendTexture->GetBackendTextureId(); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); @@ -8653,16 +9072,35 @@ namespace MobileGL::MG_Backend::DirectGLES { // Bind necessary FBO and texture BindCurrentFBO(FramebufferTarget::Read); auto activeTextureUnit = MGB_CTX->GetActiveTextureUnit(); - const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit) - .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) - .GetBoundObject(); - auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); - if (!backendTextureSlot || !*backendTextureSlot) { - MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", - textureObject ? textureObject->GetExternalIndex() : 0); - return; + TextureImpl::BackendTextureObject* dstBackendTexture = nullptr; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.4): see CopyTexImage2D - the record's Dst handle, never the + // client's unit binding slot or the client allocator (T2/T4). + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle); + if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr) { + MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no " + "twin on this side", + dstHandle.Slot, dstHandle.Gen); + return; + } + dstBackendTexture = backendTextureSlot->get(); + } else +#endif + { + const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit) + .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)) + .GetBoundObject(); + auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); + if (!backendTextureSlot || !*backendTextureSlot) { + MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.", + textureObject ? textureObject->GetExternalIndex() : 0); + return; + } + dstBackendTexture = backendTextureSlot->get(); } - (*backendTextureSlot)->Bind(target, activeTextureUnit); + dstBackendTexture->Bind(target, activeTextureUnit); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); @@ -8684,7 +9122,7 @@ namespace MobileGL::MG_Backend::DirectGLES { }); } else { MGLOG_D("%s: Backend depth", __func__); - auto currentTex = (*backendTextureSlot)->GetBackendTextureId(); + auto currentTex = dstBackendTexture->GetBackendTextureId(); DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__](auto err) { MGLOG_D("ES error (%s:%d): %s", file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 4dcc85e9..ecb72285 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2702,6 +2702,13 @@ namespace MobileGL::MG_Backend::DirectGLES { return twin ? twin->get() : nullptr; } +#if MOBILEGL_BUILD_DISAGGREGATED + void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, + SizeT end, const char* site) { + ServerStaged().RequireCoverage(&resource, hostBase, start, end, site); + } +#endif + MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) { if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle; // Through the table's own single-entry front memo rather than straight into @@ -3100,7 +3107,17 @@ namespace MobileGL::MG_Backend::DirectGLES { // D-N keeps this call here for P3a. It can queue ranges and move the record, so // everything below is read AFTER it. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.8): under an active transport the frontend accessor is a + // layer-1 surface ("buffer-legacy-arm") and the client's own pre-verb + // persistent-map push is the only producer; the call is skipped outright. Under + // monolith D-N's placement stands. + if (bufferObject && MG_Config::Transport == MG_Config::TransportMode::Monolith) { + bufferObject->SyncPersistentMappedRange(); + } +#else if (bufferObject) bufferObject->SyncPersistentMappedRange(); +#endif const auto* record = ResourceRecordOf(res); const SizeT size = record != nullptr ? static_cast(record->Desc.Width) : 0; @@ -3128,24 +3145,28 @@ namespace MobileGL::MG_Backend::DirectGLES { // and its bytes are read through persistentPtr. if (hostBase != nullptr) resource->hostBytes = hostBase; // "DOES THE SHADOW THIS PATH IS ABOUT TO UPLOAD HOLD MEANINGFUL BYTES?" - and it has - // to be asked of the SAME thing the bytes come from, which is why it is not - // record->Desc.HasDefinedContent. The descriptor states what was true at the last - // resource_respecify and NOTHING refreshes it afterwards: resource_subdata, - // resource_flush_range, buffer_subdata_resident and OnGpuWritten all define content - // (BufferObject.cpp:85, :101, :127, :365, :441) without re-emitting a descriptor, - // and re-emitting one per glBufferSubData would be new wire traffic D-J forbids. So - // after the ordinary glBufferData(NULL) + glBufferSubData idiom the descriptor still - // says "undefined", this full re-upload passed nullptr, RespecifyStorageWith - // CLEARED the queued ranges and stamped syncedChangeSerial - i.e. an empty store - // declared current, with the application's bytes dropped and nothing saying so. - // The legacy arm pairs bufferObject.MappedData() with bufferObject.HasDefinedContent() - // (RespecifyStorageNow) and this arm pairs the same two, so the source of the bytes - // and the statement about them can never disagree. Declared, like C-1's IsMapped(): - // it is a frontend read this function keeps for P3a and it retires the moment the - // client publishes a live content flag beside the descriptor (P5/P8). With no - // frontend object (the handle-only drains) the descriptor is all there is. + // to be asked of the SAME thing the bytes come from. The legacy arm pairs + // bufferObject.MappedData() with bufferObject.HasDefinedContent() (RespecifyStorageNow) + // and this arm pairs the same two, so the source of the bytes and the statement about + // them can never disagree. + // + // P5c (hd, CONTRACT-P5C §3.6 / B1): with an active transport the answer is the + // DESCRIPTOR's bit - which the client publishes on ResourceCreate/Respecify - OR the + // staged coverage the content records behind it delivered. The coverage half is what + // keeps the answer equal to the frontend flag's in the one case the descriptor alone + // cannot see: an ORPHANING respecify (descriptor clear) followed by glBufferSubData, + // which defines content without a new descriptor - the bytes crossed as + // resource_subdata and the staged store's coverage is exactly "content defined since + // the last orphan". With no coverage and a clear bit the store is undefined by the + // application's own declaration and the upload stays a pure NULL reallocation. const Bool shadowHasContent = - bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0); +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? (record->Desc.HasDefinedContent != 0 || + ServerStaged().CoveredRunCount(resource) != 0) + : +#endif + (bufferObject ? bufferObject->HasDefinedContent() : (record->Desc.HasDefinedContent != 0)); const void* initialData = shadowHasContent ? hostBase : nullptr; // M-3 / codex 4: a RespecifyStorageWith(..., initialData != nullptr) is a WHOLE-STORE // [0, size) upload from the base, so it owes the same coverage the pending-range drain @@ -9143,7 +9164,16 @@ namespace MobileGL::MG_Backend::DirectGLES { surface.Res.Slot, surface.Res.Gen); return false; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the corroborating cross-check against the frontend object's handle + // is a client-allocator probe (T2). Under an active transport the record is the + // ONLY statement of identity (rule E) and the check does not run; monolith + // keeps it verbatim. The record's own handle was already validated above (N-3). + if (MG_Config::Transport == MG_Config::TransportMode::Monolith && + !(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) { +#else if (!(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) { +#endif MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} but the frontend " "attachment's texture %u is handle {%u, %u} - refusing rather than " "attaching one of them", @@ -9358,7 +9388,15 @@ namespace MobileGL::MG_Backend::DirectGLES { // FramebufferAttachmentType::None, i.e. GL_NONE. A surface that matches no colour // point cannot be a legal read buffer and is refused rather than guessed. if (FramebufferSubsystemEnabled()) { - const MG_Pipe::MGPipeHandle fbo = g_backendFramebufferObjects.HandleOf(stateFBOObject.get()); + // P5c (hd): with an active transport the handle is the caller's + // (m_pushedSyncHandle), never the client allocator's - see SyncToBackend. + const MG_Pipe::MGPipeHandle fbo = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? m_pushedSyncHandle + : +#endif + g_backendFramebufferObjects.HandleOf(stateFBOObject.get()); // ID-19: the OBJECT's record, not "the record of whatever is bound to READ". A // framebuffer whose read buffer is being pushed need not be the read binding at // all - glNamedFramebufferReadBuffer and the DSA clears reach here by name - and @@ -9586,7 +9624,18 @@ namespace MobileGL::MG_Backend::DirectGLES { // there is no framebuffer create or destroy in the catalogue and none is invented // - so the handle exists purely to key set_framebuffer_state, which is exactly // what it is used for here. - const MG_Pipe::MGPipeHandle fbo = g_backendFramebufferObjects.HandleOf(stateFBOObject.get()); + // + // P5c (hd): with an active transport the handle is the one the caller is applying + // (m_pushedSyncHandle - the record-driven sync set it), and the client allocator + // is never probed (T2). A null there means a caller reached this arm without a + // record, which the null-record refusal below names. + const MG_Pipe::MGPipeHandle fbo = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? m_pushedSyncHandle + : +#endif + g_backendFramebufferObjects.HandleOf(stateFBOObject.get()); pushedRecord = PushedFramebufferRecord(fbo); if (pushedRecord == nullptr) { MGLOG_E_ONCE("MGPipe: framebuffer %u has no applier record on the handle arm, so it " diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 38359e4d..2ac11aae 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -467,6 +467,23 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_slotTable.LiveGenAt(slot); } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding + // both the record's handle and the frontend object (the record-driven sync) notes the + // object so a later handle-only resolution can reach it without the client allocator. + void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { + if (EsprytSlotTablesEnabled()) { + m_slotTable.NoteStateForHandle(handle, stateObj); + } + } + StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { + if (EsprytSlotTablesEnabled()) { + return m_slotTable.StateForHandle(handle); + } + return nullptr; + } +#endif + // NO ReleaseByHandle HERE, AND THAT IS A DECISION (review M-4). The death half of // GetOrCreateByHandle exists for a kind whose announcement is its own destroy CALL // rather than the shared death notice - which is the BUFFER family @@ -892,6 +909,14 @@ namespace MobileGL::MG_Backend::DirectGLES { GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res); GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the staged-coverage assertion (StagedShadowStore::RequireCoverage) for a + // read of the server shadow outside the upload ladders - the indirect command-byte + // resolver. A no-op for a base that is not this resource's server shadow. + void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start, + SizeT end, const char* site); +#endif + // MONOLITH GLUE, and named as such: the handle of a resource this backend is looking // at through a frontend object, resolved through the client allocator's lifetime-id // index. Every caller is a site P3a deliberately does NOT migrate - the SSBO / UBO / @@ -1810,6 +1835,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is // bound as both draw and read (otherwise glReadBuffer changes would be silently dropped). void SyncReadBufferToBackend(const SharedPtr& stateFBOObject); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.2): the framebuffer handle the CURRENT sync is keyed on. + // A caller applying a record sets it before SyncToBackend / SyncReadBufferToBackend, + // which then read the applier's record for THAT handle instead of probing the + // client's slot allocator for the frontend object's lifetime id (T2). Read only + // with an active transport; monolith resolves through HandleOf as it always did. + MG_Pipe::MGPipeHandle m_pushedSyncHandle = MG_Pipe::kMGPipeNullHandle; +#endif void InvalidateSyncedState(); Uint GetBackendFramebufferId() const { return m_backendFBOId; } void Bind(FramebufferTarget target) const; diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index a84e6e72..7e606466 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -75,6 +75,12 @@ // belong on the client, and the backend should receive the handle in the verb payload. It is // NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers, // so nothing catches it automatically. +// +// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the +// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise +// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the +// allocator's own three entries carry, repeated here so the refusal names this surface), and +// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead. namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_PIPE_PUSH @@ -228,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // arm StateBackendObjectRegistry::GetOrCreate arms it itself. EnsureProcessTeardownSentinel(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.1): this overload MINTS - it is monolith glue, and with + // an active transport a call from the apply thread is Fatal{RoleViolation, + // "MGPipeSlots"} before the allocator is touched. Split paths call the handle + // overload below. (The check also lives at the allocator's own three entries; it + // is repeated at this entry so the refusal names this surface even if the entry + // set changes.) + MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("GetOrCreate(StatePtr)"); +#endif + const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId()); MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), @@ -334,6 +350,34 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry.Live ? entry.Gen : 0; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): remember the frontend object a HANDLE-keyed twin was synced from. The + // minting overload sets stateRef itself; the handle overload cannot (no object + // crosses), so a caller that legitimately holds the object - the record-driven sync, + // which arrived holding it through the object-class barrier-pulled rows - notes it + // here. It is what lets a later handle-only resolution (P5c's named blit) reach the + // frontend object the twin's sync body still walks, without probing the client's slot + // allocator (T2). Same liveness rules as the minted stateRef: never an identity test, + // never read to decide the slot is dead. + void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= m_slots.size()) return; + Entry& entry = m_slots[handle.Slot]; + if (!entry.Live || entry.Gen != handle.Gen) return; + entry.stateRef = stateObj; + } + + // The frontend object noted for this handle, or null. The handle answers identity; + // this answers only "which object was this twin last synced from". + StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { + if (MG_Pipe::MGPipeHandleIsNull(handle) || handle.Slot >= m_slots.size()) { + return nullptr; + } + const Entry& entry = m_slots[handle.Slot]; + if (!entry.Live || entry.Gen != handle.Gen) return nullptr; + return entry.stateRef.lock(); + } +#endif + // P3a: the death half of the overload above, for a kind whose announcement is its own // destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather // than destroying it in place, because the caller may still have to decide what @@ -391,6 +435,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // allocator probe it always cost; a hit is refreshed the moment anyone acquires. MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): same guard as the minting overload - a lifetime-id probe from the + // apply thread is Fatal{RoleViolation, "MGPipeSlots"} with an active transport. + MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("HandleOf"); +#endif const Uint64 lifetimeId = stateObj->GetLifetimeId(); if (lifetimeId == m_memoLifetimeId) return m_memoHandle; const MG_Pipe::MGPipeHandle handle = @@ -414,6 +463,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // Returns whether the object had a slot of this kind, i.e. whether anything was freed; // a second call for the same id answers false because the allocator no longer maps it. static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the Find/Free pair below is monolith-only with an active transport - + // a frontend death is announced to the server by the object_death record (ct), + // and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}. + MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed"); +#endif const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId); if (MG_Pipe::MGPipeHandleIsNull(handle)) return false; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 7cfe864f..5ad96c6c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,15 @@ namespace MobileGL::MG_Backend::DirectGLES { if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) { return &server->GetFormatCapabilities(); } + // P5c (hd, CONTRACT-P5C §3.7): the mirror fallback is REFUSED with an active + // transport. The server's backend exists whenever the session does, so reaching + // here is a bring-up ordering defect, and a silent read of the client caps mirror + // (pActiveBackendObject is client memory, rule E) would hide it. + MGLOG_F("MGPipe: Fatal{RoleViolation, \"caps-mirror\"} - the format-capability " + "fallback to the client caps mirror was reached with an active transport " + "but no server backend; the server's own backend is the only legal source " + "on the apply thread"); + std::abort(); } return pActiveBackendObject ? &pActiveBackendObject->GetFormatCapabilities() : nullptr; } diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 0d5760b3..03911879 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1,4 +1,4 @@ -// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +// MobileGL - MobileGL/MG_Backend/DirectVulkan/DirectVulkan.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 @@ -17,6 +17,10 @@ #include "MG_Util/Metrics/PipeStats.h" #include "MG_Util/Metrics/TextureMetrics.h" #include "MG_Util/Miscellany/IndexGenerator.h" +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#endif #include #include #include @@ -710,9 +714,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding) { auto* programObject = TryGetDirectVulkanProgram(program); if (!programObject || storageBlockName == nullptr) return; - const Int maxBindings = pActiveBackendObject - ? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings - : 0; + const Int maxBindings = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.7): with an active transport the dynamic parameters are + // the SERVER's own backend's - the client caps mirror (pActiveBackendObject) is + // client memory the apply thread may not name (rule E). Monolith reads the mirror + // as it always did. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? (MG_Remote::Server::ServerLoopInstance().Backend() != nullptr + ? static_cast(MG_Remote::Server::ServerLoopInstance().Backend() + ->GetDynamicParameters() + .MaxShaderStorageBufferBindings) + : 0) + : +#endif + pActiveBackendObject + ? pActiveBackendObject->GetDynamicParameters().MaxShaderStorageBufferBindings + : 0; if (storageBlockBinding >= static_cast(maxBindings)) { MGB_CTX->RecordError( ErrorCode::InvalidValue, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp index 8c498758..7e96cf96 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp @@ -10,6 +10,10 @@ #include "MagmaPipeArms.h" #include "MG_Util/Converters/MGToStr/DataTypeConverter.h" #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#endif #include namespace MobileGL::MG_Backend::DirectVulkan { @@ -246,6 +250,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { // set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the // module, so a float32 stream would be fed to a Float64 input. const Bool narrowFloat64Arrays = +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the + // SERVER's own backend's - the client caps mirror is client memory (rule E). + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr || + !MG_Remote::Server::ServerLoopInstance().Backend() + ->GetDynamicParameters() + .SupportsFloat64VertexAttributes) + : +#endif MG_Backend::pActiveBackendObject == nullptr || !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes; if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) { @@ -499,7 +513,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { // demoted `vec` input - the same thing DirectGLES does for the same state. The // frontend RECORDS the format either way, so this gate is the only thing standing // between a legal glVertexAttribLFormat and a mismatched pipeline. - if (MG_Backend::pActiveBackendObject == nullptr || + if ( +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.7): see the narrowFloat64Arrays site above. + MG_Config::Transport != MG_Config::TransportMode::Monolith + ? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr || + !MG_Remote::Server::ServerLoopInstance().Backend() + ->GetDynamicParameters() + .SupportsFloat64VertexAttributes) + : +#endif + MG_Backend::pActiveBackendObject == nullptr || !MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) { return VK_FORMAT_UNDEFINED; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 7cc2f3e0..2a4f6d63 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -33,6 +33,9 @@ #include "MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.h" #include "MG_Util/Texture/PixelStoreProcessor.h" #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#endif #include #include #include @@ -664,6 +667,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { static void ApplyLineWidthState(VkCommandBuffer commandBuffer) { Float lineWidth = MGB_CTX->GetLineWidth(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.7): with an active transport the dynamic parameters are the + // SERVER's own backend's - the client caps mirror is client memory (rule E). Monolith + // reads the mirror as it always did. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + if (MG_Backend::BackendObject* server = MG_Remote::Server::ServerLoopInstance().Backend()) { + const auto& dynamicParameters = server->GetDynamicParameters(); + const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin; + const Float maxLineWidth = dynamicParameters.AliasedLineWidthRangeMax; + if (lineWidth < minLineWidth) { + lineWidth = minLineWidth; + } else if (lineWidth > maxLineWidth) { + lineWidth = maxLineWidth; + } + } + } else +#endif if (MG_Backend::pActiveBackendObject != nullptr) { const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); const Float minLineWidth = dynamicParameters.AliasedLineWidthRangeMin; diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index c6940523..d2c6f7c5 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -9,7 +9,30 @@ // SlotAllocator.h. Compiled only under MOBILEGL_PIPE_PUSH. #include +#if MOBILEGL_BUILD_DISAGGREGATED +#include +#include +#include + +#include +#endif + namespace MobileGL::MG_Pipe { +#if MOBILEGL_BUILD_DISAGGREGATED + void MGPipeRefuseAllocatorFromApplyThread(const char* entry) { + if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; + if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return; + MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread called " + "MGPipeSlots().%s. With an active transport the client slot allocator is " + "client-only memory (CONTRACT-P5C §3.1, rule E): a handle arrives already " + "minted in a record, and a server that resolves or mints one off a frontend " + "object's lifetime id is reading memory that will not exist on its side of a " + "real split", + entry); + std::abort(); + } +#endif + namespace { // The ShaderCso band the ordinary allocator must never enter: the top 1/16 of the // ShaderCso slot space is reserved for PROGRAM PIPELINE COMPOSITES, which are minted @@ -166,6 +189,9 @@ namespace MobileGL::MG_Pipe { } MGPipeHandle MGPipeSlotAllocator::FindByLifetimeId(MGPipeKind kind, Uint64 lifetimeId) const { +#if MOBILEGL_BUILD_DISAGGREGATED + MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); +#endif if (lifetimeId == 0) return kMGPipeNullHandle; const KindState& state = StateOf(kind); const auto it = state.ByLifetimeId.find(lifetimeId); @@ -176,12 +202,18 @@ namespace MobileGL::MG_Pipe { } MGPipeHandle MGPipeSlotAllocator::Acquire(MGPipeKind kind, Uint64 lifetimeId) { +#if MOBILEGL_BUILD_DISAGGREGATED + MGPipeRefuseAllocatorFromApplyThread("Acquire"); +#endif const MGPipeHandle existing = FindByLifetimeId(kind, lifetimeId); if (!MGPipeHandleIsNull(existing)) return existing; return AllocateFor(kind, lifetimeId); } void MGPipeSlotAllocator::Free(MGPipeKind kind, MGPipeHandle handle) { +#if MOBILEGL_BUILD_DISAGGREGATED + MGPipeRefuseAllocatorFromApplyThread("Free"); +#endif KindState& state = StateOf(kind); SlotState* entry = EntryOf(state, kind, handle.Slot); if (entry == nullptr) return; diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index b2e9f36e..4e2f42fe 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -163,4 +163,14 @@ namespace MobileGL::MG_Pipe { // The monolith's one client allocator. Under split there is one per client context. MGPipeSlotAllocator& MGPipeSlots(); + +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): with an active transport this allocator is a + // CLIENT-only surface. Acquire, FindByLifetimeId and Free called from the apply thread - + // i.e. a server that resolves or mints handles off a frontend object's lifetime id (T2), + // which is memory that will not exist on its side of a real split - are + // Fatal{RoleViolation, "MGPipeSlots"}. Compiled out entirely outside split builds, so the + // pull build's bytes do not move (G1). + void MGPipeRefuseAllocatorFromApplyThread(const char* entry); +#endif } // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index bfd409bf..19990232 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -670,6 +670,50 @@ namespace MobileGL::MG_Pipe { MGPipeHandle BoundShaderCso = kMGPipeNullHandle; Uint64 ProgramBindingSerial = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + // ---- P5c (hd, CONTRACT-P5C §3.2): THE CURRENT VERB'S OWN HANDLES. ---------------- + // + // Server state, written by ServerVerbSink at the top of a verb's dispatch and read by + // the backend DURING THAT SAME VERB, so the apply thread resolves the verb's objects + // from the handles the record carried instead of probing the client's slot allocator + // for a frontend object's lifetime id (T2). It is per-verb WORKING state, not object + // state: every writer overwrites the whole set for its verb (null included), so a + // value is only ever read between its verb's write and that verb's return, and there + // is nothing to clear at a reset. + // + // Blit: both null = the bound-form blit (the bindings answer, as before). A non-null + // pair names the read/draw framebuffers of a NAMED blit (MGPBlit::ReadFbo/DrawFbo); + // the backend must then run its named-blit arm and raise VerbBlitNamedConsumed - the + // sink reads that flag back so a backend with no named arm is a loud decline rather + // than a silent blit of whatever is bound. + MGPipeHandle VerbBlitReadFbo = kMGPipeNullHandle; + MGPipeHandle VerbBlitDrawFbo = kMGPipeNullHandle; + Bool VerbBlitNamedConsumed = false; + // copy_framebuffer_to_texture's destination texture (MGPCopyFromFramebuffer::Dst). + MGPipeHandle VerbCopyTexDst = kMGPipeNullHandle; + // generate_mipmap's texture (MGPMipPlan::Res). + MGPipeHandle VerbMipRes = kMGPipeNullHandle; + // The current indirect draw's command buffer and (for the *Count forms) parameter + // buffer (MGPDrawIndirect::Buffer / ParameterBuffer). + MGPipeHandle VerbIndirectBuffer = kMGPipeNullHandle; + MGPipeHandle VerbIndirectParameterBuffer = kMGPipeNullHandle; + // dispatch_indirect's command buffer (MGPGridInfo::IndirectBuffer). + MGPipeHandle VerbDispatchIndirectBuffer = kMGPipeNullHandle; + + // Every verb's writer calls this FIRST and then sets its own fields, so no field ever + // outlives the verb that wrote it and a reader can treat non-null as "this verb's". + void ClearVerbHandles() { + VerbBlitReadFbo = kMGPipeNullHandle; + VerbBlitDrawFbo = kMGPipeNullHandle; + VerbBlitNamedConsumed = false; + VerbCopyTexDst = kMGPipeNullHandle; + VerbMipRes = kMGPipeNullHandle; + VerbIndirectBuffer = kMGPipeNullHandle; + VerbIndirectParameterBuffer = kMGPipeNullHandle; + VerbDispatchIndirectBuffer = kMGPipeNullHandle; + } +#endif + // ---- THE THREE FRAMEBUFFER ACCESSORS (ID-19(b)/(d)). They are functions rather than // members because the storage moved under them and their callers must not have to know // it did: `DrawFramebuffer()` / `ReadFramebuffer()` answer the question the two members diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 216ed756..ac9fae5e 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -734,42 +734,19 @@ namespace MobileGL::MG_Remote::Client { 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. + // P5c (hd, CONTRACT-P5C §3.3): the scoped rebind of the client's own read/draw + // binding slots is DELETED. It existed only to stage values for the server's + // binding-slot read, and that read is gone: the sink resolves both framebuffers + // from the record's handles. The validate still refreshes both framebuffers' + // emitted state (their Named set_framebuffer_state records), which is now the + // only description the server syncs from. No driver call or binding change + // happens on the client. MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer); BeforeReadOnlyVerb(); MG_Pipe::MGPBlit record{}; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index dcfc3783..177dbdd2 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -229,16 +229,35 @@ namespace MobileGL::MG_Remote::Server { const MG_Backend::GlobalBackendFunctionsTable* table = Table("blit"); if (table == nullptr) return false; if (table->GL.BlitFramebuffer == nullptr) return false; - // Same ruling as OnClear's: the read and draw framebuffers are already bound by the - // set_framebuffer_state records that preceded this one, so the unnamed entry point is - // the one that matches what the server's state actually is. For the named form the - // client temporarily binds the two named objects and validates before emitting; its - // existing BARRIER-PULLED fields stay fixed until this call returns. ReadFbo/DrawFbo - // retain the original identities for the later handle-only endpoint (CONTRACT-P5B §6). +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.3): the record's handles cross to the backend as the verb's + // own state. The bound form carries two nulls and nothing changes; the named form's + // pair is what the backend's named-blit arm resolves - the sink no longer relies on + // "the read and draw framebuffers are already bound" (the client's ScopedBlitBindings + // staging is deleted with this), and a backend that does not consume the pair has no + // named arm, which is a loud decline rather than a blit of whatever is bound. + auto& applierState = MG_Pipe::MGPipeApplier(); + applierState.ClearVerbHandles(); + applierState.VerbBlitReadFbo = blit.ReadFbo; + applierState.VerbBlitDrawFbo = blit.DrawFbo; + const Bool named = !MG_Pipe::MGPipeHandleIsNull(blit.ReadFbo) || + !MG_Pipe::MGPipeHandleIsNull(blit.DrawFbo); +#endif table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0, blit.DstY0, blit.DstX1, blit.DstY1, static_cast(blit.Mask), static_cast(blit.Filter)); +#if MOBILEGL_BUILD_DISAGGREGATED + if (named && !applierState.VerbBlitNamedConsumed) { + MGLOG_E_ONCE("MGPipe: a named blit (read {%u, %u}, draw {%u, %u}) reached a backend " + "with no named-blit arm; the verb is DECLINED rather than applied to " + "the bound framebuffers", + blit.ReadFbo.Slot, blit.ReadFbo.Gen, blit.DrawFbo.Slot, blit.DrawFbo.Gen); + applierState.VerbBlitReadFbo = MG_Pipe::kMGPipeNullHandle; + applierState.VerbBlitDrawFbo = MG_Pipe::kMGPipeNullHandle; + return false; + } +#endif ++m_blits; return true; } @@ -447,6 +466,15 @@ namespace MobileGL::MG_Remote::Server { if (indirect != nullptr) { // The layout already refused a record that sets both flags or declares ranges // beside the block, so NumDraws is 0 and there is no span here. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): the command/parameter buffer handles cross as the + // verb's own state; the backend's indirect arm resolves the buffer twins from + // them instead of reading the client's GL_DRAW_INDIRECT_BUFFER binding slot. + auto& applierState = MG_Pipe::MGPipeApplier(); + applierState.ClearVerbHandles(); + applierState.VerbIndirectBuffer = indirect->Buffer; + applierState.VerbIndirectParameterBuffer = indirect->ParameterBuffer; +#endif const auto offset = reinterpret_cast(static_cast(indirect->Offset)); const auto drawCount = static_cast(indirect->DrawCount); const auto stride = static_cast(indirect->Stride); @@ -650,6 +678,14 @@ namespace MobileGL::MG_Remote::Server { // 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. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): the buffer handle itself is now also the verb's own + // state, so the backend's dispatch-indirect arm resolves the twin from the record + // rather than from the client's GL_DISPATCH_INDIRECT_BUFFER binding slot. + auto& applierState = MG_Pipe::MGPipeApplier(); + applierState.ClearVerbHandles(); + applierState.VerbDispatchIndirectBuffer = grid.IndirectBuffer; +#endif gl.DispatchComputeIndirect(static_cast(grid.IndirectOffset)); } else { if (gl.DispatchCompute == nullptr) return false; @@ -870,6 +906,15 @@ namespace MobileGL::MG_Remote::Server { Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) { const auto* table = Table("GenerateMipmap"); if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.2): the texture the client resolved at Target on the active + // unit crosses as the verb's own state; the backend's mip-descriptor check resolves + // the record from it instead of probing the client allocator for the bound object's + // lifetime id (T2's mip half). + auto& applierState = MG_Pipe::MGPipeApplier(); + applierState.ClearVerbHandles(); + applierState.VerbMipRes = plan.Res; +#endif table->GL.GenerateMipmap(plan.Target); return true; } @@ -877,6 +922,14 @@ namespace MobileGL::MG_Remote::Server { Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) { const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); if (table == nullptr) return false; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.4): the destination texture the client resolved at the + // active unit crosses as the verb's own state; the backend resolves its twin from the + // handle instead of reading the client's texture-unit binding slot (T4). + auto& applierState = MG_Pipe::MGPipeApplier(); + applierState.ClearVerbHandles(); + applierState.VerbCopyTexDst = copy.Dst; +#endif if (copy.SubImage) { if (table->GL.CopyTexSubImage2D == nullptr) return false; table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset, diff --git a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp index 2cdd41cb..cc1f9f10 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp +++ b/MobileGL/MG_State/GLState/BufferState/BufferObject.cpp @@ -29,6 +29,28 @@ namespace MobileGL::MG_State::GLState { const BufferBackendOps* g_bufferBackendOps = nullptr; // Starts at 1 so a zero-initialized cache slot can never carry a live buffer's id. std::atomic g_nextBufferLifetimeId{1}; + +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.8 / §6 layer 1): the frontend BufferObject's legacy + // accessors are a layer-1 surface. With an active transport, the pre-handle buffer + // arm that reads them (Managers.cpp's RespecifyStorageNow / UploadRangeNow / + // IsBufferDrawClean / the old EnsureBufferResource body) stays compiled but may not + // run: the apply thread calling one is Fatal{RoleViolation, "buffer-legacy-arm"} - + // the same shape as Fatal{PipeLegacyMemosDisabled} - so a cleared subsystem bit 7 no + // longer leaves the arm silently readable. Client-thread callers (the GL thread's + // own state) are unaffected. + void RefuseLegacyBufferArmFromApplyThread(const char* accessor) { + if (!MG_Remote::Client::PersistentMapTracker::PushIsArmed()) return; + if (!MG_Remote::Client::PersistentMapTracker::OnServerRole()) return; + MGLOG_F("MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} - the apply thread " + "called BufferObject::%s on a frontend object. With an active transport " + "the server reads the applier's resource record and its own staged shadow; " + "a frontend object is client memory (rule E) and the pre-handle buffer arm " + "is monolith-only", + accessor); + std::abort(); + } +#endif } Uint64 BufferObject::AllocateLifetimeId() { @@ -378,6 +400,9 @@ namespace MobileGL::MG_State::GLState { void BufferObject::SyncPersistentMappedRange() { #if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd): the named refusal comes FIRST - the silent return below used to let a + // server-side caller slip through with one MGLOG_D's worth of evidence (B3). + RefuseLegacyBufferArmFromApplyThread("SyncPersistentMappedRange"); // Split's client pre-verb hook publishes these bytes. The retained backend sync // sites must do nothing on the apply thread: re-entering this producer there would // overwrite the server shadow through the monolith adapter without crossing the wire. @@ -914,6 +939,9 @@ namespace MobileGL::MG_State::GLState { } const Uint8* BufferObject::MappedData() const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyBufferArmFromApplyThread("MappedData"); +#endif return m_resource.Bytes(); } @@ -934,10 +962,18 @@ namespace MobileGL::MG_State::GLState { } Uint64 BufferObject::GetChangeSerial() const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyBufferArmFromApplyThread("GetChangeSerial"); +#endif return m_changeSerial; } Bool BufferObject::HasDefinedContent() const { +#if MOBILEGL_BUILD_DISAGGREGATED + // §3.6's surface as well as §3.8's: the descriptor's bit and the staged coverage + // answer on this side; the frontend flag is client memory. + RefuseLegacyBufferArmFromApplyThread("HasDefinedContent"); +#endif return m_hasDefinedContent; } @@ -950,6 +986,9 @@ namespace MobileGL::MG_State::GLState { } Bool BufferObject::IsMapped() const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyBufferArmFromApplyThread("IsMapped"); +#endif return m_isMapped; } diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index e40a97dd..b04edfbd 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -419,22 +419,26 @@ TEST(RemoteClientControls, BoundPackBufferOffsetReadRefusesByName) { } TEST(RemoteClientControls, ServerRoleRespecifyDoesNotEmitClientInitialBytes) { + // P5c (hd): the in-process server-role drive itself is now refused by name. The emit + // path's first move is the client allocator's Acquire for the buffer's handle, and with + // an active transport that entry is Fatal{RoleViolation, "MGPipeSlots"} from the apply + // thread (CONTRACT-P5C §3.1, rule E). "No client initial bytes" therefore holds the + // strong way - there is no client-emitter path for the server role to reach at all; ev's + // SEG_EVENT producers are what replaces the mechanism this case used to drive. const auto child = RunInChild([] { CapsPeer backend; Srv::ServerSessionInstance().SetBackend(&backend); StartControlSession(); MG_Config::Features.PipePush |= kMGPipeSubsystemResources; - const auto before = ClientWireRecordsEmitted(); - if (Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { MG_State::GLState::BufferObject buffer(701); Uint8 data[64]{}; buffer.Respecify(sizeof(data), data); return MOBILEGL_OK; - }, nullptr) != MOBILEGL_OK) ::_exit(92); - if (ClientWireRecordsEmitted() != before) ::_exit(93); + }, nullptr); ClientSessionInstance().Stop(); }); - ExpectChildSuccess(child); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); } TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) { @@ -451,34 +455,33 @@ TEST(RemoteClientControls, HeaderOnlyDynamicStateCrossesWithoutBlobMissing) { } TEST(RemoteClientControls, ServerRoleFlushDoesNotRunTheClientSubDataFollowup) { + // P5c (hd): same refusal as the respecify sibling above - a server-role flush reaching the + // client emit path is stopped at the allocator entry before any follow-up question arises + // (Fatal{RoleViolation, "MGPipeSlots"}; ev's events replace the mechanism). const auto child = RunInChild([] { CapsPeer backend; Srv::ServerSessionInstance().SetBackend(&backend); StartControlSession(); MG_Config::Features.PipePush |= kMGPipeSubsystemResources; - const auto rc = Srv::ServerLoopInstance().RunOnApplyThread([](void*) { - static int subdata = 0, flushes = 0; + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { MGPipeResourceOps ops{}; ops.Create = [](MGPipeHandle, const MGPResourceDesc&) {}; ops.Respecify = [](MGPipeHandle, const MGPResourceDesc&, const void*) {}; - ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) { ++subdata; }; - ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) { ++flushes; }; + ops.SubData = [](MGPipeHandle, const MGPSubData&, const void*) {}; + ops.FlushRange = [](MGPipeHandle, const MGPFlushRange&, const void*) {}; ops.Destroy = [](MGPipeHandle) {}; MGPipeSetResourceOps(&ops); { MG_State::GLState::BufferObject buffer(702); buffer.Resize(64); - subdata = flushes = 0; MGPipeEmitResourceFlushRange(buffer, 16, 16, 0); - if (subdata != 0 || flushes != 1) ::_exit(95); } MGPipeSetResourceOps(nullptr); return MOBILEGL_OK; }, nullptr); - if (rc != MOBILEGL_OK) ::_exit(96); ClientSessionInstance().Stop(); }); - ExpectChildSuccess(child); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); } // Uses the shipped EGL/GL frontend and real DirectGLES driver. Invoked explicitly by the // package gate because a native driver is not a prerequisite of the unit lane. diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 96a156d2..20abcbf1 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1230,10 +1230,12 @@ struct F1Peer : Codec::WireVerbSink { MGPClear clear{}; MGPCopyFromFramebuffer copy{}; MGPMipPlan mip{}; + MGPBlit blit{}; unsigned calls = 0; Bool OnClear(const MGPClear& v) override { clear = v; ++calls; return true; } Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& v) override { copy = v; ++calls; return true; } Bool OnGenerateMipmap(const MGPMipPlan& v) override { mip = v; ++calls; return true; } + Bool OnBlit(const MGPBlit& v) override { blit = v; ++calls; return true; } void Install() { if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); @@ -1514,6 +1516,333 @@ TEST(RemoteF1, UnboundNamedfiRefusesByName) { } #endif +// ===================================================================================== +// P5c (hd, CONTRACT-P5C §3): the record's handles are the server's resolution. The sink +// publishes each verb's own handles into the applier's verb stash (§3.2), the named blit's +// client emitter no longer rebinds anything (§3.3), and the layer-1 guards refuse the +// client-only surfaces from the apply thread by name (§3.1, §3.7, §3.8). +// ===================================================================================== +#if MGTEST_HAVE_FORK +#include +#include +#include +#include + +TEST(RemoteF1, BlitNamedCarriesHandlesAndLeavesTheClientBindingsAlone) { + // Red once (executed, reverted): re-bind the named arguments in the client shadow before + // emitting (the deleted ScopedBlitBindings); the two binding-slot asserts fail. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; + peer.Install(); + MG_State::pGLContext = MakeUnique(); + auto boundRead = MakeShared(11); + auto boundDraw = MakeShared(12); + auto namedRead = MakeShared(13); + auto namedDraw = MakeShared(14); + MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read).Bind(boundRead); + MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw).Bind(boundDraw); + + RemoteEmitTable().GL.BlitNamedFramebuffer(namedRead, namedDraw, 0, 0, 8, 8, 0, 0, 8, 8, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + const auto& r = peer.blit; + if (peer.calls != 1 || r.ReadFbo != MGPipeFramebufferEmitter::HandleFor(*namedRead) || + r.DrawFbo != MGPipeFramebufferEmitter::HandleFor(*namedDraw)) ::_exit(101); + // The client's own bindings are untouched: the server resolves the named pair from the + // record, not from a staged binding override (T3). + if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Read) + .GetBoundObject() + .get() != boundRead.get()) ::_exit(102); + if (MG_State::pGLContext->GetFramebufferBindingSlot(MobileGL::FramebufferTarget::Draw) + .GetBoundObject() + .get() != boundDraw.get()) ::_exit(103); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); +} + +namespace { + Bool g_stubBlitCalled = false; + MGPipeHandle g_observedBlitRead = kMGPipeNullHandle; + MGPipeHandle g_observedBlitDraw = kMGPipeNullHandle; + void StubBlitFramebuffer(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum) { + g_stubBlitCalled = true; + const auto& st = MG_Pipe::MGPipeApplier(); + g_observedBlitRead = st.VerbBlitReadFbo; + g_observedBlitDraw = st.VerbBlitDrawFbo; + } + void StubBlitFramebufferConsuming(GLint x0, GLint y0, GLint x1, GLint y1, GLint dx0, GLint dy0, + GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) { + StubBlitFramebuffer(x0, y0, x1, y1, dx0, dy0, dx1, dy1, mask, filter); + MG_Pipe::MGPipeApplier().VerbBlitNamedConsumed = true; + } + + MGPipeHandle g_observedCopyDst = kMGPipeNullHandle; + void StubCopyTexImage2D(GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint) { + g_observedCopyDst = MG_Pipe::MGPipeApplier().VerbCopyTexDst; + } + MGPipeHandle g_observedMipRes = kMGPipeNullHandle; + void StubGenerateMipmap(GLenum) { g_observedMipRes = MG_Pipe::MGPipeApplier().VerbMipRes; } + MGPipeHandle g_observedIndirect = kMGPipeNullHandle; + MGPipeHandle g_observedParameter = kMGPipeNullHandle; + void StubMultiDrawArraysIndirectCount(GLenum, const void*, GLintptr, GLsizei, GLsizei) { + const auto& st = MG_Pipe::MGPipeApplier(); + g_observedIndirect = st.VerbIndirectBuffer; + g_observedParameter = st.VerbIndirectParameterBuffer; + } + MGPipeHandle g_observedDispatchIndirect = kMGPipeNullHandle; + void StubDispatchComputeIndirect(GLintptr) { + g_observedDispatchIndirect = MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer; + } +} // namespace + +TEST(RemoteF1, NamedBlitStashesHandlesAndDeclinesWhenTheBackendHasNoNamedArm) { + // Red once (executed, reverted): do not write the verb stash; the observed handles are null. + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPBlit r{}; + r.ReadFbo = {41, 2}; + r.DrawFbo = {42, 3}; + r.Mask = GL_COLOR_BUFFER_BIT; + r.Filter = GL_NEAREST; + const Bool applied = sink.OnBlit(r); + if (applied) ::_exit(101); // a named pair the backend did not consume must decline + if (!g_stubBlitCalled) ::_exit(102); + // The backend saw the record's handles as the verb's own state (CONTRACT-P5C §3.2/§3.3). + if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(103); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, NamedBlitIsAppliedWhenTheBackendConsumesThePair) { + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.BlitFramebuffer = &StubBlitFramebufferConsuming; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPBlit r{}; + r.ReadFbo = {41, 2}; + r.DrawFbo = {42, 3}; + r.Mask = GL_COLOR_BUFFER_BIT; + r.Filter = GL_NEAREST; + if (!sink.OnBlit(r)) ::_exit(101); + if (g_observedBlitRead != r.ReadFbo || g_observedBlitDraw != r.DrawFbo) ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, BoundBlitCarriesNoHandlesAndIsAppliedAsBefore) { + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.BlitFramebuffer = &StubBlitFramebuffer; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPBlit r{}; + r.Mask = GL_COLOR_BUFFER_BIT; + r.Filter = GL_NEAREST; + if (!sink.OnBlit(r)) ::_exit(101); + if (!MGPipeHandleIsNull(g_observedBlitRead) || !MGPipeHandleIsNull(g_observedBlitDraw)) + ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, CopyFramebufferToTextureStashesTheDestinationHandle) { + // Red once (executed, reverted): do not write VerbCopyTexDst; the observed handle is null. + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.CopyTexImage2D = &StubCopyTexImage2D; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPCopyFromFramebuffer r{}; + r.Dst = {51, 4}; + r.Target = GL_TEXTURE_2D; + r.Level = 1; + r.InternalFormat = GL_RGBA8; + r.Width = 8; + r.Height = 8; + if (!sink.OnCopyFramebufferToTexture(r)) ::_exit(101); + if (g_observedCopyDst != r.Dst) ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, GenerateMipmapStashesTheTextureHandle) { + // Red once (executed, reverted): do not write VerbMipRes; the observed handle is null. + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.GenerateMipmap = &StubGenerateMipmap; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPMipPlan r{}; + r.Res = {52, 5}; + r.Target = GL_TEXTURE_2D; + if (!sink.OnGenerateMipmap(r)) ::_exit(101); + if (g_observedMipRes != r.Res) ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, AnIndirectDrawStashesTheCommandAndParameterBuffers) { + // Red once (executed, reverted): do not write the indirect stash; the observed handles are null. + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.MultiDrawArraysIndirectCount = &StubMultiDrawArraysIndirectCount; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPDrawInfo info{}; + info.Mode = GL_TRIANGLES; + info.IndexSize = 0; + info.NumDraws = 0; + info.InstanceCount = 1; + MGPDrawIndirect indirect{}; + indirect.Buffer = {61, 1}; + indirect.ParameterBuffer = {62, 1}; + indirect.Offset = 64; + indirect.DrawCount = 3; + indirect.Stride = 16; + if (!sink.OnDrawVbo(info, nullptr, nullptr, &indirect)) ::_exit(101); + if (g_observedIndirect != indirect.Buffer || g_observedParameter != indirect.ParameterBuffer) + ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +TEST(RemoteF1, DispatchIndirectStashesTheCommandBuffer) { + // Red once (executed, reverted): do not write VerbDispatchIndirectBuffer; the observed + // handle is null. + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + CapsPeer backend; + backend.table.GL.DispatchComputeIndirect = &StubDispatchComputeIndirect; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPGridInfo grid{}; + grid.IsIndirect = 1; + grid.IndirectBuffer = {63, 1}; + grid.IndirectOffset = 128; + if (!sink.OnLaunchGrid(grid)) ::_exit(101); + if (g_observedDispatchIndirect != grid.IndirectBuffer) ::_exit(102); + ::_exit(0); + }); + ExpectChildSuccess(child); +} + +// ---- the layer-1 guards (CONTRACT-P5C §6), each verified red once by its own Fatal --------- + +TEST(RemoteGuards, AllocatorAcquireFromTheApplyThreadIsFatalByName) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + MGPipeSlots().Acquire(MGPipeKind::Texture, 424242); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +TEST(RemoteGuards, AllocatorFindByLifetimeIdFromTheApplyThreadIsFatalByName) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, 424242); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +TEST(RemoteGuards, AllocatorFreeFromTheApplyThreadIsFatalByName) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + MGPipeSlots().Free(MGPipeKind::Texture, {7, 1}); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +namespace { + struct FakeTwin { + int marker = 0; + }; + using TestTextureTable = + MG_Backend::DirectGLES::BackendSlotTable; +} // namespace + +TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + MG_State::pGLContext = MakeUnique(); + TestTextureTable table; + MG_State::GLState::TextureObject2D tex(44); + table.HandleOf(&tex); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) { + const auto child = RunInChild([] { + StartControlSession(); + Srv::ServerLoopInstance().RunOnApplyThread([](void*) { + MG_State::pGLContext = MakeUnique(); + TestTextureTable table; + auto tex = MakeShared(45); + table.GetOrCreate(tex); + return MOBILEGL_OK; + }, nullptr); + ClientSessionInstance().Stop(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); +} + +// The four accessor drives share one shape: the BufferObject is constructed on the CLIENT +// thread (its own resource_create publication is a legal client-side emit), and only the +// accessor runs on the apply thread, where it must die at the accessor's own guard. +#define MGL_BUFFER_GUARD_TEST(Name, Id, Call) TEST(RemoteGuards, Name) { const auto child = RunInChild([] { StartControlSession(); MG_State::GLState::BufferObject buffer(Id); Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { auto& buffer = *static_cast(self); Call; return MOBILEGL_OK; }, &buffer); ClientSessionInstance().Stop(); }); ExpectNamedAbort(child, "Fatal{RoleViolation, \"buffer-legacy-arm\"}"); } +MGL_BUFFER_GUARD_TEST(BufferMappedDataFromTheApplyThreadIsFatalByName, 703, (void)buffer.MappedData()) +MGL_BUFFER_GUARD_TEST(BufferIsMappedFromTheApplyThreadIsFatalByName, 704, (void)buffer.IsMapped()) +MGL_BUFFER_GUARD_TEST(BufferChangeSerialFromTheApplyThreadIsFatalByName, 705, (void)buffer.GetChangeSerial()) +MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalByName, 706, + buffer.SyncPersistentMappedRange()) +#undef MGL_BUFFER_GUARD_TEST + +TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) { + const auto child = RunInChild([] { + MG_Config::Transport = MG_Config::TransportMode::InProcess; + // No server backend exists in this process: the format-caps fallback to the client + // mirror is refused by name (CONTRACT-P5C §3.7). + (void)MG_Backend::DirectGLES::ActiveBackendFormatCaps(); + }); + ExpectNamedAbort(child, "Fatal{RoleViolation, \"caps-mirror\"}"); +} +#endif + + int main(int argc, char** argv) { namespace fs = std::filesystem; const fs::path path = diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index defe0d7e..078ee1f8 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -1629,44 +1629,6 @@ TEST(StagedShadowProductionTest, ATwinSurvivingContextLossDropsItsFreedShadowBas // shows it": B is the corruption, the upload is the draw, the mapped read-back is the picture. Red // once by restoring the MappedData() fallback in liveHostBase(): the store then holds A, the // client's bytes, and the R-2.5 audit could never reach a draw again. -TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) { - MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots; - EglServerFixture fixture; - MGL_EGL_BRING_UP_OR_BAIL(fixture); - ASSERT_TRUE(fixture.MakeCurrent()); - namespace GLES = MG_Backend::DirectGLES; - static auto nativeDelete = GLES::g_GLESFuncs.glDeleteFramebuffers; - static std::atomic deleted{0}; - static std::atomic wrongThread{false}; - nativeDelete = GLES::g_GLESFuncs.glDeleteFramebuffers; - deleted.store(0); - wrongThread.store(false); - auto framebuffer = MakeShared(901u); - GLuint driverId = 0; - ASSERT_EQ(OnApply([&] { - auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer); - twin = MakeShared(); - driverId = twin->GetBackendFramebufferId(); - twin->Bind(FramebufferTarget::Draw); - GLES::g_GLESFuncs.glDeleteFramebuffers = +[](GLsizei count, const GLuint* names) { - if (!Server::ServerLoop::OnApplyThread()) wrongThread.store(true); - deleted.fetch_add(count); - nativeDelete(count, names); - }; - }), MOBILEGL_OK); - ASSERT_NE(driverId, 0u); - framebuffer.reset(); // Frontend destructor runs on this client thread. - EXPECT_EQ(deleted.load(), 1u); - EXPECT_FALSE(wrongThread.load()); - ASSERT_EQ(OnApply([&] { - GLES::g_GLESFuncs.glDeleteFramebuffers = nativeDelete; - GLint bound = -1; - GLES::g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &bound); - EXPECT_EQ(bound, 0) << "native deletion must unbind the framebuffer in the owner context"; - }), MOBILEGL_OK); - fixture.TearDown(); -} - TEST(StagedShadowProductionTest, TheEnsurePathUploadsTheServerShadowNotTheClientObjectsBytes) { EglServerFixture fixture; MGL_EGL_BRING_UP_OR_BAIL(fixture); @@ -1820,6 +1782,49 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot "whole-store refusal must key on the descriptor's HasDefinedContent); the log says: " << appended; } +namespace { + // The drive below, in a FORKED CHILD that brings the server up itself (the integration + // harness's pre-flight shape), so a Fatal on the apply thread ends the child and not the + // case. Exit 8 = the child's bring-up failed; exit 0 = the apply-thread twin mint ran, + // which is the refusal this case pins having been removed. + [[noreturn]] void RunFrontendFramebufferDeathOnApplyThreadAndExit() { + EglServerFixture fixture; + if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8); + namespace GLES = MG_Backend::DirectGLES; + auto framebuffer = MakeShared(901u); + (void)OnApply([&] { + auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer); + twin = MakeShared(); + }); + framebuffer.reset(); + ::_exit(0); + } +} // namespace + +// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): this case's original drive - mint the framebuffer +// twin on the apply thread off the frontend object's lifetime id, then let the frontend death +// notice reach the in-process death switch there - is refused by name at the first step: +// the minting GetOrCreate(StatePtr) is monolith glue and the apply-thread call is +// Fatal{RoleViolation, "MGPipeSlots"}. The deletion-on-owner semantics it pinned return with +// ct's object_death record (the framebuffer family's first wire delete), whose sink releases +// the twin BY HANDLE. The name stays; the pin is now the refusal that stands until ct lands. +TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) { + MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots; + { + EglServerFixture probe; + MGL_EGL_BRING_UP_OR_BAIL(probe); + probe.TearDown(); + } + const SizeT mark = ReadLog().size(); + EXPECT_EXIT(RunFrontendFramebufferDeathOnApplyThreadAndExit(), ::testing::KilledBySignal(SIGABRT), ".*") + << "the apply-thread mint of a frontend framebuffer's twin was not refused; exit 8 is the " + "child's own bring-up failing"; + const std::string appended = ReadLogFrom(mark); + EXPECT_NE(appended.find("Fatal{RoleViolation, \"MGPipeSlots\"}"), std::string::npos) + << "the abort was not the allocator guard's; the log says: " << appended; +} + + #endif // !_WIN32 // ===================================================================================== diff --git a/MobileGL/MG_Test/Wire/c1f_redcheck.py b/MobileGL/MG_Test/Wire/c1f_redcheck.py index 1535277f..fe6e96ed 100644 --- a/MobileGL/MG_Test/Wire/c1f_redcheck.py +++ b/MobileGL/MG_Test/Wire/c1f_redcheck.py @@ -58,12 +58,11 @@ def cases(): [SUITE+'ReadPixelsPutsTheTightExtentOnTheWire'], 'RemoteClientTest'), ('M4-escape-errors-deleted', WIRE, escape_error, [SUITE+'ResourceRespecifyErrorIsNotADecline', SUITE+'MapPersistentErrorIsNotADecline'], 'RemoteClientTest'), - ('M5-server-role-guards-deleted', FILL, - replace('!MG_Remote::Client::RunsAsTheServerRole() && ', '', 2), - [SUITE+'ServerRoleRespecifyDoesNotEmitClientInitialBytes'], 'RemoteClientTest'), - ('M5-server-flush-guard-deleted', FILL, - replace('!MG_Remote::Client::RunsAsTheServerRole() && size != 0', 'size != 0'), - [SUITE+'ServerRoleFlushDoesNotRunTheClientSubDataFollowup'], 'RemoteClientTest'), + ('M5-allocator-guards-deleted', 'MobileGL/MG_Impl/Pipe/SlotAllocator.cpp', + lambda src: replace('MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId");', '')( + replace('MGPipeRefuseAllocatorFromApplyThread("Acquire");', '')(src)), + [SUITE+'ServerRoleRespecifyDoesNotEmitClientInitialBytes', + SUITE+'ServerRoleFlushDoesNotRunTheClientSubDataFollowup'], 'RemoteClientTest'), ('M8-real-pack-binding', EMIT, replace('if (MG_State::pGLContext != nullptr &&', 'if (false && MG_State::pGLContext != nullptr &&'), [SUITE+'BoundPackBufferOffsetReadRefusesByName'], 'RemoteClientTest'),