[Feature] (SharedImage): a glFlush publishes an image producer's accesses, so Xwayland's glamor gets implicit sync

This commit is contained in:
2026-10-03 01:51:20 -04:00
parent 316ffe02d0
commit 08f23543aa
23 changed files with 535 additions and 2 deletions
+12
View File
@@ -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;
+31
View File
@@ -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();
+6
View File
@@ -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,
+4
View File
@@ -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:
+5 -2
View File
@@ -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);
}
}
+40
View File
@@ -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);
}