mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (SharedImage): a glFlush publishes an image producer's accesses, so Xwayland's glamor gets implicit sync
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Uint64> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -585,6 +585,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::PublishSharedImageAccesses() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const std::lock_guard<std::recursive_mutex> 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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
@@ -595,6 +600,31 @@
|
||||
static_cast<Uint32>(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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "EGLImpl.h"
|
||||
#include "EGLPlatformExtensions.h"
|
||||
#include "SharedImageFlushPolicy.h"
|
||||
#include "WaylandWindow.h"
|
||||
#include "../GetProcAddress.h"
|
||||
#include <Init.h>
|
||||
@@ -32,6 +33,8 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <optional>
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
@@ -1433,6 +1436,34 @@ namespace MobileGL::MG_Impl::EGLImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::atomic<Bool>& SharedImageBoundToTexture() {
|
||||
static std::atomic<Bool> 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <Includes.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -7178,7 +7178,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
MG_State::pGLContext->RecordError(
|
||||
ErrorCode::InvalidOperation,
|
||||
MakeUnique<GenericErrorInfo>("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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<std::mutex> lock(m_mutex);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
|
||||
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<GetIntegervFn>(EGL::GetProcAddress("glGetIntegerv"));
|
||||
const auto getStringi = reinterpret_cast<GetStringiFn>(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<unsigned>(i));
|
||||
if (ext != nullptr && std::strcmp(reinterpret_cast<const char*>(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<GetStringFn>(EGL::GetProcAddress("glGetString"));
|
||||
const char* renderer = getString ? reinterpret_cast<const char*>(getString(kGlRenderer)) : nullptr;
|
||||
const char* version = getString ? reinterpret_cast<const char*>(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<Uint32>(formats[i]) == kXrgb8888;
|
||||
r.argbImportable |= static_cast<Uint32>(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<EGLAttrib>(image.Offset), EGL_DMA_BUF_PLANE0_PITCH_EXT,
|
||||
static_cast<EGLAttrib>(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<ClearFn>(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<ssize_t>(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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user