diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 0293c081..db93234f 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -362,6 +362,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(); @@ -907,10 +943,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); + } } } @@ -937,6 +989,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); } } @@ -1452,6 +1517,11 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif #if MOBILEGL_PIPE_PUSH if (EsprytSlotTablesEnabled()) { // No memo on this arm. The memo existed to turn the registry's hash probe into @@ -1569,6 +1639,13 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool imageBindableStorageRequired) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): this sync still arrives holding the FRONTEND object + // (the object-class rows) and resolves its twin by frontend identity - the + // frontend-keyed registry P3b/P4b rekeys onto handles. The probe (and the mint on + // a first sync) is named debt inside the scope, not an unwrapped violation. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; #endif auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get()); auto& backendSlot = backendTextureSlot ? *backendTextureSlot @@ -2418,6 +2495,12 @@ namespace MobileGL::MG_Backend::DirectGLES { static const MG_Pipe::MGPImageView* ResolveShaderImageRecord( Uint unit, const MG_State::GLState::ITextureObject* boundTexture) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the image seam's identity check below resolves the + // bound texture's handle by frontend identity - frontend-keyed twin resolution, + // named debt inside the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif // P4a decline-site I1: M - the mask says this family is not switched on, and it // IS D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification // round - and it is what keeps I2 below quiet on a mask that never armed images. @@ -2852,6 +2935,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()); } @@ -3041,6 +3132,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 @@ -3109,6 +3210,12 @@ namespace MobileGL::MG_Backend::DirectGLES { MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr; for (auto& target : fboTargets) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the twin resolutions this iteration may run + // below are frontend-keyed, named debt inside the scope - P3b/P4b rekeys the + // registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto& slot = GetFramebufferBindingSlotChecked(target); auto& currentFBO = slot.GetBoundObject(); @@ -4036,6 +4143,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // Anything else and the frontend's own MapUBO answers, exactly as it does today. static const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecord( const MG_State::GLState::ProgramObject* program) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the program's handle is resolved by frontend + // identity below - frontend-keyed twin resolution, named debt inside the scope - + // P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif // P4a decline-site P1: M - the mask says this family is not switched on, or there // is no current program at all; it IS D's ProgramSubsystemEnabled() now. Silent, // confirmed at the verification round. @@ -4122,6 +4235,12 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncCurrentProgram(const SharedPtr& currentProgram) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the twin resolution below still keys on the frontend + // program object - named debt inside the scope - P3b/P4b rekeys the registry onto + // handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; #endif g_backendProgramObjects.CollectGarbageIfNeeded(); SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded(); @@ -4374,6 +4493,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // and the twin memo replaces even that with an array probe on the steady path. const auto& currentFBO = slot.GetBoundObject(); if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif FramebufferImpl::BackendFramebufferObject* twin = nullptr; #if MOBILEGL_PIPE_PUSH if (EsprytSlotTablesEnabled()) { @@ -4446,12 +4570,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; @@ -4642,6 +4834,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } // Bind texture object +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt + // inside the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); if (!backendTextureSlot || !*backendTextureSlot) { fullyResolved = false; @@ -4704,6 +4901,11 @@ namespace MobileGL::MG_Backend::DirectGLES { static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend( Int unit, const SharedPtr& samplerObject) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto& memo = g_unitSamplerLookupMemos[static_cast(unit)]; #if MOBILEGL_PIPE_PUSH if (EsprytSlotTablesEnabled()) { @@ -5032,6 +5234,12 @@ namespace MobileGL::MG_Backend::DirectGLES { if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) { #ifdef TRACY_ENABLE ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND); +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the program/sampler twin resolutions below still key + // on frontend objects - named debt inside the scope - P3b/P4b rekeys the registry + // onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; #endif // The twin SyncCurrentProgram just resolved for this draw; the registry // Find only runs if the stash somehow does not match (defensive fallback). @@ -5389,6 +5597,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) { return PrgramImpl::g_currentDrawBackendProgram; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif if (auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(currentProgram.get())) { return backendProgramSlot->get(); } @@ -5656,14 +5869,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); @@ -5728,12 +5963,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); @@ -5819,6 +6074,11 @@ namespace MobileGL::MG_Backend::DirectGLES { return 0; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get()); auto& backendObj = backendProgramSlot ? *backendProgramSlot : PrgramImpl::g_backendProgramObjects.GetOrCreate(programObject); @@ -6163,44 +6423,121 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT sourceIndexSize = MG_Util::GetGLTypeSize(indexType); const Uint32 fixedMax = MG_Util::FixedRestartIndexForGLType(indexType); const Uint32 applicationRestartIndex = MGB_CTX->GetPrimitiveRestartIndex(); - const auto& indexBuffer = BoundElementArrayBuffer(); const Uint8* source = nullptr; SizeT indexCount = 0; SizeT sourceByteOffset = 0; + Bool hasElementBuffer = false; +#if MOBILEGL_BUILD_DISAGGREGATED + // -1: monolith, or this arm never resolved a backend id, and the tail asks + // BoundElementArrayBufferId() exactly as it always did. + Int serverElementBinding = -1; +#endif - if (indexBuffer) { - // The WHOLE buffer is rewritten, not just this draw's range, so that every index - // keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be - // adjusted from here. It is an ELEMENT index, so it survives widening unchanged. - const SizeT sizeBytes = indexBuffer->GetSize(); - if (sizeBytes < sourceIndexSize) { - return; // Nothing to restart on; let the driver see the draw unchanged. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (hd, CONTRACT-P5C §3.5): with an active transport this constructor runs on the + // apply thread, where the frontend BufferObject's legacy accessors are Fatal by name + // (BufferObject.cpp's "buffer-legacy-arm" refusal). The element buffer therefore + // resolves from the applier's set_index_buffer state (T2) and the bytes read from the + // server's own staged shadow - "the backend's ScopedRestartIndexSubstitution reads the + // index bytes it needs on its side" (CONTRACT-P5B d1), the same arm + // MultiDrawElementsIndirectCount takes for its command buffer. A draw whose record + // carried client indices (no index buffer bound) takes the client-pointer arm below, + // `indices` already resolved through the server's segment resolver. Monolith takes + // the original body, verbatim. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto& applierState = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle elements = applierState.IndexBuffer.Res; + serverElementBinding = 0; + if (!MG_Pipe::MGPipeHandleIsNull(elements)) { + auto* elementResource = BufferImpl::FindBufferResourceForHandle(elements); + // The WHOLE buffer is rewritten, not just this draw's range, so that every + // index keeps its position: an indirect draw's firstIndex lives in GPU memory + // and cannot be adjusted from here. It is an ELEMENT index, so it survives + // widening unchanged. + const SizeT sizeBytes = BufferImpl::ResourceWidthForHandle(elements); + if (sizeBytes < sourceIndexSize) { + return; // Nothing to restart on; let the driver see the draw unchanged. + } + if (sizeBytes > kMaxRestartRewriteBytes) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element " + "array buffer rewritten every draw, which is past the %zu-byte ceiling. Use " + "GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones " + "value of the index type.", + applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes); + m_valid = false; + return; + } + // The staged shadow is the source of truth for CPU reads on this side: a + // persistent map's blocks and a shader write's writeback were consumed by the + // applier before this verb ran. + const Uint8* hostBytes = elementResource ? elementResource->hostBytes : nullptr; + if (hostBytes == nullptr) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of " + "the bound element array buffer and none is available.", + applicationRestartIndex); + m_valid = false; + return; + } + // The M-3 rule for a whole-store reader (Managers.cpp:3238): for a store whose + // content the application SUPPLIED a coverage gap is a missing record and is + // Fatal by name; for one it ORPHANED the gap is its own undefined content and + // reading the shadow's zero-fill is exactly what the monolith arm's + // MappedData() answers. + const MG_Pipe::MGPipeResourceRecord* elementRecord = nullptr; + if (elements.Slot < applierState.Resources.size()) { + const auto& candidate = applierState.Resources[elements.Slot]; + if (candidate.Live && candidate.Gen == elements.Gen) elementRecord = &candidate; + } + if (elementRecord != nullptr && elementRecord->Desc.HasDefinedContent != 0) { + BufferImpl::RequireStagedCoverage(*elementResource, hostBytes, 0, sizeBytes, + "primitive_restart_substitution"); + } + hasElementBuffer = true; + serverElementBinding = static_cast(elementResource->id); + source = hostBytes; + indexCount = sizeBytes / sourceIndexSize; + sourceByteOffset = reinterpret_cast(indices); } - if (sizeBytes > kMaxRestartRewriteBytes) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element " - "array buffer rewritten every draw, which is past the %zu-byte ceiling. Use " - "GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones " - "value of the index type.", - applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes); - m_valid = false; - return; + } else +#endif + { + const auto& indexBuffer = BoundElementArrayBuffer(); + if (indexBuffer) { + hasElementBuffer = true; + // The WHOLE buffer is rewritten, not just this draw's range, so that every index + // keeps its position: an indirect draw's firstIndex lives in GPU memory and cannot be + // adjusted from here. It is an ELEMENT index, so it survives widening unchanged. + const SizeT sizeBytes = indexBuffer->GetSize(); + if (sizeBytes < sourceIndexSize) { + return; // Nothing to restart on; let the driver see the draw unchanged. + } + if (sizeBytes > kMaxRestartRewriteBytes) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs the %zu-byte element " + "array buffer rewritten every draw, which is past the %zu-byte ceiling. Use " + "GL_PRIMITIVE_RESTART_FIXED_INDEX, or set glPrimitiveRestartIndex to the all-ones " + "value of the index type.", + applicationRestartIndex, sizeBytes, kMaxRestartRewriteBytes); + m_valid = false; + return; + } + // The shadow is the source of truth for CPU reads, but a persistent map or a + // shader write may have moved past it since the last sync. + indexBuffer->SyncPersistentMappedRange(); + indexBuffer->SyncGpuWrites(); + source = indexBuffer->MappedData(); + if (source == nullptr) { + MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of " + "the bound element array buffer and none is available.", + applicationRestartIndex); + m_valid = false; + return; + } + indexCount = sizeBytes / sourceIndexSize; + sourceByteOffset = reinterpret_cast(indices); } - // The shadow is the source of truth for CPU reads, but a persistent map or a - // shader write may have moved past it since the last sync. - indexBuffer->SyncPersistentMappedRange(); - indexBuffer->SyncGpuWrites(); - source = indexBuffer->MappedData(); - if (source == nullptr) { - MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of " - "the bound element array buffer and none is available.", - applicationRestartIndex); - m_valid = false; - return; - } - indexCount = sizeBytes / sourceIndexSize; - sourceByteOffset = reinterpret_cast(indices); - } else { + } + if (!hasElementBuffer) { // No element array buffer: `indices` is a client pointer, so only the draw's own // range is readable and an indirect draw has nothing to read at all. if (count <= 0 || indices == nullptr || sourceIndexSize == 0) { @@ -6254,14 +6591,22 @@ namespace MobileGL::MG_Backend::DirectGLES { m_valid = false; return; } - m_previousBinding = BoundElementArrayBufferId(); + m_previousBinding = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + if (serverElementBinding >= 0) { + m_previousBinding = static_cast(serverElementBinding); + } else +#endif + { + m_previousBinding = BoundElementArrayBufferId(); + } BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id); m_substituted = true; m_indexType = destinationType; // The rewritten copy starts at byte 0 of the scratch buffer and holds one // destination-width element per source element, so an EBO-sourced draw keeps its ELEMENT // offset (rescaled to the new width) and a client-memory draw reads from the front. - m_indices = indexBuffer + m_indices = hasElementBuffer ? reinterpret_cast((sourceByteOffset / sourceIndexSize) * destinationIndexSize) : nullptr; } @@ -6292,6 +6637,11 @@ namespace MobileGL::MG_Backend::DirectGLES { PrepareForDraw(syncBit); const auto& currentVAO = MGB_CTX->GetBoundVertexArray(); if (currentVAO) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get()); if (backendVAOSlot && *backendVAOSlot) { (*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first, count); @@ -6333,6 +6683,11 @@ namespace MobileGL::MG_Backend::DirectGLES { for (GLsizei i = 0; i < drawcount; ++i) { // Client-side arrays are uploaded per sub-draw range, like the single DrawArrays path. if (currentVAO) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt + // inside the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendVAOSlot = VertexArrayImpl::g_backendVertexArrayObjects.Find(currentVAO.get()); if (backendVAOSlot && *backendVAOSlot) { (*backendVAOSlot)->SyncClientSideAttributesForDrawArrays(currentVAO, first[i], count[i]); @@ -6400,6 +6755,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"); @@ -6431,6 +6794,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) { @@ -6502,6 +6912,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"); @@ -6533,6 +6951,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) { @@ -6706,6 +7166,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), @@ -6747,6 +7215,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"); @@ -7763,6 +8239,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()); @@ -7859,6 +8392,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(); @@ -8089,8 +8645,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"); @@ -8188,6 +8745,11 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(texture.get()); if (!backendTextureSlot || !*backendTextureSlot) { return; @@ -8508,6 +9070,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // The record resolution is the same registry lookup texture sync and // EnsureGenerateMipmapStorageAllocated's disaggregated arm already make. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get()); const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle); if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) { @@ -8560,6 +9125,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // same §1 extent derivation, same registry resolution, same refusal when the handle // arm has no record to read. if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get()); const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle); if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) { @@ -8623,18 +9191,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, @@ -8668,7 +9260,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()); }); @@ -8718,16 +9310,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()); @@ -8749,7 +9360,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()); }); @@ -8992,6 +9603,11 @@ namespace MobileGL::MG_Backend::DirectGLES { static SharedPtr SyncRenderbufferObjectToBackend( const SharedPtr& renderbufferObject) { if (!renderbufferObject) return nullptr; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on a first + // sync), named debt inside the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif SharedPtr backendRenderbufferObject; if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) { backendRenderbufferObject = *slot; @@ -9321,6 +9937,11 @@ namespace MobileGL::MG_Backend::DirectGLES { auto& programObject = MGB_CTX->GetProgramObject(program); if (!programObject) return; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendProgramSlot = PrgramImpl::g_backendProgramObjects.Find(programObject.get()); if (!backendProgramSlot || !*backendProgramSlot) return; auto& backendObj = *backendProgramSlot; @@ -11171,6 +11792,11 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("GetTexImage: bound texture object = %p (name=%u)", textureObject.get(), textureObject ? textureObject->GetExternalIndex() : 0); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); if (!backendTextureSlot || !*backendTextureSlot) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 42178fb0..65adb379 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -220,6 +220,12 @@ namespace MobileGL::MG_Backend::DirectGLES { }, &death); return; } + // P5c merge coordination: while the death notice still rides the mailbox it lands + // HERE, on the apply thread, and every arm's DestroyByLifetimeId probes the client + // allocator. That hop is exactly what ct's object_death record replaces (the sink + // then resolves by the record's handle and the scope dies with the mailbox); until + // ct lands the probes are named debt inside the G6 scope, not unwrapped violations. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; #endif switch (kind) { case MG_Pipe::MGPipeKind::Texture: @@ -2841,6 +2847,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 @@ -2851,6 +2864,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // allocator probe on a miss - and remembers the answer, which the minting overload // was previously the only writer of. A null answer is deliberately never memoised // (see the comment at HandleOf). +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c merge coordination (CONTRACT-P5C §3.1's named exemption): every caller of + // this function is a site whose handle-carrying records (set_shader_buffers / + // set_stream_output_targets) the client does not emit until P4b, so the probe + // below runs inside the scoped exemption. The scope, not a silent guard removal, + // is what keeps the debt named and greppable. + const MG_Pipe::MGPipeReverseAnnouncementScope reverseAnnouncement; +#endif return g_backendBufferResources.HandleOf(bufferObject); } @@ -3245,7 +3266,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; @@ -3273,24 +3304,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 @@ -4438,6 +4473,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // MGPipeSlots().FindByLifetimeId, which is a hash lookup. HandleOf answers // identically - the same allocator probe on a miss - and remembers the answer; a // null answer is deliberately never memoised (SlotTables.h, at HandleOf). +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the sampler-view registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif return g_backendSamplerViews.HandleOf(textureObject); } } // namespace SamplerViewImpl @@ -5769,8 +5809,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // because that is what this arm consumes. Bool markedRemintPull = false; const MG_Pipe::MGPipeHandle rearmRes = - TextureResourceSubsystemEnabled() ? g_backendTextureObjects.HandleOf(stateTextureObject.get()) - : MG_Pipe::kMGPipeNullHandle; + TextureResourceSubsystemEnabled() + ? [&]() { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named + // debt inside the scope - P3b/P4b rekeys the registry. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif + return g_backendTextureObjects.HandleOf(stateTextureObject.get()); + }() + : MG_Pipe::kMGPipeNullHandle; const Uint32 rearmResourceTarget = MG_Pipe::MGPipeResourceTargetForTextureTarget(stateTextureObject->GetTarget()); if (TextureResourceSubsystemEnabled() && MG_Pipe::MGPipeHandleIsNull(rearmRes)) { @@ -6932,6 +6980,12 @@ namespace MobileGL::MG_Backend::DirectGLES { const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr; MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle; if (TextureResourceSubsystemEnabled()) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the sync arrives holding the frontend object + // (the object-class rows) and resolves its handle by frontend identity - + // named debt inside the scope, P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif pushedRes = g_backendTextureObjects.HandleOf(stateTextureObject.get()); pushedStorage = PipeTextureRecordForHandle(pushedRes); if (pushedStorage == nullptr) { @@ -8279,6 +8333,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // object this backend still arrives holding. Under a real split the handle rides in // the payload; the client mints it off this object's lifetime id, so the allocator // resolves it here through the table's own single-entry front memo. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); const auto* record = PipeTextureRecordForHandle(res); if (record == nullptr) { @@ -8510,6 +8569,11 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_PIPE_PUSH const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams( const SharedPtr& stateTextureObject) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); const auto* record = PipeTextureRecordForHandle(res); if (record == nullptr) { @@ -9044,6 +9108,11 @@ namespace MobileGL::MG_Backend::DirectGLES { if (attachmentObject.IsTexture()) { const auto& textureObject = attachmentObject.GetTexture(); SharedPtr backendTextureObject; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on + // a first attach), named debt inside the scope - P3b/P4b rekeys the registry. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif if (auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get())) { backendTextureObject = *backendTextureSlot; } else { @@ -9093,6 +9162,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } } else if (attachmentObject.IsRenderbuffer()) { const auto& renderbufferObject = attachmentObject.GetRenderbuffer(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on + // a first attach), named debt inside the scope - P3b/P4b rekeys the registry. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif SharedPtr backendRenderbufferObject; if (auto* backendRenderbufferSlot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) { @@ -9405,7 +9479,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", @@ -9477,6 +9560,12 @@ namespace MobileGL::MG_Backend::DirectGLES { surface.Res.Slot, surface.Res.Gen); return false; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the corroborating cross-check below resolves the + // frontend renderbuffer's handle by frontend identity - named debt inside the + // scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif if (!(RenderbufferImpl::g_backendRenderbufferObjects.HandleOf(renderbufferObject.get()) == surface.Res)) { MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} but the frontend " @@ -9620,7 +9709,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 @@ -9848,7 +9945,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 " @@ -10126,6 +10234,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // Verify that the backend object's name and parameters match the frontend attachment state if (attachmentObject.IsTexture()) { const auto& textureObject = attachmentObject.GetTexture(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named + // debt inside the scope - P3b/P4b rekeys the registry. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get()); MOBILEGL_ASSERT(backendTextureSlot != nullptr && *backendTextureSlot != nullptr, "No backend texture found while framebuffer reports texture attachment."); @@ -10142,6 +10255,11 @@ namespace MobileGL::MG_Backend::DirectGLES { "Attachment texture level mismatch between GLES and state object."); } else if (attachmentObject.IsRenderbuffer()) { const auto& renderbufferObject = attachmentObject.GetRenderbuffer(); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named + // debt inside the scope - P3b/P4b rekeys the registry. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif auto* backendRboSlot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get()); MOBILEGL_ASSERT( @@ -12682,6 +12800,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // stateProgramObject. The verify build is where the codec is exercised, by // serialising, deserialising and field-comparing before storing. if (ProgramSubsystemEnabled()) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the program's ShaderCso handle is resolved by + // frontend identity below - frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif // MONOLITH GLUE: the ShaderCso handle of a program this backend still arrives // holding. The reader hides the composite band, so a program-pipeline composite // - which the server must never learn is one - resolves through the same call. @@ -13000,6 +13124,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // the values are taken from the object, which in monolith are the very // values the client content-addressed, so the picture stays right while // the gap is visible rather than silent. +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the registration probe below resolves the + // sampler's handle by frontend identity - frontend-keyed twin resolution, + // named debt inside the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif if (!MG_Pipe::MGPipeHandleIsNull( g_backendSamplerObjects.HandleOf(stateSamplerObject.get()))) { MGLOG_E_ONCE("MGPipe: sampler %u was synced without its unit's SamplerCso handle, so " @@ -13291,6 +13421,12 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_PIPE_PUSH const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr; if (TextureResourceSubsystemEnabled()) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §5.4): the renderbuffer's handle is resolved by + // frontend identity below - frontend-keyed twin resolution, named debt inside + // the scope - P3b/P4b rekeys the registry onto handles. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif // MONOLITH GLUE, named as such: the Renderbuffer handle of an object this // backend still arrives holding. Under a real split it rides in the payload. const MG_Pipe::MGPipeHandle res = g_backendRenderbufferObjects.HandleOf(stateRBOObject.get()); 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 e3f603b7..e40317d0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -40,6 +40,9 @@ #if MOBILEGL_BUILD_DISAGGREGATED // P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on. #include +#include +// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope. +#include #endif #include #include @@ -672,6 +675,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; @@ -4522,6 +4542,17 @@ void main() { } void VulkanRenderer::ShutdownBlitResources() { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §3.1's named exemption): the hidden blit program and samplers + // are FRONTEND objects the server backend created on the apply thread, and under an + // active transport they die here on that same thread. Their destructors run the client + // death helper, whose lifetime-id probe is the frontend-keyed registry family - it + // resolves to nothing (no handle was ever minted for these objects) and routes no + // delete, so the scope admits the probe as named debt rather than letting the guard + // Fatal at server teardown. P7 gives these resources storage that is not a frontend + // object. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif m_blitResources = {}; } @@ -4596,6 +4627,12 @@ void main() { } void VulkanRenderer::ShutdownDepthMipmapResources() { +#if MOBILEGL_BUILD_DISAGGREGATED + // Same shape as ShutdownBlitResources above: the hidden depth-mipmap program is a + // frontend object created and destroyed by the server backend on the apply thread, and + // its destructor's lifetime-id probe is admitted here as named debt. + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; +#endif m_depthMipmapResources = {}; } @@ -8928,6 +8965,53 @@ void main() { void VulkanRenderer::BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §3.3/§5.4): MAGMA'S NAMED ARM. A blit record whose + // ReadFbo/DrawFbo are non-null is a glBlitNamedFramebuffer: both framebuffers resolve + // from the record's handles, and the sink is told the pair was consumed - a backend + // that leaves the flag clear has no named arm and the verb declines there, loudly. + // Magma has no FBO twin registry (it reads the frontend object wherever it syncs), so + // the resolution here is frontend-keyed - the handle was minted over the frontend + // object's lifetime id, and the two probes below (the client allocator's slot entry + // and the frontend context's framebuffer pool) are named debt inside the scope, the + // same shape as Espryt's StateForHandle arm: P3b/P4b retire it by carrying the object + // identity in the record. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + auto& applierState = MG_Pipe::MGPipeApplier(); + const MG_Pipe::MGPipeHandle readHandle = applierState.VerbBlitReadFbo; + const MG_Pipe::MGPipeHandle drawHandle = applierState.VerbBlitDrawFbo; + if (!MG_Pipe::MGPipeHandleIsNull(readHandle) || !MG_Pipe::MGPipeHandleIsNull(drawHandle)) { + const auto resolveEndpoint = [](MG_Pipe::MGPipeHandle handle) + -> SharedPtr { + const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; + if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) { + return MG_State::pGLContext ? MG_State::pGLContext->GetFramebufferObject(0) : nullptr; + } + if (!MG_Pipe::MGPipeSlots().IsLive(MG_Pipe::MGPipeKind::Framebuffer, handle)) { + return nullptr; + } + const Uint64 lifetimeId = + MG_Pipe::MGPipeSlots().LifetimeIdOfSlot(MG_Pipe::MGPipeKind::Framebuffer, handle.Slot); + if (lifetimeId == 0 || MG_State::pGLContext == nullptr) return nullptr; + return MG_State::pGLContext->FindFramebufferObjectByLifetimeId(lifetimeId); + }; + auto readFbo = resolveEndpoint(readHandle); + auto drawFbo = resolveEndpoint(drawHandle); + if (!readFbo || !drawFbo) { + // Leave the pair UNCONSUMED: the sink's decline is the loud answer a + // missing endpoint deserves, not a silent blit of whatever is bound. + MGLOG_E_ONCE("MGPipe: Magma's named blit could not resolve an endpoint (read {%u, %u}, draw " + "{%u, %u}) to a frontend framebuffer; the verb declines at the sink", + readHandle.Slot, readHandle.Gen, drawHandle.Slot, drawHandle.Gen); + return; + } + applierState.VerbBlitNamedConsumed = true; + BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, + dstX1, dstY1, mask, filter); + return; + } + } +#endif auto readFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); auto drawFbo = MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); BlitNamedFramebuffer(readFbo, drawFbo, srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); diff --git a/MobileGL/MG_Impl/Pipe/ResourceTracker.h b/MobileGL/MG_Impl/Pipe/ResourceTracker.h index 4da14a39..48f3b4e6 100644 --- a/MobileGL/MG_Impl/Pipe/ResourceTracker.h +++ b/MobileGL/MG_Impl/Pipe/ResourceTracker.h @@ -714,8 +714,16 @@ namespace MobileGL::MG_Pipe { bufferObject->MarkGpuWritten(); return; } - const MGPipeHandle res = - MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, bufferObject->GetLifetimeId()); + const MGPipeHandle res = [&]() { +#if MOBILEGL_BUILD_DISAGGREGATED + // CONTRACT-P5C §3.1's named exemption, Magma's half: the binding records that + // would carry this buffer's handle are P4b's to emit and Magma's server-side + // binding table is P7's, so until then the probe runs inside the scope - the + // debt's named, greppable form rather than a silent guard removal. + const MGPipeReverseAnnouncementScope reverseAnnouncement; +#endif + return MGPipeSlots().FindByLifetimeId(MGPipeKind::Buffer, bufferObject->GetLifetimeId()); + }(); if (MGPipeHandleIsNull(res)) { MGLOG_E_ONCE("MGPipe: no handle for the GPU-write announcement of buffer %u - the " "reverse channel is installed but the mint is missing, so the mark " diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index c6940523..397c75ea 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -9,7 +9,67 @@ // 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; + // The named exemption of CONTRACT-P5C §3.1: the sites whose handle-carrying records + // are not emitted yet (P4b/P7) probe read-only inside the scope. The G6 + // frontend-keyed registry family (P3b/P4b) does the same inside its own scope. + if (MGPipeReverseAnnouncementScope::Active()) return; + if (MGPipeFrontendKeyedRegistryScope::Active()) 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(); + } + + namespace { + thread_local Uint32 g_reverseAnnouncementScopeDepth = 0; + } + + MGPipeReverseAnnouncementScope::MGPipeReverseAnnouncementScope() { + ++g_reverseAnnouncementScopeDepth; + } + + MGPipeReverseAnnouncementScope::~MGPipeReverseAnnouncementScope() { + --g_reverseAnnouncementScopeDepth; + } + + Bool MGPipeReverseAnnouncementScope::Active() { + return g_reverseAnnouncementScopeDepth != 0; + } + + namespace { + thread_local Uint32 g_frontendKeyedRegistryScopeDepth = 0; + } + + MGPipeFrontendKeyedRegistryScope::MGPipeFrontendKeyedRegistryScope() { + ++g_frontendKeyedRegistryScopeDepth; + } + + MGPipeFrontendKeyedRegistryScope::~MGPipeFrontendKeyedRegistryScope() { + --g_frontendKeyedRegistryScopeDepth; + } + + Bool MGPipeFrontendKeyedRegistryScope::Active() { + return g_frontendKeyedRegistryScopeDepth != 0; + } +#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 +226,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 +239,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..b30b9450 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -163,4 +163,47 @@ 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); + + // The NAMED EXEMPTION to the rule above (CONTRACT-P5C §3.1): the family of sites whose + // handle-carrying records the client does not EMIT yet. set_shader_buffers and + // set_stream_output_targets exist in the catalogue but are P4b's to emit + // (SetHashSuppressor.h says so), so the buffer binding-point ensures and the GPU-written + // announcement they feed have no record handle to resolve from today. Inside this scope + // ONE read-only lifetime-id probe stays legal; the scope is the debt's measurable, + // greppable form, and it retires with P4b's emission (Espryt) and P7's server-side + // binding table (Magma). Every other apply-thread allocator access stays Fatal. + class MGPipeReverseAnnouncementScope { + public: + MGPipeReverseAnnouncementScope(); + ~MGPipeReverseAnnouncementScope(); + MGPipeReverseAnnouncementScope(const MGPipeReverseAnnouncementScope&) = delete; + MGPipeReverseAnnouncementScope& operator=(const MGPipeReverseAnnouncementScope&) = delete; + static Bool Active(); + }; + + // The SECOND named exemption family (CONTRACT-P5C §5.4): the frontend-keyed twin + // registry (audit row G6). The texture / sampler-view / FBO-legacy HandleOf probes are + // how the server answers "which twin is this frontend object" while the registry is + // keyed by frontend identity - server-PRIVATE state whose rekey onto handles is + // P3b/P4b's, not P5c's. A probe inside this scope stays a read-only, barrier-held debt; + // wrapping a NEW site in it is the greppable act of naming that debt, and an unwrapped + // probe from the apply thread is still Fatal{RoleViolation, "MGPipeSlots"}. + class MGPipeFrontendKeyedRegistryScope { + public: + MGPipeFrontendKeyedRegistryScope(); + ~MGPipeFrontendKeyedRegistryScope(); + MGPipeFrontendKeyedRegistryScope(const MGPipeFrontendKeyedRegistryScope&) = delete; + MGPipeFrontendKeyedRegistryScope& operator=(const MGPipeFrontendKeyedRegistryScope&) = delete; + static Bool Active(); + }; +#endif } // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 6a7f3caf..e15e32fc 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -693,6 +693,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_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 3f4f28f8..34e98dc4 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -1212,6 +1212,12 @@ namespace MobileGL::MG_State { return m_framebufferState.ValidateFramebufferObject(index); } +#if MOBILEGL_BUILD_DISAGGREGATED + SharedPtr GLContext::FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const { + return m_framebufferState.FindFramebufferObjectByLifetimeId(lifetimeId); + } +#endif + // Sampler void GLContext::GenSamplerNames(Uint number, Vector& samplers) { m_samplerState.GenerateNames(number, samplers); diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index b01310c8..079fb662 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -544,6 +544,11 @@ namespace MobileGL { // Framebuffer void GenFramebufferNames(Uint number, Vector& framebuffers); const SharedPtr& GetFramebufferObject(Uint index); +#if MOBILEGL_BUILD_DISAGGREGATED + // See FramebufferState::FindFramebufferObjectByLifetimeId - the named-blit + // consumer's handle-to-frontend resolution on a backend with no FBO twins. + SharedPtr FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const; +#endif BindingSlot& GetFramebufferBindingSlot(FramebufferTarget target); const SharedPtr& CreateFramebufferObject(Uint index); void MarkFramebufferObjectForDeletion(Uint index); diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp index e0177788..6843bdc7 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.cpp @@ -78,4 +78,15 @@ namespace MobileGL::MG_State::GLState { Bool FramebufferState::ValidateFramebufferObject(Uint index) const { return m_framebufferObjects.find(index) != m_framebufferObjects.end(); } + +#if MOBILEGL_BUILD_DISAGGREGATED + SharedPtr FramebufferState::FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const { + for (const auto& [index, framebufferObject] : m_framebufferObjects) { + if (framebufferObject && framebufferObject->GetLifetimeId() == lifetimeId) { + return framebufferObject; + } + } + return nullptr; + } +#endif } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h index 94dd27c4..fbde98c5 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferState.h @@ -25,6 +25,16 @@ namespace MobileGL::MG_State::GLState { Bool ValidateName(Uint index) const; Bool ValidateFramebufferObject(Uint index) const; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5c (G6, CONTRACT-P5C §3.3/§5.4): the reverse of HandleFor() for a server that holds + // only the handle - the named-blit consumer on a backend with no FBO twin registry + // (Magma) resolves the verb's ReadFbo/DrawFbo to the frontend object by the lifetime + // id the handle was minted over. Named debt, reached inside + // MGPipeFrontendKeyedRegistryScope; P3b/P4b retire it by carrying the object identity + // in the record. nullptr when no live framebuffer owns the id. + SharedPtr FindFramebufferObjectByLifetimeId(Uint64 lifetimeId) const; +#endif + #if MOBILEGL_PIPE_PUSH // P2 brief D4: "did the attachment set or the default geometry of ANY framebuffer // move". It does NOT cover a BIND - a bind writes a BindingSlot, not the object - so 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'),