diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index f1a4f256..2efc3c0a 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -666,6 +666,14 @@ namespace MobileGL { (void)region; return true; } + // IMPLICIT SYNC BY FLUSH (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the session + // on this (the apply) thread ends a frame at a glFlush. Every shared image it used since + // its last boundary is published as written and read by one fence of everything it + // submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first + // use of an image in a frame waits for that image's pending reads (it may be about to + // overwrite what another session still samples). A backend without shared images has + // nothing to publish. + virtual Bool PublishSharedImageAccesses() { return true; } // ---- shared images, CLIENT side (BackendObject_Remote implements these) ---------- // @@ -711,6 +719,10 @@ namespace MobileGL { (void)textureLifetimeId, (void)id; return false; } + // In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server, + // returning once the fence is published. Skipped (true) when nothing reached the stream + // since the last one. + virtual Bool FlushSharedImageAccesses() { return false; } virtual void Initialize() = 0; virtual Bool InitCapabilities() = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 1a80a8ee..57b3a44d 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1230,6 +1230,14 @@ namespace MobileGL::MG_Backend::DirectGLES { return DirectGLES::SetCurrentDrawDamageRegion(region); } + Bool BackendObject_DirectGLES::PublishSharedImageAccesses() { +#if MOBILEGL_BUILD_DISAGGREGATED + return DirectGLES::SharedImageImpl::PublishPendingAccesses(); +#else + return true; +#endif + } + Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) { return BackendObject::SwapEGLBuffers(dpy, draw); } diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 2e99b6bc..565ffde2 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -51,6 +51,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override; // eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update. Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override; + // A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses). + Bool PublishSharedImageAccesses() override; void ReleaseEGLSurface(EGLSurface surface) override; void ReleaseEGLResources() override; // The server window going and coming back: each window surface on it swaps its native EGL diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index ef5280ae..4fb3a3d3 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -18813,6 +18813,9 @@ namespace MobileGL::MG_Backend::DirectGLES { struct ReadFrame { Uint64 Id = 0; SI::ReadTracker Tracker; + // The session ends frames at publishing flushes (PublishPendingAccesses): it writes + // images, so its first use of one in a frame waits for that image's pending reads. + Bool ImplicitSync = false; }; std::atomic g_readFrameIds{0}; @@ -18840,11 +18843,30 @@ namespace MobileGL::MG_Backend::DirectGLES { CloseFd(fence); ReadFrame& frame = CurrentReadFrame(); if (state.NotedFrame != frame.Id) { + // A session that renders into images (it ends frames with a publishing flush) may be + // about to overwrite this one: what other sessions still sample goes first. + if (frame.ImplicitSync) { + for (const int read : SI::DupPendingReadFences(*state.Image)) WaitForFenceBeforeLaterCommands(read); + } frame.Tracker.NoteRead(state.Image); state.NotedFrame = frame.Id; } } + Bool PublishPendingAccesses() { + ReadFrame& frame = CurrentReadFrame(); + frame.ImplicitSync = true; + if (frame.Tracker.Pending() == 0) return true; + int fence = -1; + if (t_boundNativeContext != EGL_NO_CONTEXT && !ExportFence(&fence)) { + // No fence to publish: the work is waited out here, and published as completed. + (void)WaitForCompletion(); + } + frame.Tracker.PublishFrameAsWrite(fence); + frame.Id = g_readFrameIds.fetch_add(1, std::memory_order_relaxed) + 1; + return true; + } + void PublishPendingReads() { ReadFrame& frame = CurrentReadFrame(); if (frame.Tracker.Pending() == 0) return; @@ -19035,6 +19057,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } void AcquireForSampling(const EglImageRef& image) { (void)image; } void PublishPendingReads() {} + Bool PublishPendingAccesses() { return true; } void ForgetPresentTargetsAfterTerminate() {} #endif } // namespace SharedImageImpl diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index d955bf39..68892b49 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -546,6 +546,11 @@ namespace MobileGL::MG_Backend::DirectGLES { // at Present and wherever the current native context stops being current (its reads can // only be fenced from it). Nothing noted = nothing done. void PublishPendingReads(); + // IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): everything the + // calling session used since its last boundary is published as written and read by one + // fence, and the session's first use of an image in each frame from now on also waits for + // the image's pending reads. + Bool PublishPendingAccesses(); } // namespace SharedImageImpl #endif diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index d36d65b5..7cccee83 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -585,6 +585,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { #endif } + Bool BackendObject_DirectVulkan::PublishSharedImageAccesses() { +#if MOBILEGL_BUILD_DISAGGREGATED + const std::lock_guard lock(m_eglStateMutex); + return pVulkanRenderer ? pVulkanRenderer->PublishSharedImageAccesses() : true; +#else + return true; +#endif + } + // A swapchain image keeps its content through a present whatever region is drawn next: the // damage region the client may declare is not needed here. Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge(Bool damageRegionFollows) { diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 80c57d75..26563751 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -42,6 +42,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { const MG_Util::Damage::Region& region) override; // The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge). Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override; + // A glFlush that ends a shared-image producer's frame (VulkanRenderer::PublishSharedImageAccesses). + Bool PublishSharedImageAccesses() override; void ReleaseEGLSurface(EGLSurface surface) override; void ReleaseEGLResources() override; // The server window going and coming back: the window surface's target (VkSurfaceKHR, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index bc92bc7a..0fa6e241 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -191,6 +191,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // renderer has written before - the rest of it is left as it is; a first write, a scaled // or turned copy copies all of it. Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, const MG_Util::Damage::Region& region); + // IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): every image acquired + // since the last boundary goes back to the foreign family, the recording is submitted with + // the exportable semaphore, and its sync_file is published to each as a write and a read. + // From then on an image's first acquire per frame also waits for its pending reads. + Bool PublishSharedImageAccesses(); // EGL_BUFFER_AGE_EXT of the active target's default framebuffer for the next frame: the age // of the swapchain image acquired for it (SwapchainObject::BufferAgeOf). Asking makes the // target keep its presented images' content from then on. @@ -721,6 +726,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // m_sharedImageSyncFd: the device imports and exports SYNC_FD binary semaphores; without // it the waits happen on the CPU and nothing is exported (see WireSharedImage.inc). Bool m_sharedImageSyncFd = false; + // The session ends frames at publishing flushes (PublishSharedImageAccesses): it writes the + // images it acquires, so a first acquire waits for the image's pending reads as well. + Bool m_sharedImageImplicitSync = false; PFN_vkImportSemaphoreFdKHR m_importSemaphoreFd = nullptr; PFN_vkGetSemaphoreFdKHR m_getSemaphoreFd = nullptr; struct SharedImageWait { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc index 55d23e58..43df6dfb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc @@ -517,6 +517,11 @@ // The write's fence gates the submission carrying the acquire: this recording's, or an // earlier one if a flush takes it first - either way ahead of every later command. StageSharedImageWait(fence, kSharedImageUseStages); + // A session that writes images (implicit sync by flush) may be about to overwrite what + // another session still samples: those reads go first. + if (m_sharedImageImplicitSync && !resource.sharedImageHeld) { + for (const int read : SI::DupPendingReadFences(*image)) StageSharedImageWait(read, kSharedImageUseStages); + } const Uint32 family = static_cast(m_physicalDevice.queueFamilies.graphicsFamily); VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; @@ -595,6 +600,31 @@ static_cast(releases.size()), releases.data()); } + Bool VulkanRenderer::PublishSharedImageAccesses() { + m_sharedImageImplicitSync = true; + if (m_sharedImageReads.Pending() == 0 && m_sharedImagesHeld.empty()) return true; + ReleaseHeldSharedImages(); + if (m_sharedImageSyncFd && m_sharedImageSignal == VK_NULL_HANDLE) + m_sharedImageSignal = TakeSharedImageSemaphore(/*exportable=*/true); + m_sharedImageSignalSubmitted = VK_NULL_HANDLE; + if (!FlushPendingCommands()) { + MGLOG_E_ONCE("Magma: a flush's shared-image accesses could not be submitted"); + m_sharedImageReads.PublishFrame(-1); // drops the references; nothing to order against + return false; + } + const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE); + int fence = -1; + if (signal == VK_NULL_HANDLE || !ExportSharedImageSignal(signal, fence)) { + // No fence came out of it: the submission is waited for and published as completed. + fence = -1; + if (!WaitForSubmitsUpTo(m_submitCounter, UINT64_MAX)) { + MGLOG_E_ONCE("Magma: a flush's shared-image accesses could not be waited for"); + } + } + m_sharedImageReads.PublishFrameAsWrite(fence); + return true; + } + void VulkanRenderer::PublishSharedImageReads() { const VkSemaphore signal = std::exchange(m_sharedImageSignalSubmitted, VK_NULL_HANDLE); int fence = -1; diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index d893c117..6a291907 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -8,6 +8,7 @@ #include "EGLImpl.h" #include "EGLPlatformExtensions.h" +#include "SharedImageFlushPolicy.h" #include "WaylandWindow.h" #include "../GetProcAddress.h" #include @@ -32,6 +33,8 @@ #include #include #endif +#include +#include #include #include #include @@ -1433,6 +1436,34 @@ namespace MobileGL::MG_Impl::EGLImpl { return true; } + namespace { + std::atomic& SharedImageBoundToTexture() { + static std::atomic bound{false}; + return bound; + } + } // namespace + + void NoteSharedImageBoundToTexture() { SharedImageBoundToTexture().store(true, std::memory_order_relaxed); } + + void FlushSharedImageAccesses() { + if (!SharedImageBoundToTexture().load(std::memory_order_relaxed) && + std::getenv("MOBILEGL_SHARED_IMAGE_FLUSH_SYNC") == nullptr) + return; +#if MOBILEGL_BUILD_DISAGGREGATED + static const Bool ownsServerWindow = MG_Config::ServerOwnedWindowSurfaces(); +#else + constexpr Bool ownsServerWindow = false; +#endif + if (!FlushPublishesSharedImageAccesses(SharedImagesAvailable(), SharedImageBoundToTexture().load(), + ownsServerWindow, std::getenv("MOBILEGL_SHARED_IMAGE_FLUSH_SYNC"))) + return; + auto* backendObject = MG_Backend::pActiveBackendObject.get(); + if (backendObject != nullptr && !backendObject->FlushSharedImageAccesses()) { + MGLOG_E_ONCE("glFlush: this session's shared-image accesses could not be published; another session may " + "read them early"); + } + } + EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib* attrib_list) { auto* state = GetState(); diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h index 6cb06857..e0a0a5fe 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h @@ -68,6 +68,12 @@ namespace MobileGL::MG_Impl::EGLImpl { Bool SharedImagesAvailable(); // The shared image an EGLImage names (EGL_LINUX_DMA_BUF_EXT), for glEGLImageTargetTexture2DOES. Bool LookupSharedImage(EGLImage image, Uint64* id, EGLint* width, EGLint* height); + // glEGLImageTargetTexture2DOES bound a shared image to a texture: from now on glFlush/glFinish + // may be shared-image boundaries (SharedImageFlushPolicy.h). + void NoteSharedImageBoundToTexture(); + // From glFlush/glFinish, under the stream lock: publish this session's shared-image accesses + // when the policy says a flush is a boundary in this process. Cheap when it is not. + void FlushSharedImageAccesses(); EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list); EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window, const EGLAttrib* attrib_list); diff --git a/MobileGL/MG_Impl/EGLImpl/SharedImageFlushPolicy.h b/MobileGL/MG_Impl/EGLImpl/SharedImageFlushPolicy.h new file mode 100644 index 00000000..16e725e4 --- /dev/null +++ b/MobileGL/MG_Impl/EGLImpl/SharedImageFlushPolicy.h @@ -0,0 +1,42 @@ +// MobileGL - MobileGL/MG_Impl/EGLImpl/SharedImageFlushPolicy.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once + +// WHEN glFlush IS A SHARED-IMAGE BOUNDARY (docs/Disaggregated/notes/anland/plan-x11-gpu.md). +// +// A dma-buf producer that renders into its buffers and hands them on after a glFlush (an X +// server's glamor committing a window pixmap) relies on implicit sync: whoever samples the buffer +// next is ordered after the flushed work by the kernel. MobileGL's images are ordered by its server +// instead, and the server learns of a write at a boundary - a swap, a present, or this flush. So a +// flush becomes a round trip that publishes the session's accesses, but only in a process for which +// that can matter: it has bound a shared image to a texture, it does not own the server's window +// (the compositor samples client buffers and ends its frames at its swaps), and nobody turned it +// off. MOBILEGL_SHARED_IMAGE_FLUSH_SYNC=1 forces it on (still only with shared images), =0 off. +// +// Header-only and pure: a host test pins the table. + +#include + +#include + +namespace MobileGL::MG_Impl::EGLImpl { + inline Bool FlushPublishesSharedImageAccesses(Bool sharedImagesAvailable, Bool boundSharedImageToTexture, + Bool ownsServerWindow, const char* overrideValue) { + if (!sharedImagesAvailable) return false; + if (overrideValue != nullptr && overrideValue[0] != '\0') { + if (std::strcmp(overrideValue, "0") == 0 || std::strcmp(overrideValue, "off") == 0 || + std::strcmp(overrideValue, "false") == 0) + return false; + if (std::strcmp(overrideValue, "1") == 0 || std::strcmp(overrideValue, "on") == 0 || + std::strcmp(overrideValue, "true") == 0) + return true; + } + return boundSharedImageToTexture && !ownsServerWindow; + } +} // namespace MobileGL::MG_Impl::EGLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index e19d92a9..653cf4a2 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -123,6 +123,8 @@ MOBILEGL_GL_API void glFinish() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope; #if MOBILEGL_BUILD_DISAGGREGATED + // A shared-image producer's boundary first (implicit sync; EGLImpl/SharedImageFlushPolicy.h). + MobileGL::MG_Impl::EGLImpl::FlushSharedImageAccesses(); MobileGL::MG_Pipe::MGPipeClientFinish(); #endif } @@ -130,6 +132,7 @@ MOBILEGL_GL_API void glFlush() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope; #if MOBILEGL_BUILD_DISAGGREGATED + MobileGL::MG_Impl::EGLImpl::FlushSharedImageAccesses(); MobileGL::MG_Pipe::MGPipeClientFlush(); #endif } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 2f64b75a..669e8ea5 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -7178,7 +7178,9 @@ namespace MobileGL::MG_Impl::GLImpl { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, "the image could not be bound.")); + return; } + MG_Impl::EGLImpl::NoteSharedImageBoundToTexture(); } void TexImage1D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLint border, GLenum format, diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 267cb948..fe9a3d78 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -2013,6 +2013,9 @@ namespace MobileGL::MG_Pipe { // (EGL_KHR_partial_update) before it draws. // SetDamageRegion eglSetDamageRegionKHR for the current draw surface: Damage is the region // the frame is about to draw (GL window coordinates; 0 rectangles = all of it). + // Flush glFlush/glFinish of a session that renders INTO images (an X server's glamor): + // every image the session used since its last boundary is published as written + // and read by the flush's fence (implicit sync); answered once published. // // A Present carries the region of the frame to copy (DamageCount rectangles of Damage, GL // window coordinates, origin bottom-left; 0 = all of it): the client keeps per image what the @@ -2028,6 +2031,7 @@ namespace MobileGL::MG_Pipe { kMGPSharedImageAttach = 5, kMGPSharedImageQueryBufferAge = 6, kMGPSharedImageSetDamageRegion = 7, + kMGPSharedImageFlush = 8, }; inline constexpr Uint32 kMGPBufferAgeDamageRegionFollows = 1u; diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp index a9e89590..7da3ee71 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.cpp @@ -684,6 +684,17 @@ namespace MobileGL::MG_Remote::Client { return session->EmitSharedImage(op, -1, nullptr, nullptr); } + Bool BackendObject_Remote::FlushSharedImageAccesses() { + ClientSession* session = ClientSession::Active(); + if (session == nullptr || !session->Started()) return false; + if (m_sharedImageFlushedAt != 0 && session->LastPublishedSeq() == m_sharedImageFlushedAt) return true; + MG_Pipe::MGPSharedImageOp op{}; + op.Op = MG_Pipe::kMGPSharedImageFlush; + const Bool published = session->EmitSharedImage(op, -1, nullptr, nullptr); + m_sharedImageFlushedAt = session->LastPublishedSeq(); + return published; + } + // NO strong CreateRemoteBackendObject() lives here, and the reason is a link fact, not an // oversight. v1's Init.cpp calls MG_Remote::Client::CreateRemoteBackendObject() and ships a // __attribute__((weak)) placeholder for it beside ServerLoop that aborts by name; its comment diff --git a/MobileGL/MG_Remote/Client/BackendObject_Remote.h b/MobileGL/MG_Remote/Client/BackendObject_Remote.h index b06c9834..2b73a891 100644 --- a/MobileGL/MG_Remote/Client/BackendObject_Remote.h +++ b/MobileGL/MG_Remote/Client/BackendObject_Remote.h @@ -94,6 +94,7 @@ namespace MobileGL::MG_Remote::Client { Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) override; Bool SetDamageRegion(const MG_Util::Damage::Region& region) override; Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) override; + Bool FlushSharedImageAccesses() override; // Copies the caps mirror's FormatCapabilityCache into the base class's // m_formatCapabilities. Public because CapsMirror's adoption hook is a free function @@ -122,6 +123,10 @@ namespace MobileGL::MG_Remote::Client { // geometry adopted before it returns, and every refusal named in this process's log. Bool CreateServerOwnedWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle); + // The stream position right after the last shared-image Flush record: a flush with nothing + // published since has nothing new to fence and costs no round trip. + Uint64 m_sharedImageFlushedAt = 0; + // The generation of the snapshot m_formatCapabilities was filled from. Exposed only // through the log line on a refresh: a cache that silently stopped tracking the mirror // is exactly the shape trap 2 exists to prevent. diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 659d1ceb..790e9e57 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -1586,6 +1586,13 @@ namespace MobileGL::MG_Remote::Server { if (backend == nullptr) return false; return backend->SetCurrentDamageRegion(DamageOfRecord(op.DamageCount, op.Damage)); } + case MG_Pipe::kMGPSharedImageFlush: { + // The answer is posted after the fence is published: the client commits the buffer to + // its compositor only after this returns, so the compositor's session finds the write. + MG_Backend::BackendObject* backend = ServerLoopInstance().Backend(); + if (backend == nullptr) return false; + return backend->PublishSharedImageAccesses(); + } case MG_Pipe::kMGPSharedImageAttach: { SI::ImageRef image = holder.Get(op.ImageId); if (image == nullptr) return false; diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp index d6010906..051bd91f 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.cpp @@ -394,6 +394,15 @@ namespace MobileGL::MG_Remote::Server::SharedImages { m_images.clear(); } + void ReadTracker::PublishFrameAsWrite(int fence) { + for (const auto& [id, image] : m_images) { + PublishWrite(*image, Dup(fence)); + PublishRead(*image, Dup(fence)); + } + CloseFd(fence); + m_images.clear(); + } + void SessionHolder::Hold(const ImageRef& image) { if (image == nullptr) return; const std::lock_guard lock(m_mutex); diff --git a/MobileGL/MG_Remote/Server/SharedImageRegistry.h b/MobileGL/MG_Remote/Server/SharedImageRegistry.h index dfd28c04..08c87253 100644 --- a/MobileGL/MG_Remote/Server/SharedImageRegistry.h +++ b/MobileGL/MG_Remote/Server/SharedImageRegistry.h @@ -134,6 +134,12 @@ namespace MobileGL::MG_Remote::Server::SharedImages { void NoteRead(const ImageRef& image); // Takes ownership of `fence` (a sync_file for the frame, -1 if it has completed). void PublishFrame(int fence); + // IMPLICIT SYNC BY FLUSH: a session that renders into images as well as sampling them, with + // no swap and no explicit write (an X server's glamor) ends its frame at glFlush. Every image + // it used since its last boundary gets `fence` both as a write - the generation moves, so the + // next reader waits for it - and as a read, so the next writer waits for it too. Takes + // ownership of `fence` (-1: the work has completed; the generation still moves). + void PublishFrameAsWrite(int fence); SizeT Pending() const { return m_images.size(); } private: diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index 39d5b04a..8c9ee208 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -70,6 +70,9 @@ if (NOT WIN32) # `eglinfo -B`'s per-API sequence (surfaceless make-current, the ES probe's NULL config) through # a real MobileGL client against a real `--serve` supervisor, as ServerOwnedSurfaceTest does. list(APPEND MOBILEGL_WIRE_TESTS EglInfoSequenceTest) + # Xwayland's glamor against MobileGL's EGL (GBM platform, config-less core context, surfaceless, + # dma-buf import) and the shared_image Flush verb its glFlush sends; a real supervisor again. + list(APPEND MOBILEGL_WIRE_TESTS GlamorEglSequenceTest) # P14 S2 (docs/Disaggregated/design/11-state-ownership.md): several sessions in ONE process. # The in-process unix supervisor serves each session on its own control thread with its own # ServerSession/ServerLoop/PipeInputs block/latch domain/segment resolver; two SHM clients are @@ -109,7 +112,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) ${MGL_ROOT}/3rdparty/flatbuffers/include ) if (test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR - test STREQUAL "EglInfoSequenceTest" OR + test STREQUAL "EglInfoSequenceTest" OR test STREQUAL "GlamorEglSequenceTest" OR test STREQUAL "AdoptTierStreamTest" OR test STREQUAL "MultiSessionTest" OR test STREQUAL "ServerProbeTest" OR test STREQUAL "ClientRecoveryTest" OR test STREQUAL "ConcurrentContextsTest") # It builds records and a program archive by hand, so it reaches MG_Pipe and MG_State @@ -141,7 +144,7 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "EglInfoSequenceTest" OR test STREQUAL "AdoptTierStreamTest" OR - test STREQUAL "ServerProbeTest") + test STREQUAL "ServerProbeTest" OR test STREQUAL "GlamorEglSequenceTest") # It execs the real image, so it must be built first and must be told # where it is. MOBILEGL_TEST_SERVER_PATH is read by the test's # ServerImage() helper; without it the test would depend on the working diff --git a/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp b/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp new file mode 100644 index 00000000..ef338c7f --- /dev/null +++ b/MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp @@ -0,0 +1,265 @@ +// MobileGL - MobileGL/MG_Test/Wire/GlamorEglSequenceTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// XWAYLAND'S GLAMOR AGAINST MOBILEGL'S EGL, END TO END ON THE HOST: a real MobileGL client against +// a real `--serve` supervisor, through what xwl_glamor_gbm_init_egl and glamor_init (Xwayland +// 24.1) require before they take the GPU path: +// +// client extension EGL_MESA_platform_gbm (or _KHR_), eglGetPlatformDisplay(GBM, gbm_device), +// eglInitialize, eglBindAPI(EGL_OPENGL_API), eglCreateContext(EGL_NO_CONFIG_KHR, core 3.1), +// surfaceless eglMakeCurrent, GL_RENDERER not llvmpipe/softpipe, desktop GL >= 2.1, no +// GL_ARB_compatibility (core), GL_ARB_vertex_array_object, GL_OES_EGL_image, +// EGL_EXT_image_dma_buf_import + _modifiers (dmabuf_capable), XRGB8888/ARGB8888 importable, +// eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) of a shared image's descriptor. +// +// And the implicit-sync flush (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the shared_image +// Flush verb the client sends from glFlush is answered by the server. + +#include "P12ServerRig.h" + +#include +#include + +using namespace MobileGL; +using namespace P12; + +namespace { + namespace EGL = MobileGL::MG_Impl::EGLImpl; + + constexpr EGLenum kPlatformGbm = 0x31D7; + constexpr unsigned kGlRenderer = 0x1F01; + constexpr unsigned kGlVersion = 0x1F02; + constexpr unsigned kGlExtensions = 0x1F03; + constexpr unsigned kGlNumExtensions = 0x821D; + constexpr Uint32 kXrgb8888 = 0x34325258u; + constexpr Uint32 kArgb8888 = 0x34325241u; + + struct GlamorReport { + Int32 platformGbmExtension = 0; + Int32 display = 0; + Int32 initialized = 0; + Int32 noConfigCoreContext = 0; + Int32 surfacelessCurrent = 0; + Int32 dmabufCapable = 0; + Int32 desktopGl = 0; + Int32 glVersion = 0; // epoxy_gl_version(): major * 10 + minor + Int32 rendererNotSoftware = 0; + Int32 coreProfile = 0; // no GL_ARB_compatibility + Int32 vertexArrayObject = 0; + Int32 oesEglImage = 0; + Int32 xrgbImportable = 0; + Int32 argbImportable = 0; + Int32 imageCreated = 0; + Int32 flushAnswered = 0; + Int32 flushAgainAnswered = 0; + Int32 eglError = 0; + char renderer[128] = {}; + char note[128] = {}; + }; + + bool HasToken(const char* list, const char* token) { + if (list == nullptr) return false; + const std::string all = std::string(" ") + list + " "; + return all.find(std::string(" ") + token + " ") != std::string::npos; + } + + // epoxy_has_gl_extension on a core context: glGetStringi. + bool HasGlExtension(const char* name) { + using GetIntegervFn = void (*)(unsigned, int*); + using GetStringiFn = const unsigned char* (*)(unsigned, unsigned); + const auto getIntegerv = reinterpret_cast(EGL::GetProcAddress("glGetIntegerv")); + const auto getStringi = reinterpret_cast(EGL::GetProcAddress("glGetStringi")); + if (getIntegerv == nullptr || getStringi == nullptr) return false; + int count = 0; + getIntegerv(kGlNumExtensions, &count); + for (int i = 0; i < count; ++i) { + const unsigned char* ext = getStringi(kGlExtensions, static_cast(i)); + if (ext != nullptr && std::strcmp(reinterpret_cast(ext), name) == 0) return true; + } + return false; + } + + void RunGlamor(GlamorReport& r) { + r.platformGbmExtension = HasToken(EGL::QueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS), "EGL_MESA_platform_gbm") || + HasToken(EGL::QueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS), "EGL_KHR_platform_gbm") + ? 1 + : 0; + // Any gbm_device* will do: this process has no GBM loader to ask its backend's name, and the + // display's frames come from the server, never from the device. + static int fakeGbmDevice[16] = {}; + const EGLDisplay dpy = EGL::GetPlatformDisplay(kPlatformGbm, fakeGbmDevice, nullptr); + r.display = dpy != EGL_NO_DISPLAY ? 1 : 0; + if (!r.display) return; + EGLint major = 0, minor = 0; + r.initialized = EGL::Initialize(dpy, &major, &minor) == EGL_TRUE ? 1 : 0; + if (!r.initialized) return; + const char* exts = EGL::QueryString(dpy, EGL_EXTENSIONS); + r.dmabufCapable = + HasToken(exts, "EGL_EXT_image_dma_buf_import") && HasToken(exts, "EGL_EXT_image_dma_buf_import_modifiers"); + + (void)EGL::BindAPI(EGL_OPENGL_API); + const EGLint core31[] = {EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE}; + const EGLContext ctx = EGL::CreateContext(dpy, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT, core31); + r.noConfigCoreContext = ctx != EGL_NO_CONTEXT ? 1 : 0; + if (!r.noConfigCoreContext) { + r.eglError = EGL::GetError(); + return; + } + r.surfacelessCurrent = EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, ctx) == EGL_TRUE ? 1 : 0; + if (!r.surfacelessCurrent) { + r.eglError = EGL::GetError(); + return; + } + + using GetStringFn = const unsigned char* (*)(unsigned); + const auto getString = reinterpret_cast(EGL::GetProcAddress("glGetString")); + const char* renderer = getString ? reinterpret_cast(getString(kGlRenderer)) : nullptr; + const char* version = getString ? reinterpret_cast(getString(kGlVersion)) : nullptr; + std::snprintf(r.renderer, sizeof(r.renderer), "%s", renderer ? renderer : ""); + // xwl_glamor_gbm_init_egl's refusals. + r.rendererNotSoftware = renderer != nullptr && std::strncmp(renderer, "llvmpipe", 8) != 0 && + std::strstr(renderer, "softpipe") == nullptr; + // epoxy_is_desktop_gl / epoxy_gl_version. + if (version != nullptr) { + r.desktopGl = std::strncmp(version, "OpenGL ES", 9) != 0; + int vmaj = 0, vmin = 0; + if (std::sscanf(version, "%d.%d", &vmaj, &vmin) == 2) r.glVersion = vmaj * 10 + vmin; + } + r.coreProfile = !HasGlExtension("GL_ARB_compatibility"); + r.vertexArrayObject = HasGlExtension("GL_ARB_vertex_array_object") || HasGlExtension("GL_OES_vertex_array_object"); + r.oesEglImage = HasGlExtension("GL_OES_EGL_image"); + + EGLint formats[16] = {}; + EGLint count = 0; + if (EGL::QueryDmaBufFormats(dpy, 16, formats, &count)) { + for (EGLint i = 0; i < count; ++i) { + r.xrgbImportable |= static_cast(formats[i]) == kXrgb8888; + r.argbImportable |= static_cast(formats[i]) == kArgb8888; + } + } + + // A window pixmap: a shared image (what gbm_bo_create gives glamor), bound through + // EGL_LINUX_DMA_BUF_EXT the way xwl_glamor_gbm_create_pixmap_for_bo does. + auto* backend = MG_Backend::pActiveBackendObject.get(); + MG_Backend::SharedImageExport image; + if (backend != nullptr && backend->AllocateSharedImage(64, 32, kXrgb8888, &image) && image.Fd >= 0) { + const EGLAttrib attribs[] = {EGL_WIDTH, 64, EGL_HEIGHT, 32, EGL_LINUX_DRM_FOURCC_EXT, kXrgb8888, + EGL_DMA_BUF_PLANE0_FD_EXT, image.Fd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, + static_cast(image.Offset), EGL_DMA_BUF_PLANE0_PITCH_EXT, + static_cast(image.Stride), EGL_NONE}; + const EGLImage eglImage = EGL::CreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs); + r.imageCreated = eglImage != EGL_NO_IMAGE ? 1 : 0; + if (eglImage != EGL_NO_IMAGE) (void)EGL::DestroyImage(dpy, eglImage); + ::close(image.Fd); + (void)backend->ReleaseSharedImage(image.Id); + } + + // glamor's block handler: glFlush, which in an X server is where its renders become visible + // to the compositor. The verb must be answered (an unknown verb is refused). + using ClearFn = void (*)(unsigned); + if (const auto clear = reinterpret_cast(EGL::GetProcAddress("glClear"))) clear(0x4000); + r.flushAnswered = backend != nullptr && backend->FlushSharedImageAccesses() ? 1 : 0; + // Nothing reached the stream since: no round trip, still a success. + r.flushAgainAnswered = backend != nullptr && backend->FlushSharedImageAccesses() ? 1 : 0; + + (void)EGL::MakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + (void)EGL::DestroyContext(dpy, ctx); + (void)EGL::Terminate(dpy); + } + + // The supervisor on an abstract unix socket, as the device's is: shared images travel as + // descriptors on its aux socket, which a TCP endpoint does not have. + bool LaunchUnixSupervisor(ServerProcess* server) { + server->logBase = "/tmp/mgl-p12-glamor-" + std::to_string(::getpid()) + ".log"; + ::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1); + PinHeadlessEgl(); + // The listener takes the native address; the client's selector carries the transport prefix. + const std::string address = "@mgl-glamor-test-" + std::to_string(::getpid()); + server->endpoint = "unix:" + address; + Debug::TruncateRoleLogs(server->logBase.c_str()); + Remote::Server::LaunchedServer launched; + if (Remote::Server::LaunchServerWithArgs(ServerImage(), address, {"--serve"}, &launched) != MOBILEGL_OK) + return false; + server->pid = launched.pid; + return WaitFor([&] { return server->Log().find("listening on") != std::string::npos; }, 10000); + } + + GlamorReport RunGlamorPeer(const ServerProcess& server, const std::string& clientLogBase) { + GlamorReport report{}; + int fds[2] = {-1, -1}; + if (::pipe(fds) != 0) return report; + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid == 0) { + ::close(fds[0]); + ::setenv("MOBILEGL_LOG_FILE_PATH", clientLogBase.c_str(), 1); + ::setenv("MOBILEGL_TRANSPORT", "spawn", 1); + ::setenv("MOBILEGL_IPC_CONTROL", server.endpoint.c_str(), 1); + ::setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES", 1); + ::setenv("MOBILEGL_IPC_SURFACE", "offscreen", 1); + GlamorReport r{}; + RunGlamor(r); + const ssize_t wrote = ::write(fds[1], &r, sizeof(r)); + (void)wrote; + std::fflush(nullptr); + ::_exit(0); + } + ::close(fds[1]); + if (pid < 0) { + ::close(fds[0]); + return report; + } + pollfd pfd{fds[0], POLLIN, 0}; + if (::poll(&pfd, 1, 90000) > 0) { + if (::read(fds[0], &report, sizeof(report)) != static_cast(sizeof(report))) { + report = GlamorReport{}; + std::snprintf(report.note, sizeof(report.note), "peer died before reporting"); + } + } else { + std::snprintf(report.note, sizeof(report.note), "peer timed out"); + ::kill(pid, SIGKILL); + } + ::close(fds[0]); + int status = 0; + ::waitpid(pid, &status, 0); + return report; + } +} // namespace + +TEST(GlamorEglSequence, XwaylandFindsEverythingGlamorNeeds) { + ServerProcess server; + ASSERT_TRUE(LaunchUnixSupervisor(&server)) << server.Log(); + const std::string clientLog = "/tmp/mgl-glamor-client-" + std::to_string(::getpid()) + ".log"; + Debug::TruncateRoleLogs(clientLog.c_str()); + const GlamorReport r = RunGlamorPeer(server, clientLog); + ASSERT_EQ(r.display, 1) << r.note; + EXPECT_EQ(r.platformGbmExtension, 1); + ASSERT_EQ(r.initialized, 1); + EXPECT_EQ(r.dmabufCapable, 1) << "glamor would not be dmabuf_capable: no linux-dmabuf window buffers"; + ASSERT_EQ(r.noConfigCoreContext, 1) << "eglCreateContext(EGL_NO_CONFIG_KHR) error 0x" << std::hex << r.eglError; + ASSERT_EQ(r.surfacelessCurrent, 1) << "surfaceless eglMakeCurrent error 0x" << std::hex << r.eglError; + std::printf("[ glamor ] renderer='%s' GL %d.%d\n", r.renderer, r.glVersion / 10, r.glVersion % 10); + EXPECT_EQ(r.rendererNotSoftware, 1) << r.renderer; + EXPECT_EQ(r.desktopGl, 1); + EXPECT_GE(r.glVersion, 21); + EXPECT_EQ(r.coreProfile, 1); + EXPECT_EQ(r.vertexArrayObject, 1); + EXPECT_EQ(r.oesEglImage, 1); + EXPECT_EQ(r.xrgbImportable, 1); + EXPECT_EQ(r.argbImportable, 1); + EXPECT_EQ(r.imageCreated, 1) << "a shared image's descriptor did not import as an EGLImage"; + EXPECT_EQ(r.flushAnswered, 1) << "the shared_image Flush verb was not answered"; + EXPECT_EQ(r.flushAgainAnswered, 1); + std::error_code ec; + if (!::testing::Test::HasFailure()) { + std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Client), ec); + std::filesystem::remove(Debug::RoleLogPath(clientLog.c_str(), Debug::LogRole::Server), ec); + std::filesystem::remove(clientLog, ec); + } +} diff --git a/MobileGL/MG_Test/Wire/SharedImageTest.cpp b/MobileGL/MG_Test/Wire/SharedImageTest.cpp index ce89b9e4..502bf46b 100644 --- a/MobileGL/MG_Test/Wire/SharedImageTest.cpp +++ b/MobileGL/MG_Test/Wire/SharedImageTest.cpp @@ -255,3 +255,43 @@ TEST(SharedImageSync, AReadTrackerPublishesOneFrameToEveryImageItRead) { EXPECT_TRUE(SI::WaitForReads(*a, 10)); EXPECT_TRUE(SI::WaitForReads(*b, 10)); } + +// Implicit sync by flush (glamor): what a session used since its last boundary is published as a +// write AND a read - a compositor sampling an X window's pixmap waits for the flush's fence, and the +// next writer of that pixmap (or of a client buffer the X server copied from) waits for it too. +TEST(SharedImageSync, AFlushPublishesEveryUsedImageAsAWriteAndARead) { + std::string why; + SI::ImageRef windowPixmap = SI::Allocate(8, 8, SI::kFourccXrgb8888, why); + SI::ImageRef clientBuffer = SI::Allocate(8, 8, SI::kFourccXrgb8888, why); + SI::ImageRef untouched = SI::Allocate(8, 8, SI::kFourccXrgb8888, why); + ASSERT_TRUE(windowPixmap && clientBuffer && untouched) << why; + SI::ReadTracker frame; + frame.NoteRead(windowPixmap); + frame.NoteRead(clientBuffer); + FakeFence flush; + frame.PublishFrameAsWrite(flush.Dup()); + EXPECT_EQ(frame.Pending(), 0u); + + Uint64 generation = 0; + for (const SI::ImageRef& image : {windowPixmap, clientBuffer}) { + const int write = SI::DupWriteFence(*image, &generation); + EXPECT_EQ(generation, 1u) << "a reader must see the flush as a new write"; + ASSERT_GE(write, 0); + EXPECT_FALSE(SI::SyncFile::Signaled(write)); + ::close(write); + EXPECT_FALSE(SI::WaitForReads(*image, 10)) << "a writer must wait for the flush"; + } + EXPECT_EQ(SI::DupWriteFence(*untouched, &generation), -1); + EXPECT_EQ(generation, 0u); + + flush.Signal(); + EXPECT_EQ(SI::DupWriteFence(*windowPixmap, &generation), -1); + EXPECT_TRUE(SI::WaitForReads(*windowPixmap, 10)); + EXPECT_TRUE(SI::WaitForReads(*clientBuffer, 10)); + + // A completed flush (no fence) still moves the generation. + frame.NoteRead(windowPixmap); + frame.PublishFrameAsWrite(-1); + EXPECT_EQ(SI::DupWriteFence(*windowPixmap, &generation), -1); + EXPECT_EQ(generation, 2u); +}