[Feature] (GLX): present windows as shared images through DRI3 and Present, with MIT-SHM readback as the fallback

This commit is contained in:
2026-10-03 01:51:22 -04:00
parent 08f23543aa
commit 198d6feaf3
9 changed files with 1669 additions and 9 deletions
+2
View File
@@ -402,6 +402,8 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
+169 -9
View File
@@ -12,8 +12,11 @@
#include "../EGLImpl/EGLImpl.h"
#include "../EGLImpl/WaylandWindow.h"
#include "../GetProcAddress.h"
#include "X11Present.h"
#include "X11PresentXcb.h"
#include <Init.h>
#include <Config.h>
#include <MG_Backend/BackendObjects.h>
namespace MobileGL::MG_Impl::GLXImpl {
namespace {
@@ -72,6 +75,8 @@ namespace MobileGL::MG_Impl::GLXImpl {
// GLX_EXT_swap_control
constexpr int GLX_SWAP_INTERVAL_EXT = 0x20F1;
constexpr int GLX_MAX_SWAP_INTERVAL_EXT = 0x20F2;
// GLX_EXT_buffer_age
constexpr int GLX_BACK_BUFFER_AGE_EXT = 0x20F4;
// GLX_ARB_create_context / _profile / _no_error
constexpr int GLX_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
constexpr int GLX_CONTEXT_MINOR_VERSION_ARB = 0x2092;
@@ -86,6 +91,15 @@ namespace MobileGL::MG_Impl::GLXImpl {
constexpr const char* kGLXExtensions =
"GLX_ARB_create_context GLX_ARB_create_context_no_error GLX_ARB_create_context_profile "
"GLX_ARB_get_proc_address GLX_EXT_swap_control GLX_MESA_swap_control GLX_SGI_swap_control";
// The server answers its surfaces' buffer age (EGL_EXT_buffer_age) when it has shared images.
constexpr const char* kGLXExtensionsWithBufferAge =
"GLX_ARB_create_context GLX_ARB_create_context_no_error GLX_ARB_create_context_profile "
"GLX_ARB_get_proc_address GLX_EXT_swap_control GLX_MESA_swap_control GLX_SGI_swap_control "
"GLX_EXT_buffer_age";
const char* GLXExtensions() {
return EGLImpl::SharedImagesAvailable() ? kGLXExtensionsWithBufferAge : kGLXExtensions;
}
// ---- Xlib access (dlopen'd at runtime, same convention as the backends;
// libX11 is never a link-time dependency of libMobileGL) ----
@@ -230,6 +244,33 @@ namespace MobileGL::MG_Impl::GLXImpl {
const FBConfigInfo* FBConfig = nullptr;
};
// The server's shared images, as X11Present's chain uses them (the GL stream's verbs: the
// caller holds the stream lock, as every GLX entry point does).
class BackendImageSource final : public X11Present::ImageSource {
public:
Bool Allocate(Uint32 width, Uint32 height, Uint32 fourcc, X11Present::Image* out) override {
auto* backend = MG_Backend::pActiveBackendObject.get();
MG_Backend::SharedImageExport image;
if (backend == nullptr || !backend->AllocateSharedImage(width, height, fourcc, &image)) return false;
out->Id = image.Id;
out->Fd = image.Fd;
out->Width = image.Width;
out->Height = image.Height;
out->Stride = image.Stride;
out->Offset = image.Offset;
out->Fourcc = image.Fourcc;
out->Modifier = image.Modifier;
return true;
}
void Release(Uint64 id) override {
if (auto* backend = MG_Backend::pActiveBackendObject.get()) (void)backend->ReleaseSharedImage(id);
}
Bool CopyFrame(Uint64 id, const MG_Util::Damage::Region& region) override {
auto* backend = MG_Backend::pActiveBackendObject.get();
return backend != nullptr && backend->PresentToSharedImage(id, region);
}
};
struct DrawableSurface {
EGLDisplay Display = EGL_NO_DISPLAY;
EGLSurface Surface = EGL_NO_SURFACE;
@@ -244,6 +285,14 @@ namespace MobileGL::MG_Impl::GLXImpl {
Uint32 Gc = 0;
Vector<Uint8> Scratch;
Vector<Uint8> Image;
// How its frames reach the window (chosen at the first swap, X11Present::SelectPath):
// DRI3+Present shared images, or the readback put with MIT-SHM or plain PutImage.
Bool PathChosen = false;
X11Present::Path Path = X11Present::Path::PutImage;
UniquePtr<X11Present::Connection> PresentConnection;
UniquePtr<BackendImageSource> Images;
UniquePtr<X11Present::Chain> Chain;
UniquePtr<X11Present::ShmPresenter> Shm;
};
std::recursive_mutex& RegistryMutex() {
@@ -279,8 +328,18 @@ namespace MobileGL::MG_Impl::GLXImpl {
};
thread_local ThreadCurrent t_current;
// vblank_mode=0 (the benchmark convention X11 GL drivers honour) never waits for the
// display, whatever the application asks; anything else leaves the interval to it.
Bool VblankForcedOff() {
static const Bool off = [] {
const char* mode = std::getenv("vblank_mode");
return mode != nullptr && std::strcmp(mode, "0") == 0;
}();
return off;
}
Int& SwapIntervalShadow() {
static auto* interval = new Int(1);
static auto* interval = new Int(VblankForcedOff() ? 0 : 1);
return *interval;
}
@@ -334,10 +393,62 @@ namespace MobileGL::MG_Impl::GLXImpl {
return true;
}
Int& SwapIntervalShadow();
Int64 SteadyNowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
// Which way this drawable's frames go, decided once (X11Present::SelectPath), with the
// DRI3 chain set up when that is the way. A chain that cannot be set up falls through to
// the readback paths.
void ChoosePresentPath(GLXDrawableHandle drawable, DrawableSurface& surface, void* connection) {
surface.PathChosen = true;
X11Present::ConnectionCaps caps = X11Present::QueryConnectionCaps(connection);
caps.SharedImages = EGLImpl::SharedImagesAvailable();
const char* override = std::getenv("MOBILEGL_GLX_PRESENT");
surface.Path = X11Present::SelectPath(caps, surface.Depth, override);
if (surface.Path == X11Present::Path::Dri3Present) {
surface.PresentConnection =
X11Present::CreateXcbConnection(connection, static_cast<Uint32>(drawable), caps);
if (surface.PresentConnection) {
surface.Images = MakeUnique<BackendImageSource>();
surface.Chain = MakeUnique<X11Present::Chain>(
*surface.PresentConnection, *surface.Images, surface.Depth, static_cast<Int32>(surface.Width),
static_cast<Int32>(surface.Height), &SteadyNowMs);
} else {
caps.Dri3Major = 0;
surface.Path = X11Present::SelectPath(caps, surface.Depth, override);
}
}
if (surface.Path == X11Present::Path::ShmPutImage) {
surface.Shm = X11Present::CreateShmPresenter(connection);
if (!surface.Shm) surface.Path = X11Present::Path::PutImage;
}
MGLOG_I("glx: window 0x%lx (%ux%u, depth %u) presented through %s (DRI3 %u.%u, Present %u.%u, MIT-SHM "
"%u.%u, %s connection, shared images %s)",
drawable, surface.Width, surface.Height, surface.Depth, X11Present::PathName(surface.Path),
caps.Dri3Major, caps.Dri3Minor, caps.PresentMajor, caps.PresentMinor, caps.ShmMajor, caps.ShmMinor,
caps.LocalConnection ? "local" : "remote", caps.SharedImages ? "yes" : "no");
}
// The chain's resources go (pixmaps freed, images released); the drawable keeps whatever
// readback path is left.
void DropChain(DrawableSurface& surface) {
surface.Chain.reset();
surface.PresentConnection.reset();
surface.Images.reset();
}
// A REMOTE DRAWABLE IS A PBUFFER, AND NOTHING OF IT REACHES THE X WINDOW BY ITSELF: the
// backend draws in another process (another OS), where an XID means nothing. So at every
// swap the frame is read back and put on the window with PutImage - the same contract as
// the Wayland windows' wl_shm presentation. Called with the drawable current.
// swap the frame goes to the window one of three ways (X11Present.h): copied GPU-side into
// a shared image the X server has as a DRI3 pixmap and presented with PresentPixmap - no
// CPU copy at all; or read back and put with MIT-SHM; or read back and put with PutImage,
// the same contract as the Wayland windows' wl_shm presentation. Called with the drawable
// current.
void PresentRemoteDrawable(GLXDrawableHandle drawable, DrawableSurface& surface) {
const EGLImpl::GLStreamScope stream; // the readback is GL records of this thread's context
const auto& xcb = Xcb();
@@ -348,6 +459,20 @@ namespace MobileGL::MG_Impl::GLXImpl {
}
void* connection = xcb.GetXCBConnection(surface.XDisplay);
if (!connection) return;
if (!surface.PathChosen) ChoosePresentPath(drawable, surface, connection);
if (surface.Chain) {
if (surface.Chain->Present(MG_Util::Damage::Region::Full(), SwapIntervalShadow())) return;
if (!surface.Chain->Broken()) return; // the frame is dropped; the chain goes on
MGLOG_I("glx: window 0x%lx leaves DRI3+Present: %s; its frames are read back from now on", drawable,
surface.Chain->BrokenReason());
DropChain(surface);
X11Present::ConnectionCaps caps = X11Present::QueryConnectionCaps(connection);
surface.Path = X11Present::SelectPath(caps, surface.Depth, "readback");
if (surface.Path == X11Present::Path::ShmPutImage) {
surface.Shm = X11Present::CreateShmPresenter(connection);
if (!surface.Shm) surface.Path = X11Present::Path::PutImage;
}
}
if (surface.Gc == 0) {
surface.Gc = xcb.GenerateId(connection);
(void)xcb.CreateGC(connection, surface.Gc, static_cast<Uint32>(drawable), 0, nullptr);
@@ -355,6 +480,15 @@ namespace MobileGL::MG_Impl::GLXImpl {
const EGLint width = static_cast<EGLint>(surface.Width);
const EGLint height = static_cast<EGLint>(surface.Height);
const SizeT rowBytes = static_cast<SizeT>(width) * 4;
if (surface.Shm) {
const Bool put = surface.Shm->Put(
static_cast<Uint32>(drawable), surface.Gc, surface.Depth, width, height,
[&](Uint8* rows, SizeT stride) { EGLImpl::ReadBackFrameBGRA(width, height, surface.Scratch, rows, stride); });
if (put) return;
MGLOG_I("glx: window 0x%lx: MIT-SHM put failed; PutImage from now on", drawable);
surface.Shm.reset();
surface.Path = X11Present::Path::PutImage;
}
surface.Image.resize(rowBytes * static_cast<SizeT>(height));
EGLImpl::ReadBackFrameBGRA(width, height, surface.Scratch, surface.Image.data(), rowBytes);
@@ -374,6 +508,8 @@ namespace MobileGL::MG_Impl::GLXImpl {
}
void ReleaseDrawableSurface(DrawableSurface& surface) {
DropChain(surface);
surface.Shm.reset();
if (surface.Gc != 0 && surface.XDisplay) {
const auto& xcb = Xcb();
if (void* connection = xcb.Valid() ? xcb.GetXCBConnection(surface.XDisplay) : nullptr) {
@@ -408,6 +544,8 @@ namespace MobileGL::MG_Impl::GLXImpl {
static_cast<EGLint>(height))) {
surface.Width = width;
surface.Height = height;
// The shared images follow: the next frame goes into ones of the new size.
if (surface.Chain) surface.Chain->Resize(static_cast<Int32>(width), static_cast<Int32>(height));
}
}
@@ -456,7 +594,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
record.XDisplay = dpy;
record.Depth = depth;
record.LastSizePoll = std::chrono::steady_clock::now();
auto [inserted, _] = surfaces.emplace(drawable, record);
auto [inserted, _] = surfaces.emplace(drawable, std::move(record));
return &inserted->second;
}
@@ -670,7 +808,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
}
const char* QueryExtensionsString(Display*, int) {
return kGLXExtensions;
return GLXExtensions();
}
const char* GetClientString(Display*, int name) {
@@ -680,7 +818,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
case GLX_VERSION:
return "1.4";
case GLX_EXTENSIONS:
return kGLXExtensions;
return GLXExtensions();
default:
return nullptr;
}
@@ -1086,8 +1224,15 @@ namespace MobileGL::MG_Impl::GLXImpl {
PresentRemoteDrawable(drawable, it->second);
}
EGLImpl::SwapBuffers(it->second.Display, it->second.Surface);
// A resize takes effect for the next frame.
SyncSurfaceSize(dpy, drawable, it->second);
// A resize takes effect for the next frame: at once when Present reported one, else at the
// next throttled geometry poll.
Int32 configuredWidth = 0;
Int32 configuredHeight = 0;
const Bool configured =
it->second.Chain && it->second.Chain->TakeConfiguredSize(&configuredWidth, &configuredHeight) &&
(static_cast<Uint32>(configuredWidth) != it->second.Width ||
static_cast<Uint32>(configuredHeight) != it->second.Height);
SyncSurfaceSize(dpy, drawable, it->second, configured);
}
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
@@ -1187,6 +1332,21 @@ namespace MobileGL::MG_Impl::GLXImpl {
case GLX_SWAP_INTERVAL_EXT:
*value = static_cast<unsigned int>(SwapIntervalShadow());
return;
case GLX_BACK_BUFFER_AGE_EXT: {
// GLX_EXT_buffer_age: the age of the buffer the drawable's next frame draws into - the
// server's pbuffer, which keeps its content - asked of the EGL surface behind it, which
// answers only while it is the calling thread's draw surface.
*value = 0;
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
auto& surfaces = DrawableSurfaces();
auto it = surfaces.find(drawable);
if (it == surfaces.end() || t_current.Draw != drawable) return;
EGLint age = 0;
if (EGLImpl::QuerySurface(it->second.Display, it->second.Surface, EGL_BUFFER_AGE_EXT, &age) && age > 0) {
*value = static_cast<unsigned int>(age);
}
return;
}
case GLX_MAX_SWAP_INTERVAL_EXT:
*value = 4;
return;
@@ -1210,7 +1370,7 @@ namespace MobileGL::MG_Impl::GLXImpl {
return;
}
// MobileGL validates 0..4; adaptive vsync (negative) clamps to regular.
interval = std::clamp(interval, 0, 4);
interval = VblankForcedOff() ? 0 : std::clamp(interval, 0, 4);
EGLImpl::SwapInterval(display, interval);
SwapIntervalShadow() = interval;
}
+284
View File
@@ -0,0 +1,284 @@
// MobileGL - MobileGL/MG_Impl/GLXImpl/X11Present.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
#include "X11Present.h"
#include <cstring>
#if !defined(_WIN32)
#include <unistd.h>
#endif
namespace MobileGL::MG_Impl::GLXImpl::X11Present {
namespace {
void CloseDescriptor(int fd) {
#if !defined(_WIN32)
if (fd >= 0) ::close(fd);
#else
(void)fd;
#endif
}
} // namespace
const char* PathName(Path path) {
switch (path) {
case Path::Dri3Present:
return "DRI3+Present shared images";
case Path::ShmPutImage:
return "readback + MIT-SHM PutImage";
case Path::PutImage:
return "readback + PutImage";
}
return "?";
}
Uint32 FourccForDepth(Uint32 depth) {
switch (depth) {
case 24:
return kFourccXrgb8888;
case 32:
return kFourccArgb8888;
default:
return 0;
}
}
Path SelectPath(const ConnectionCaps& caps, Uint32 depth, const char* overrideValue) {
const Bool putImageOnly = overrideValue != nullptr && std::strcmp(overrideValue, "putimage") == 0;
const Bool noDri3 = putImageOnly || (overrideValue != nullptr && (std::strcmp(overrideValue, "readback") == 0 ||
std::strcmp(overrideValue, "shm") == 0));
if (!noDri3 && caps.SharedImages && caps.LocalConnection && FourccForDepth(depth) != 0 &&
caps.Dri3Major >= 1 && caps.PresentMajor >= 1) {
return Path::Dri3Present;
}
// MIT-SHM 1.2 is the first with a client-made segment passed as a descriptor (no SysV
// shared memory, which a container need not share with the X server).
const Bool shmFd = caps.ShmMajor > 1 || (caps.ShmMajor == 1 && caps.ShmMinor >= 2);
if (!putImageOnly && caps.LocalConnection && shmFd) return Path::ShmPutImage;
return Path::PutImage;
}
Chain::Chain(Connection& connection, ImageSource& images, Uint32 depth, Int32 width, Int32 height,
std::function<Int64()> nowMs)
: m_connection(connection), m_images(images), m_depth(depth), m_fourcc(FourccForDepth(depth)),
m_width(width > 0 ? width : 1), m_height(height > 0 ? height : 1), m_nowMs(std::move(nowMs)) {
if (m_fourcc == 0) Break("the window's depth has no shared-image format");
}
Chain::~Chain() {
for (Buffer& buffer : m_buffers) Drop(buffer);
m_connection.Flush();
}
void Chain::Break(const char* reason) {
if (m_broken) return;
m_broken = true;
m_brokenReason = reason;
}
SizeT Chain::AllocatedBuffers() const {
SizeT count = 0;
for (const Buffer& buffer : m_buffers) count += buffer.Pixmap != 0 ? 1 : 0;
return count;
}
SizeT Chain::BusyBuffers() const {
SizeT count = 0;
for (const Buffer& buffer : m_buffers) count += buffer.Pixmap != 0 && buffer.Busy ? 1 : 0;
return count;
}
void Chain::Drop(Buffer& buffer) {
// The X server keeps a pixmap a pending present still names alive past FreePixmap, and the
// X server's own import of the image holds the image on the MobileGL server: both may go.
if (buffer.Pixmap != 0) m_connection.FreePixmap(buffer.Pixmap);
if (buffer.Storage.Id != 0) m_images.Release(buffer.Storage.Id);
CloseDescriptor(buffer.Storage.Fd);
m_damage.Invalidate(static_cast<SizeT>(&buffer - m_buffers));
buffer = Buffer{};
}
Bool Chain::Allocate(Buffer& buffer) {
Drop(buffer);
Image image;
if (!m_images.Allocate(static_cast<Uint32>(m_width), static_cast<Uint32>(m_height), m_fourcc, &image) ||
image.Id == 0) {
CloseDescriptor(image.Fd);
Break("the server allocated no shared image for the window");
return false;
}
if (image.Fd < 0 || image.Width != static_cast<Uint32>(m_width) || image.Height != static_cast<Uint32>(m_height)) {
CloseDescriptor(image.Fd);
m_images.Release(image.Id);
Break("a shared image came back without a descriptor or at another size");
return false;
}
const Uint32 pixmap = m_connection.ImportPixmap(image, m_depth);
// The pixmap holds the memory now (and the X server's own import, the image).
CloseDescriptor(image.Fd);
image.Fd = -1;
if (pixmap == 0) {
m_images.Release(image.Id);
Break("the X server refused the shared image as a DRI3 pixmap");
return false;
}
buffer.Storage = image;
buffer.Pixmap = pixmap;
buffer.Busy = false;
buffer.Serial = 0;
m_damage.Invalidate(static_cast<SizeT>(&buffer - m_buffers));
return true;
}
void Chain::Handle(const Event& event) {
switch (event.Type) {
case Event::Kind::Idle:
for (Buffer& buffer : m_buffers) {
if (buffer.Pixmap != 0 && buffer.Pixmap == event.Pixmap) buffer.Busy = false;
}
break;
case Event::Kind::Complete:
if (event.Msc != 0) m_lastMsc = event.Msc;
if (m_throttle.Outstanding() && event.Serial == m_awaitedSerial) m_throttle.Done();
break;
case Event::Kind::Configure:
if (event.WindowDestroyed) {
Break("the window was destroyed");
break;
}
if (event.Width > 0 && event.Height > 0) {
m_configured = true;
m_configuredWidth = event.Width;
m_configuredHeight = event.Height;
}
break;
case Event::Kind::None:
break;
}
}
void Chain::DrainEvents() {
Event event;
while (m_connection.PollEvent(&event)) Handle(event);
}
void Chain::WaitForPacing(Int swapInterval) {
const Int64 budget = m_throttle.WaitBudgetMs(swapInterval, m_nowMs());
if (budget == 0) return;
const Int64 limit = budget == EGLImpl::Wayland::FrameThrottle::kForever ? kCompleteGiveUpMs : budget;
const Int64 deadline = m_nowMs() + limit;
Event event;
while (m_throttle.Outstanding() && !m_broken) {
const Int64 remaining = deadline - m_nowMs();
if (remaining <= 0) {
// Interval 0's bounded wait simply ends; a paced wait gives the completion up so a
// server that lost it does not stall the application forever.
if (budget == EGLImpl::Wayland::FrameThrottle::kForever) m_throttle.Done();
return;
}
if (m_connection.WaitEvent(&event, remaining)) Handle(event);
}
}
Chain::Buffer* Chain::NextFree() {
const Int64 deadline = m_nowMs() + kIdleWaitMs;
for (;;) {
if (m_broken) return nullptr;
// The free buffer presented longest ago, so the images take turns.
Buffer* best = nullptr;
for (Buffer& buffer : m_buffers) {
if (buffer.Pixmap == 0 || buffer.Busy) continue;
if (best == nullptr || buffer.Serial < best->Serial) best = &buffer;
}
if (best != nullptr) {
const Bool sized = best->Storage.Width == static_cast<Uint32>(m_width) &&
best->Storage.Height == static_cast<Uint32>(m_height);
if (sized || Allocate(*best)) return best;
return nullptr;
}
SizeT allocated = AllocatedBuffers();
if (allocated < kBuffers) {
for (Buffer& buffer : m_buffers) {
if (buffer.Pixmap == 0) return Allocate(buffer) ? &buffer : nullptr;
}
}
// Every buffer is with the X server: wait for one to come back.
const Int64 remaining = deadline - m_nowMs();
Event event;
if (remaining > 0 && m_connection.WaitEvent(&event, remaining)) {
Handle(event);
continue;
}
// None came back in time: one more buffer, up to the limit; past it the frame is
// dropped (the chain is not broken: the server may just be slow).
if (allocated < kMaxBuffers) {
for (Buffer& buffer : m_buffers) {
if (buffer.Pixmap == 0) return Allocate(buffer) ? &buffer : nullptr;
}
}
return nullptr;
}
}
Bool Chain::Present(const MG_Util::Damage::Region& damage, Int swapInterval) {
if (m_broken) return false;
DrainEvents();
if (m_connection.PresentFailed()) Break("the X server refused a PresentPixmap");
if (m_broken) return false;
WaitForPacing(swapInterval);
if (m_broken) return false;
Buffer* buffer = NextFree();
if (buffer == nullptr) return false;
const SizeT index = static_cast<SizeT>(buffer - m_buffers);
const MG_Util::Damage::Region copy = m_damage.TakeForWrite(index, damage, m_width, m_height);
if (!m_images.CopyFrame(buffer->Storage.Id, copy)) {
Break("the frame could not be copied into a shared image");
return false;
}
const Uint32 serial = ++m_serial;
const Uint32 options = swapInterval > 0 ? 0u : kPresentOptionAsync;
const Uint64 targetMsc = swapInterval > 0 && m_lastMsc != 0 ? m_lastMsc + static_cast<Uint64>(swapInterval) : 0;
MG_Util::Damage::Region update = MG_Util::Damage::Region::Full();
if (!damage.IsFull()) update = damage.FlippedY(m_height);
if (!m_connection.PresentPixmap(buffer->Pixmap, serial, options, targetMsc, update)) {
Break("PresentPixmap could not be sent");
return false;
}
buffer->Busy = true;
buffer->Serial = serial;
if (m_throttle.WantsFrameRequest()) {
m_throttle.Requested(m_nowMs());
m_awaitedSerial = serial;
}
m_connection.Flush();
++m_framesPresented;
return true;
}
void Chain::Resize(Int32 width, Int32 height) {
if (width <= 0 || height <= 0 || (width == m_width && height == m_height)) return;
m_width = width;
m_height = height;
// Old-size buffers the X server is not holding go now; the rest as they come back
// (NextFree reallocates a free buffer of the wrong size).
for (Buffer& buffer : m_buffers) {
if (buffer.Pixmap != 0 && !buffer.Busy) Drop(buffer);
}
m_damage.InvalidateAll();
}
Bool Chain::TakeConfiguredSize(Int32* width, Int32* height) {
if (!m_configured) return false;
m_configured = false;
*width = m_configuredWidth;
*height = m_configuredHeight;
return true;
}
} // namespace MobileGL::MG_Impl::GLXImpl::X11Present
+199
View File
@@ -0,0 +1,199 @@
// MobileGL - MobileGL/MG_Impl/GLXImpl/X11Present.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
// GLX WINDOWS PRESENTED THROUGH DRI3 + PRESENT (docs/Disaggregated/notes/anland/plan-x11-gpu.md).
//
// A GLX window's frames are drawn by the server into a pbuffer. Instead of reading each frame back
// and putting it on the window, the frame is copied GPU-side into one of a few server-allocated
// shared images, each imported into the X server once as a pixmap (DRI3 PixmapFromBuffer(s), the
// image's dma-buf), and presented with Present PresentPixmap. An X server drawing with MobileGL
// (Xwayland's glamor) then shows the very same image - attached to its window as a linux-dmabuf
// buffer, or copied on its GPU - and no frame crosses the CPU.
//
// The logic is here, behind two narrow interfaces: Connection (the X side of one window: the DRI3
// import, PresentPixmap, the Present events of a special-event queue) and ImageSource (the server's
// shared images and the GPU copy). X11PresentXcb.cpp implements Connection over libxcb; the host
// tests drive Chain with fakes of both.
//
// Buffers. Up to kBuffers images of the window's size (kMaxBuffers when the X server has held
// every one past kIdleWaitMs); a buffer is busy from its PresentPixmap to its IdleNotify. A
// resize drops each old-size buffer as it comes back idle.
// Pacing. FrameThrottle's rules (EGLImpl/FrameThrottle.h) on CompleteNotify: interval >= 1 waits
// for the previous frame's completion and targets last_msc + interval; interval 0 presents
// PresentOptionAsync, unpaced while completions come, slowed to about one frame a second once
// one is overdue (the X server shows nothing of the window).
// Errors. A refused import, a failed PresentPixmap, a failed copy or the window's destruction
// break the chain for good; its owner falls back to the readback path.
#include <Includes.h>
#include <MG_Util/Damage/Damage.h>
#include "../EGLImpl/FrameThrottle.h"
#include <functional>
namespace MobileGL::MG_Impl::GLXImpl::X11Present {
inline constexpr Uint64 kModifierInvalid = 0x00ffffffffffffffull;
// DRM fourccs: what an X server drawing through GBM imports a depth-24/32 pixmap as.
inline constexpr Uint32 kFourccXrgb8888 = 0x34325258u; // 'XR24'
inline constexpr Uint32 kFourccArgb8888 = 0x34325241u; // 'AR24'
// Present option bits (presentproto).
inline constexpr Uint32 kPresentOptionAsync = 1u;
// ---- which path a window's frames take -------------------------------------------------------
enum class Path { Dri3Present, ShmPutImage, PutImage };
const char* PathName(Path path);
struct ConnectionCaps {
Bool SharedImages = false; // the backend allocates shared images (a split client)
Bool LocalConnection = false; // a unix socket: descriptors can be passed
Uint32 Dri3Major = 0, Dri3Minor = 0;
Uint32 PresentMajor = 0, PresentMinor = 0;
Uint32 ShmMajor = 0, ShmMinor = 0;
};
// The fourcc a window of `depth` is presented in; 0 for a depth this cannot present.
Uint32 FourccForDepth(Uint32 depth);
// DRI3+Present when the connection has both, descriptors can travel, the backend has shared
// images and the depth is 24/32; otherwise MIT-SHM >= 1.2 (fd attach) when local; otherwise
// PutImage. `overrideValue` (MOBILEGL_GLX_PRESENT): "readback" never takes DRI3, "putimage"
// takes neither DRI3 nor MIT-SHM; anything else is automatic.
Path SelectPath(const ConnectionCaps& caps, Uint32 depth, const char* overrideValue);
// ---- the two sides -------------------------------------------------------------------------
struct Image {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint32 Fourcc = 0;
Uint64 Modifier = kModifierInvalid;
};
class ImageSource {
public:
virtual ~ImageSource() = default;
// A new image; `out->Fd` is the caller's.
virtual Bool Allocate(Uint32 width, Uint32 height, Uint32 fourcc, Image* out) = 0;
virtual void Release(Uint64 id) = 0;
// The current frame into the image, top row first; `region` in GL window coordinates (Full
// = all). True once the copy is submitted and its fence published.
virtual Bool CopyFrame(Uint64 id, const MG_Util::Damage::Region& region) = 0;
};
struct Event {
enum class Kind { None, Idle, Complete, Configure };
Kind Type = Kind::None;
Uint32 Serial = 0;
Uint32 Pixmap = 0;
Uint64 Msc = 0;
Uint8 Mode = 0;
Int32 Width = 0;
Int32 Height = 0;
Bool WindowDestroyed = false;
};
class Connection {
public:
virtual ~Connection() = default;
// A pixmap of `depth` whose storage is the image (its descriptor is duplicated for the
// request; the caller keeps its own). 0 = the X server refused it.
virtual Uint32 ImportPixmap(const Image& image, Uint32 depth) = 0;
virtual void FreePixmap(Uint32 pixmap) = 0;
// Queues a PresentPixmap. `update` is in X coordinates (top-left origin); Full = all of it.
// False when it could not even be sent.
virtual Bool PresentPixmap(Uint32 pixmap, Uint32 serial, Uint32 options, Uint64 targetMsc,
const MG_Util::Damage::Region& update) = 0;
// Whether a PresentPixmap sent earlier is now known to have failed (an X error).
virtual Bool PresentFailed() = 0;
virtual Bool PollEvent(Event* out) = 0;
// Waits up to `timeoutMs` for an event. False: none came (or the connection broke).
virtual Bool WaitEvent(Event* out, Int64 timeoutMs) = 0;
virtual void Flush() = 0;
};
// ---- one window's swap chain ----------------------------------------------------------------
class Chain {
public:
static constexpr SizeT kBuffers = 3;
static constexpr SizeT kMaxBuffers = 4;
static constexpr Int64 kIdleWaitMs = 1000;
// Longest a swap waits for a completion before it stops pacing on it (a server that lost it).
static constexpr Int64 kCompleteGiveUpMs = 5000;
// `nowMs`: a monotonic clock in milliseconds (injected for the tests).
Chain(Connection& connection, ImageSource& images, Uint32 depth, Int32 width, Int32 height,
std::function<Int64()> nowMs);
~Chain();
Chain(const Chain&) = delete;
Chain& operator=(const Chain&) = delete;
// Presents the current frame. `damage` is what it changed (GL window coordinates; Full = all).
// False: the frame was not shown this way - Broken() says whether that is for good.
Bool Present(const MG_Util::Damage::Region& damage, Int swapInterval);
// The window has a new size: the next frame goes into images of that size.
void Resize(Int32 width, Int32 height);
// A size the X server reported (ConfigureNotify) that the owner has not taken yet.
Bool TakeConfiguredSize(Int32* width, Int32* height);
Bool Broken() const { return m_broken; }
const char* BrokenReason() const { return m_brokenReason; }
// For the tests and the log.
SizeT AllocatedBuffers() const;
SizeT BusyBuffers() const;
Uint64 FramesPresented() const { return m_framesPresented; }
Int32 Width() const { return m_width; }
Int32 Height() const { return m_height; }
private:
struct Buffer {
Image Storage;
Uint32 Pixmap = 0;
Bool Busy = false;
Uint32 Serial = 0;
};
void Break(const char* reason);
void Handle(const Event& event);
void DrainEvents();
void WaitForPacing(Int swapInterval);
Buffer* NextFree();
Bool Allocate(Buffer& buffer);
void Drop(Buffer& buffer);
Connection& m_connection;
ImageSource& m_images;
Uint32 m_depth = 24;
Uint32 m_fourcc = kFourccXrgb8888;
Int32 m_width = 0;
Int32 m_height = 0;
std::function<Int64()> m_nowMs;
Buffer m_buffers[kMaxBuffers];
MG_Util::Damage::BufferDamageTracker m_damage{kMaxBuffers};
EGLImpl::Wayland::FrameThrottle m_throttle;
Uint32 m_awaitedSerial = 0;
Uint32 m_serial = 0;
Uint64 m_lastMsc = 0;
Uint64 m_framesPresented = 0;
Bool m_configured = false;
Int32 m_configuredWidth = 0;
Int32 m_configuredHeight = 0;
Bool m_broken = false;
const char* m_brokenReason = "";
};
} // namespace MobileGL::MG_Impl::GLXImpl::X11Present
+522
View File
@@ -0,0 +1,522 @@
// MobileGL - MobileGL/MG_Impl/GLXImpl/X11PresentXcb.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
#include "X11PresentXcb.h"
#if MOBILEGL_GLX_XCB_PRESENT
#include <MG_Util/Debug/Log.h>
#include <xcb/dri3.h>
#include <xcb/present.h>
#include <xcb/shm.h>
#include <xcb/xcb.h>
#include <xcb/xcbext.h>
#include <xcb/xfixes.h>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <dlfcn.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <unistd.h>
namespace MobileGL::MG_Impl::GLXImpl::X11Present {
namespace {
// presentproto: ConfigureNotify's pixmap_flags when the window is gone (Present 1.2+).
constexpr uint32_t kPresentWindowDestroyed = 1u << 0;
#define MGL_XCB_FN(name) decltype(&::name) name = nullptr
struct XcbApi {
Bool core = false, dri3 = false, present = false, xfixes = false, shm = false;
// libxcb
MGL_XCB_FN(xcb_generate_id);
MGL_XCB_FN(xcb_flush);
MGL_XCB_FN(xcb_get_extension_data);
MGL_XCB_FN(xcb_request_check);
MGL_XCB_FN(xcb_poll_for_reply);
MGL_XCB_FN(xcb_discard_reply);
MGL_XCB_FN(xcb_get_file_descriptor);
MGL_XCB_FN(xcb_connection_has_error);
MGL_XCB_FN(xcb_register_for_special_xge);
MGL_XCB_FN(xcb_unregister_for_special_event);
MGL_XCB_FN(xcb_poll_for_special_event);
MGL_XCB_FN(xcb_free_pixmap_checked);
MGL_XCB_FN(xcb_get_input_focus);
MGL_XCB_FN(xcb_get_input_focus_reply);
// libxcb-dri3
xcb_extension_t* dri3Id = nullptr;
MGL_XCB_FN(xcb_dri3_query_version);
MGL_XCB_FN(xcb_dri3_query_version_reply);
MGL_XCB_FN(xcb_dri3_pixmap_from_buffer_checked);
MGL_XCB_FN(xcb_dri3_pixmap_from_buffers_checked);
// libxcb-present
xcb_extension_t* presentId = nullptr;
MGL_XCB_FN(xcb_present_query_version);
MGL_XCB_FN(xcb_present_query_version_reply);
MGL_XCB_FN(xcb_present_select_input_checked);
MGL_XCB_FN(xcb_present_pixmap_checked);
// libxcb-xfixes
xcb_extension_t* xfixesId = nullptr;
MGL_XCB_FN(xcb_xfixes_query_version);
MGL_XCB_FN(xcb_xfixes_query_version_reply);
MGL_XCB_FN(xcb_xfixes_create_region);
MGL_XCB_FN(xcb_xfixes_destroy_region);
// libxcb-shm
xcb_extension_t* shmId = nullptr;
MGL_XCB_FN(xcb_shm_query_version);
MGL_XCB_FN(xcb_shm_query_version_reply);
MGL_XCB_FN(xcb_shm_attach_fd_checked);
MGL_XCB_FN(xcb_shm_detach);
MGL_XCB_FN(xcb_shm_put_image);
};
#undef MGL_XCB_FN
void* OpenLibrary(const char* name) {
void* lib = dlopen(name, RTLD_NOW | RTLD_NOLOAD);
return lib != nullptr ? lib : dlopen(name, RTLD_NOW | RTLD_LOCAL);
}
template <typename Fn>
Bool Resolve(void* lib, const char* name, Fn& out) {
out = lib != nullptr ? reinterpret_cast<Fn>(dlsym(lib, name)) : nullptr;
return out != nullptr;
}
const XcbApi& Api() {
static const XcbApi* api = [] {
auto* a = new XcbApi();
void* xcb = OpenLibrary("libxcb.so.1");
#define R(lib, fn) Resolve(lib, #fn, a->fn)
a->core = R(xcb, xcb_generate_id) & R(xcb, xcb_flush) & R(xcb, xcb_get_extension_data) &
R(xcb, xcb_request_check) & R(xcb, xcb_poll_for_reply) & R(xcb, xcb_discard_reply) &
R(xcb, xcb_get_file_descriptor) & R(xcb, xcb_connection_has_error) &
R(xcb, xcb_register_for_special_xge) & R(xcb, xcb_unregister_for_special_event) &
R(xcb, xcb_poll_for_special_event) & R(xcb, xcb_free_pixmap_checked) &
R(xcb, xcb_get_input_focus) & R(xcb, xcb_get_input_focus_reply);
if (a->core) {
void* dri3 = OpenLibrary("libxcb-dri3.so.0");
a->dri3Id = dri3 != nullptr ? static_cast<xcb_extension_t*>(dlsym(dri3, "xcb_dri3_id")) : nullptr;
a->dri3 = a->dri3Id != nullptr && R(dri3, xcb_dri3_query_version) &
R(dri3, xcb_dri3_query_version_reply) &
R(dri3, xcb_dri3_pixmap_from_buffer_checked);
// 1.2's multi-plane request is optional: an older libxcb-dri3 has only 1.0's.
(void)R(dri3, xcb_dri3_pixmap_from_buffers_checked);
void* present = OpenLibrary("libxcb-present.so.0");
a->presentId =
present != nullptr ? static_cast<xcb_extension_t*>(dlsym(present, "xcb_present_id")) : nullptr;
a->present = a->presentId != nullptr && R(present, xcb_present_query_version) &
R(present, xcb_present_query_version_reply) &
R(present, xcb_present_select_input_checked) &
R(present, xcb_present_pixmap_checked);
void* xfixes = OpenLibrary("libxcb-xfixes.so.0");
a->xfixesId =
xfixes != nullptr ? static_cast<xcb_extension_t*>(dlsym(xfixes, "xcb_xfixes_id")) : nullptr;
a->xfixes = a->xfixesId != nullptr && R(xfixes, xcb_xfixes_query_version) &
R(xfixes, xcb_xfixes_query_version_reply) &
R(xfixes, xcb_xfixes_create_region) &
R(xfixes, xcb_xfixes_destroy_region);
void* shm = OpenLibrary("libxcb-shm.so.0");
a->shmId = shm != nullptr ? static_cast<xcb_extension_t*>(dlsym(shm, "xcb_shm_id")) : nullptr;
a->shm = a->shmId != nullptr && R(shm, xcb_shm_query_version) &
R(shm, xcb_shm_query_version_reply) &
R(shm, xcb_shm_attach_fd_checked) & R(shm, xcb_shm_detach) &
R(shm, xcb_shm_put_image);
}
#undef R
return a;
}();
return *api;
}
xcb_connection_t* Xcb(void* connection) { return static_cast<xcb_connection_t*>(connection); }
Bool ExtensionPresent(xcb_connection_t* c, xcb_extension_t* id) {
const XcbApi& api = Api();
if (id == nullptr) return false;
const xcb_query_extension_reply_t* data = api.xcb_get_extension_data(c, id);
return data != nullptr && data->present != 0;
}
struct CachedCaps {
void* Connection = nullptr;
ConnectionCaps Caps;
Bool XFixes = false;
};
std::mutex& CapsMutex() {
static auto* mutex = new std::mutex();
return *mutex;
}
Vector<CachedCaps>& CapsCache() {
static auto* cache = new Vector<CachedCaps>();
return *cache;
}
CachedCaps Query(void* connection) {
const std::lock_guard<std::mutex> lock(CapsMutex());
for (const CachedCaps& cached : CapsCache()) {
if (cached.Connection == connection) return cached;
}
CachedCaps result;
result.Connection = connection;
const XcbApi& api = Api();
xcb_connection_t* c = Xcb(connection);
if (!api.core || c == nullptr || api.xcb_connection_has_error(c)) return result;
sockaddr_storage address{};
socklen_t length = sizeof(address);
if (::getsockname(api.xcb_get_file_descriptor(c), reinterpret_cast<sockaddr*>(&address), &length) == 0)
result.Caps.LocalConnection = address.ss_family == AF_UNIX;
if (api.dri3 && ExtensionPresent(c, api.dri3Id)) {
if (auto* reply = api.xcb_dri3_query_version_reply(c, api.xcb_dri3_query_version(c, 1, 2), nullptr)) {
result.Caps.Dri3Major = reply->major_version;
result.Caps.Dri3Minor = reply->minor_version;
std::free(reply);
}
}
if (api.present && ExtensionPresent(c, api.presentId)) {
if (auto* reply =
api.xcb_present_query_version_reply(c, api.xcb_present_query_version(c, 1, 2), nullptr)) {
result.Caps.PresentMajor = reply->major_version;
result.Caps.PresentMinor = reply->minor_version;
std::free(reply);
}
}
// XFixes demands its version be asked before any other request of it.
if (api.xfixes && ExtensionPresent(c, api.xfixesId)) {
if (auto* reply = api.xcb_xfixes_query_version_reply(c, api.xcb_xfixes_query_version(c, 5, 0), nullptr)) {
result.XFixes = reply->major_version >= 2;
std::free(reply);
}
}
if (api.shm && ExtensionPresent(c, api.shmId)) {
if (auto* reply = api.xcb_shm_query_version_reply(c, api.xcb_shm_query_version(c), nullptr)) {
result.Caps.ShmMajor = reply->major_version;
result.Caps.ShmMinor = reply->minor_version;
std::free(reply);
}
}
CapsCache().push_back(result);
return result;
}
Int64 NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
Bool Translate(const xcb_generic_event_t* raw, Event* out) {
const auto* generic = reinterpret_cast<const xcb_present_generic_event_t*>(raw);
*out = Event{};
switch (generic->evtype) {
case XCB_PRESENT_EVENT_IDLE_NOTIFY: {
const auto* idle = reinterpret_cast<const xcb_present_idle_notify_event_t*>(raw);
out->Type = Event::Kind::Idle;
out->Serial = idle->serial;
out->Pixmap = idle->pixmap;
return true;
}
case XCB_PRESENT_EVENT_COMPLETE_NOTIFY: {
const auto* complete = reinterpret_cast<const xcb_present_complete_notify_event_t*>(raw);
if (complete->kind != XCB_PRESENT_COMPLETE_KIND_PIXMAP) return false;
out->Type = Event::Kind::Complete;
out->Serial = complete->serial;
out->Msc = complete->msc;
out->Mode = complete->mode;
return true;
}
case XCB_PRESENT_EVENT_CONFIGURE_NOTIFY: {
const auto* configure = reinterpret_cast<const xcb_present_configure_notify_event_t*>(raw);
out->Type = Event::Kind::Configure;
out->Width = configure->width;
out->Height = configure->height;
out->WindowDestroyed = (configure->pixmap_flags & kPresentWindowDestroyed) != 0;
return true;
}
default:
return false;
}
}
class XcbConnection final : public Connection {
public:
XcbConnection(xcb_connection_t* c, Uint32 window, Bool buffers12, Bool xfixes)
: m_c(c), m_window(window), m_buffers12(buffers12), m_xfixes(xfixes) {}
Bool Init() {
const XcbApi& api = Api();
m_eid = api.xcb_generate_id(m_c);
m_special = api.xcb_register_for_special_xge(m_c, Api().presentId, m_eid, &m_stamp);
if (m_special == nullptr) return false;
const xcb_void_cookie_t cookie = api.xcb_present_select_input_checked(
m_c, m_eid, m_window,
XCB_PRESENT_EVENT_MASK_CONFIGURE_NOTIFY | XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY |
XCB_PRESENT_EVENT_MASK_IDLE_NOTIFY);
if (xcb_generic_error_t* error = api.xcb_request_check(m_c, cookie)) {
std::free(error);
return false;
}
return true;
}
~XcbConnection() override {
const XcbApi& api = Api();
if (m_hasPendingPresent) api.xcb_discard_reply(m_c, m_pendingPresent);
if (m_special != nullptr) {
const xcb_void_cookie_t cookie =
api.xcb_present_select_input_checked(m_c, m_eid, m_window, XCB_PRESENT_EVENT_MASK_NO_EVENT);
// The window may be gone already: an error here is expected and dropped.
api.xcb_discard_reply(m_c, cookie.sequence);
api.xcb_unregister_for_special_event(m_c, m_special);
}
api.xcb_flush(m_c);
}
Uint32 ImportPixmap(const Image& image, Uint32 depth) override {
const XcbApi& api = Api();
// libxcb closes the descriptor it sends.
const int fd = ::fcntl(image.Fd, F_DUPFD_CLOEXEC, 0);
if (fd < 0) return 0;
const Uint32 pixmap = api.xcb_generate_id(m_c);
xcb_void_cookie_t cookie{};
if (m_buffers12 && api.xcb_dri3_pixmap_from_buffers_checked != nullptr) {
int32_t fds[1] = {fd};
cookie = api.xcb_dri3_pixmap_from_buffers_checked(
m_c, pixmap, m_window, 1, static_cast<uint16_t>(image.Width), static_cast<uint16_t>(image.Height),
image.Stride, image.Offset, 0, 0, 0, 0, 0, 0, static_cast<uint8_t>(depth), 32, image.Modifier, fds);
} else {
if (image.Offset != 0 || image.Stride > 0xffffu) {
::close(fd);
return 0;
}
cookie = api.xcb_dri3_pixmap_from_buffer_checked(
m_c, pixmap, m_window, image.Stride * image.Height, static_cast<uint16_t>(image.Width),
static_cast<uint16_t>(image.Height), static_cast<uint16_t>(image.Stride),
static_cast<uint8_t>(depth), 32, fd);
}
if (xcb_generic_error_t* error = api.xcb_request_check(m_c, cookie)) {
MGLOG_I("glx: the X server refused a %ux%u shared image as a pixmap (X error %u, major %u minor %u)",
image.Width, image.Height, error->error_code, error->major_code, error->minor_code);
std::free(error);
return 0;
}
return pixmap;
}
void FreePixmap(Uint32 pixmap) override {
const XcbApi& api = Api();
api.xcb_discard_reply(m_c, api.xcb_free_pixmap_checked(m_c, pixmap).sequence);
}
Bool PresentPixmap(Uint32 pixmap, Uint32 serial, Uint32 options, Uint64 targetMsc,
const MG_Util::Damage::Region& update) override {
const XcbApi& api = Api();
if (api.xcb_connection_has_error(m_c)) return false;
Uint32 region = 0;
if (!update.IsFull() && m_xfixes) {
Vector<xcb_rectangle_t> rects;
for (const MG_Util::Damage::Rect& rect : update.Rects()) {
rects.push_back({static_cast<int16_t>(rect.X), static_cast<int16_t>(rect.Y),
static_cast<uint16_t>(rect.Width), static_cast<uint16_t>(rect.Height)});
}
if (!rects.empty()) {
region = api.xcb_generate_id(m_c);
api.xcb_xfixes_create_region(m_c, region, static_cast<uint32_t>(rects.size()), rects.data());
}
}
// An earlier present whose answer never came is given up on, not left queued.
if (m_hasPendingPresent) api.xcb_discard_reply(m_c, m_pendingPresent);
const xcb_void_cookie_t cookie =
api.xcb_present_pixmap_checked(m_c, m_window, pixmap, serial, 0, region, 0, 0, 0, 0, 0, options,
targetMsc, 0, 0, 0, nullptr);
m_pendingPresent = cookie.sequence;
m_hasPendingPresent = true;
if (region != 0) api.xcb_xfixes_destroy_region(m_c, region);
return true;
}
Bool PresentFailed() override {
const XcbApi& api = Api();
if (m_failed || !m_hasPendingPresent) return m_failed;
void* reply = nullptr;
xcb_generic_error_t* error = nullptr;
// Known once anything later has come back (an event carries the sequence): no round trip.
if (api.xcb_poll_for_reply(m_c, m_pendingPresent, &reply, &error) != 0) {
m_hasPendingPresent = false;
if (error != nullptr) {
MGLOG_I("glx: PresentPixmap failed (X error %u)", error->error_code);
m_failed = true;
}
std::free(reply);
std::free(error);
}
return m_failed;
}
Bool PollEvent(Event* out) override {
const XcbApi& api = Api();
for (;;) {
xcb_generic_event_t* raw = api.xcb_poll_for_special_event(m_c, m_special);
if (raw == nullptr) return false;
const Bool known = Translate(raw, out);
std::free(raw);
if (known) return true;
}
}
// Short poll slices: another thread (the application's event loop) may be the one
// reading the socket, and its read moves our events into the special queue.
Bool WaitEvent(Event* out, Int64 timeoutMs) override {
const XcbApi& api = Api();
api.xcb_flush(m_c);
const Int64 deadline = NowMs() + timeoutMs;
for (;;) {
if (PollEvent(out)) return true;
if (api.xcb_connection_has_error(m_c)) return false;
const Int64 remaining = deadline - NowMs();
if (remaining <= 0) return false;
pollfd p{api.xcb_get_file_descriptor(m_c), POLLIN, 0};
(void)::poll(&p, 1, static_cast<int>(std::min<Int64>(remaining, 4)));
}
}
void Flush() override { Api().xcb_flush(m_c); }
private:
xcb_connection_t* m_c = nullptr;
Uint32 m_window = 0;
Bool m_buffers12 = false;
Bool m_xfixes = false;
Uint32 m_eid = 0;
uint32_t m_stamp = 0;
xcb_special_event_t* m_special = nullptr;
unsigned int m_pendingPresent = 0;
Bool m_hasPendingPresent = false;
Bool m_failed = false;
};
class XcbShmPresenter final : public ShmPresenter {
public:
explicit XcbShmPresenter(xcb_connection_t* c) : m_c(c) {}
~XcbShmPresenter() override {
for (Segment& segment : m_segments) Release(segment);
Api().xcb_flush(m_c);
}
Bool Put(Uint32 drawable, Uint32 gc, Uint32 depth, Int32 width, Int32 height,
const std::function<void(Uint8*, SizeT)>& fill) override {
const XcbApi& api = Api();
if (width <= 0 || height <= 0 || width > 0xffff || height > 0xffff) return false;
Segment& segment = m_segments[m_next];
m_next = (m_next + 1) % 2;
// The X server may still be reading this segment's last frame: the round trip queued
// behind that put coming back says it is done with it.
if (segment.HasSync) {
std::free(api.xcb_get_input_focus_reply(m_c, segment.Sync, nullptr));
segment.HasSync = false;
}
const SizeT stride = static_cast<SizeT>(width) * 4;
const SizeT size = stride * static_cast<SizeT>(height);
if (segment.Size < size && !Make(segment, size)) return false;
fill(static_cast<Uint8*>(segment.Map), stride);
api.xcb_shm_put_image(m_c, drawable, gc, static_cast<uint16_t>(width), static_cast<uint16_t>(height), 0,
0, static_cast<uint16_t>(width), static_cast<uint16_t>(height), 0, 0,
static_cast<uint8_t>(depth), XCB_IMAGE_FORMAT_Z_PIXMAP, 0, segment.Seg, 0);
segment.Sync = api.xcb_get_input_focus(m_c);
segment.HasSync = true;
api.xcb_flush(m_c);
return true;
}
private:
struct Segment {
Uint32 Seg = 0;
void* Map = nullptr;
SizeT Size = 0;
xcb_get_input_focus_cookie_t Sync{};
Bool HasSync = false;
};
void Release(Segment& segment) {
const XcbApi& api = Api();
if (segment.HasSync) std::free(api.xcb_get_input_focus_reply(m_c, segment.Sync, nullptr));
if (segment.Seg != 0) api.xcb_shm_detach(m_c, segment.Seg);
if (segment.Map != nullptr) ::munmap(segment.Map, segment.Size);
segment = Segment{};
}
Bool Make(Segment& segment, SizeT size) {
const XcbApi& api = Api();
Release(segment);
const int fd = ::memfd_create("mobilegl-glx-shm", MFD_CLOEXEC);
if (fd < 0) return false;
if (::ftruncate(fd, static_cast<off_t>(size)) != 0) {
::close(fd);
return false;
}
void* map = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
::close(fd);
return false;
}
const Uint32 seg = api.xcb_generate_id(m_c);
// libxcb sends, then closes, the descriptor; the mapping stays.
const xcb_void_cookie_t cookie = api.xcb_shm_attach_fd_checked(m_c, seg, fd, 1);
if (xcb_generic_error_t* error = api.xcb_request_check(m_c, cookie)) {
std::free(error);
::munmap(map, size);
return false;
}
segment.Seg = seg;
segment.Map = map;
segment.Size = size;
return true;
}
xcb_connection_t* m_c = nullptr;
Segment m_segments[2];
SizeT m_next = 0;
};
} // namespace
ConnectionCaps QueryConnectionCaps(void* xcbConnection) { return Query(xcbConnection).Caps; }
UniquePtr<Connection> CreateXcbConnection(void* xcbConnection, Uint32 window, const ConnectionCaps& caps) {
const XcbApi& api = Api();
if (!api.core || !api.dri3 || !api.present || caps.Dri3Major < 1 || caps.PresentMajor < 1) return nullptr;
const CachedCaps cached = Query(xcbConnection);
const Bool buffers12 = caps.Dri3Major > 1 || (caps.Dri3Major == 1 && caps.Dri3Minor >= 2);
auto connection = MakeUnique<XcbConnection>(Xcb(xcbConnection), window, buffers12, cached.XFixes);
if (!connection->Init()) return nullptr;
return connection;
}
UniquePtr<ShmPresenter> CreateShmPresenter(void* xcbConnection) {
const XcbApi& api = Api();
if (!api.core || !api.shm || xcbConnection == nullptr) return nullptr;
return MakeUnique<XcbShmPresenter>(Xcb(xcbConnection));
}
} // namespace MobileGL::MG_Impl::GLXImpl::X11Present
#else // !MOBILEGL_GLX_XCB_PRESENT
namespace MobileGL::MG_Impl::GLXImpl::X11Present {
ConnectionCaps QueryConnectionCaps(void*) { return {}; }
UniquePtr<Connection> CreateXcbConnection(void*, Uint32, const ConnectionCaps&) { return nullptr; }
UniquePtr<ShmPresenter> CreateShmPresenter(void*) { return nullptr; }
} // namespace MobileGL::MG_Impl::GLXImpl::X11Present
#endif
+48
View File
@@ -0,0 +1,48 @@
// MobileGL - MobileGL/MG_Impl/GLXImpl/X11PresentXcb.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
// The X side of X11Present over libxcb: DRI3, Present, XFixes and MIT-SHM on the application's own
// connection (XGetXCBConnection of its Display - this library never opens one). Every libxcb entry
// point is resolved at run time, like the rest of the GLX layer, so nothing links libxcb; the
// protocol headers are only needed at build time, and a build without them has no DRI3 path
// (MOBILEGL_GLX_XCB_PRESENT is 0 and every window takes the readback path).
#include "X11Present.h"
#if defined(__linux__) && !defined(__ANDROID__) && __has_include(<xcb/dri3.h>) && __has_include(<xcb/present.h>) && \
__has_include(<xcb/xfixes.h>) && __has_include(<xcb/shm.h>)
#define MOBILEGL_GLX_XCB_PRESENT 1
#else
#define MOBILEGL_GLX_XCB_PRESENT 0
#endif
namespace MobileGL::MG_Impl::GLXImpl::X11Present {
// What the connection offers (queried once per connection). SharedImages is left false: that
// one is the backend's to answer.
ConnectionCaps QueryConnectionCaps(void* xcbConnection);
// A window's Present events on a special-event queue of their own, its DRI3 imports and its
// PresentPixmaps. Null when the connection lacks DRI3/Present or the queue cannot be set up.
UniquePtr<Connection> CreateXcbConnection(void* xcbConnection, Uint32 window, const ConnectionCaps& caps);
// THE MIT-SHM FALLBACK: frames read back into a shared segment (a memfd passed with
// ShmAttachFd, so no SysV memory has to be shared with the X server) and put with ShmPutImage -
// the pixels reach the X server without travelling down its socket. Two segments, alternating;
// a segment is written again only after a round trip queued behind its last put has come back.
class ShmPresenter {
public:
virtual ~ShmPresenter() = default;
// `fill(rows, stride)` writes a width x height top-down 32bpp frame; then it is put on
// `drawable` with `gc` at (0,0). False: not shown (the segment could not be made).
virtual Bool Put(Uint32 drawable, Uint32 gc, Uint32 depth, Int32 width, Int32 height,
const std::function<void(Uint8*, SizeT)>& fill) = 0;
};
UniquePtr<ShmPresenter> CreateShmPresenter(void* xcbConnection);
} // namespace MobileGL::MG_Impl::GLXImpl::X11Present
+20
View File
@@ -107,8 +107,28 @@ target_link_libraries(
${LINK_LIBRARIES}
)
# GLX presentation through DRI3 + Present against a fake X server and fake shared images, and the
# glFlush-as-shared-image-boundary policy (docs/Disaggregated/notes/anland/plan-x11-gpu.md).
add_executable(
X11PresentTest
X11PresentTest.cpp
SharedImageFlushPolicyTest.cpp
)
target_include_directories(X11PresentTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
X11PresentTest PRIVATE
GTest::gtest_main
${LINK_LIBRARIES}
)
include(GoogleTest)
gtest_discover_tests(X11DisplayGuardTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(X11PresentTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
gtest_discover_tests(PipeStatsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_Test/Util/SharedImageFlushPolicyTest.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
// When a glFlush publishes the session's shared-image accesses (implicit sync for a producer that
// renders into its dma-bufs, Xwayland's glamor): MG_Impl/EGLImpl/SharedImageFlushPolicy.h.
#include <MG_Impl/EGLImpl/SharedImageFlushPolicy.h>
#include <gtest/gtest.h>
using MobileGL::MG_Impl::EGLImpl::FlushPublishesSharedImageAccesses;
TEST(SharedImageFlushPolicy, AnXServerThatBoundImagesToTexturesFlushesAsABoundary) {
// Xwayland: offscreen session, glamor bound its window pixmaps' images to textures.
EXPECT_TRUE(FlushPublishesSharedImageAccesses(true, true, false, nullptr));
// An ordinary GL client never bound one: its flushes stay free.
EXPECT_FALSE(FlushPublishesSharedImageAccesses(true, false, false, nullptr));
// The compositor (owns the server window) samples client buffers and ends frames at its swaps.
EXPECT_FALSE(FlushPublishesSharedImageAccesses(true, true, true, nullptr));
// Without shared images (a monolith) there is nothing to publish, whatever is asked.
EXPECT_FALSE(FlushPublishesSharedImageAccesses(false, true, false, nullptr));
EXPECT_FALSE(FlushPublishesSharedImageAccesses(false, true, false, "1"));
}
TEST(SharedImageFlushPolicy, TheOverrideForcesEitherWay) {
EXPECT_FALSE(FlushPublishesSharedImageAccesses(true, true, false, "0"));
EXPECT_FALSE(FlushPublishesSharedImageAccesses(true, true, false, "off"));
EXPECT_TRUE(FlushPublishesSharedImageAccesses(true, false, true, "1"));
EXPECT_TRUE(FlushPublishesSharedImageAccesses(true, true, true, "on"));
// Anything else is the automatic answer.
EXPECT_TRUE(FlushPublishesSharedImageAccesses(true, true, false, ""));
EXPECT_FALSE(FlushPublishesSharedImageAccesses(true, true, true, "auto"));
}
+387
View File
@@ -0,0 +1,387 @@
// MobileGL - MobileGL/MG_Test/Util/X11PresentTest.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
// GLX presentation through DRI3 + Present (MG_Impl/GLXImpl/X11Present.h), driven with a fake X
// server and fake shared images: which path a window takes, how the images rotate through the X
// server's hands (PresentPixmap -> IdleNotify), how swaps are paced on CompleteNotify, what a resize
// does, and which failures send the window back to the readback path.
#include <MG_Impl/GLXImpl/X11Present.h>
#include <gtest/gtest.h>
#include <deque>
#include <functional>
#include <map>
#include <unistd.h>
using namespace MobileGL;
namespace XP = MobileGL::MG_Impl::GLXImpl::X11Present;
using MobileGL::MG_Util::Damage::Rect;
using MobileGL::MG_Util::Damage::Region;
namespace {
struct Clock {
Int64 now = 1000;
};
struct FakeImages final : XP::ImageSource {
Uint64 nextId = 1;
std::map<Uint64, std::pair<Uint32, Uint32>> live; // id -> size
Vector<Uint64> released;
Vector<std::pair<Uint64, Region>> copies;
Bool failAllocate = false;
Bool failCopy = false;
Bool Allocate(Uint32 width, Uint32 height, Uint32 fourcc, XP::Image* out) override {
if (failAllocate) return false;
out->Id = nextId++;
out->Fd = ::dup(0); // any descriptor: the chain closes it once the pixmap holds the memory
out->Width = width;
out->Height = height;
out->Stride = width * 4;
out->Fourcc = fourcc;
live[out->Id] = {width, height};
return true;
}
void Release(Uint64 id) override {
live.erase(id);
released.push_back(id);
}
Bool CopyFrame(Uint64 id, const Region& region) override {
copies.emplace_back(id, region);
return !failCopy;
}
};
struct Presented {
Uint32 pixmap = 0;
Uint32 serial = 0;
Uint32 options = 0;
Uint64 targetMsc = 0;
Region update;
};
struct FakeX final : XP::Connection {
Clock& clock;
std::deque<XP::Event> events;
Uint32 nextPixmap = 0x400000;
std::map<Uint32, Uint64> pixmaps; // pixmap -> image id
Vector<Uint32> freed;
Vector<Presented> presents;
Bool refuseImport = false;
Bool presentFailed = false;
Uint32 lastImportDepth = 0;
// Called whenever the chain waits with nothing queued: the X server's turn to answer.
std::function<void(FakeX&)> serverTurn;
explicit FakeX(Clock& c) : clock(c) {}
Uint32 ImportPixmap(const XP::Image& image, Uint32 depth) override {
EXPECT_GE(image.Fd, 0);
lastImportDepth = depth;
if (refuseImport) return 0;
pixmaps[++nextPixmap] = image.Id;
return nextPixmap;
}
void FreePixmap(Uint32 pixmap) override {
freed.push_back(pixmap);
pixmaps.erase(pixmap);
}
Bool PresentPixmap(Uint32 pixmap, Uint32 serial, Uint32 options, Uint64 targetMsc, const Region& update) override {
presents.push_back({pixmap, serial, options, targetMsc, update});
return true;
}
Bool PresentFailed() override { return presentFailed; }
Bool PollEvent(XP::Event* out) override {
if (events.empty()) return false;
*out = events.front();
events.pop_front();
return true;
}
Bool WaitEvent(XP::Event* out, Int64 timeoutMs) override {
if (events.empty() && serverTurn) serverTurn(*this);
if (events.empty()) {
clock.now += timeoutMs;
return false;
}
clock.now += 1;
return PollEvent(out);
}
void Flush() override {}
void Idle(Uint32 pixmap) {
XP::Event e;
e.Type = XP::Event::Kind::Idle;
e.Pixmap = pixmap;
events.push_back(e);
}
void Complete(Uint32 serial, Uint64 msc) {
XP::Event e;
e.Type = XP::Event::Kind::Complete;
e.Serial = serial;
e.Msc = msc;
events.push_back(e);
}
void Configure(Int32 w, Int32 h, Bool destroyed = false) {
XP::Event e;
e.Type = XP::Event::Kind::Configure;
e.Width = w;
e.Height = h;
e.WindowDestroyed = destroyed;
events.push_back(e);
}
};
struct Rig {
Clock clock;
FakeImages images;
FakeX x{clock};
std::unique_ptr<XP::Chain> chain;
explicit Rig(Uint32 depth = 24, Int32 w = 64, Int32 h = 48) {
chain = std::make_unique<XP::Chain>(x, images, depth, w, h, [this] { return clock.now; });
}
};
} // namespace
TEST(X11PresentPath, Dri3WhenTheConnectionAndTheBackendHaveEverything) {
XP::ConnectionCaps all;
all.SharedImages = true;
all.LocalConnection = true;
all.Dri3Major = 1;
all.Dri3Minor = 2;
all.PresentMajor = 1;
all.PresentMinor = 2;
all.ShmMajor = 1;
all.ShmMinor = 2;
EXPECT_EQ(XP::SelectPath(all, 24, nullptr), XP::Path::Dri3Present);
EXPECT_EQ(XP::SelectPath(all, 32, nullptr), XP::Path::Dri3Present);
EXPECT_EQ(XP::SelectPath(all, 16, nullptr), XP::Path::ShmPutImage) << "no shared-image format for depth 16";
EXPECT_EQ(XP::SelectPath(all, 24, "readback"), XP::Path::ShmPutImage);
EXPECT_EQ(XP::SelectPath(all, 24, "putimage"), XP::Path::PutImage);
XP::ConnectionCaps shmOnly = all; // Xwayland -shm: no glamor, so no DRI3
shmOnly.Dri3Major = shmOnly.Dri3Minor = 0;
EXPECT_EQ(XP::SelectPath(shmOnly, 24, nullptr), XP::Path::ShmPutImage);
XP::ConnectionCaps monolith = all; // a backend without shared images
monolith.SharedImages = false;
EXPECT_EQ(XP::SelectPath(monolith, 24, nullptr), XP::Path::ShmPutImage);
XP::ConnectionCaps oldShm = shmOnly; // MIT-SHM 1.1: SysV only
oldShm.ShmMinor = 1;
EXPECT_EQ(XP::SelectPath(oldShm, 24, nullptr), XP::Path::PutImage);
XP::ConnectionCaps remote = all; // TCP: no descriptor travels
remote.LocalConnection = false;
EXPECT_EQ(XP::SelectPath(remote, 24, nullptr), XP::Path::PutImage);
XP::ConnectionCaps noPresent = all;
noPresent.PresentMajor = 0;
EXPECT_EQ(XP::SelectPath(noPresent, 24, nullptr), XP::Path::ShmPutImage);
EXPECT_EQ(XP::FourccForDepth(24), XP::kFourccXrgb8888);
EXPECT_EQ(XP::FourccForDepth(32), XP::kFourccArgb8888);
EXPECT_EQ(XP::FourccForDepth(30), 0u);
}
TEST(X11PresentChain, AFrameIsCopiedIntoAnImportedImageAndPresented) {
Rig rig(32, 64, 48);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
ASSERT_EQ(rig.images.live.size(), 1u);
const auto& [id, size] = *rig.images.live.begin();
EXPECT_EQ(size, std::make_pair(64u, 48u));
EXPECT_EQ(rig.x.lastImportDepth, 32u);
ASSERT_EQ(rig.images.copies.size(), 1u);
EXPECT_EQ(rig.images.copies[0].first, id);
EXPECT_TRUE(rig.images.copies[0].second.IsFull());
ASSERT_EQ(rig.x.presents.size(), 1u);
EXPECT_EQ(rig.x.pixmaps.at(rig.x.presents[0].pixmap), id);
EXPECT_EQ(rig.x.presents[0].options, XP::kPresentOptionAsync);
EXPECT_EQ(rig.x.presents[0].serial, 1u);
EXPECT_EQ(rig.chain->BusyBuffers(), 1u);
}
TEST(X11PresentChain, AnImageComesBackWithIdleNotifyAndIsReused) {
Rig rig;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
const Uint32 first = rig.x.presents[0].pixmap;
rig.x.Idle(first);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.x.presents[1].pixmap, first) << "the idle image is taken before a new one is made";
EXPECT_EQ(rig.chain->AllocatedBuffers(), 1u);
// Held by the server: the next frame takes a second image, and a third.
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.chain->AllocatedBuffers(), 3u);
EXPECT_EQ(rig.chain->BusyBuffers(), 3u);
// Idle for an unknown pixmap is ignored.
rig.x.Idle(0x1234);
rig.x.Idle(rig.x.presents[2].pixmap);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.x.presents[4].pixmap, rig.x.presents[2].pixmap);
EXPECT_EQ(rig.chain->AllocatedBuffers(), 3u);
}
TEST(X11PresentChain, AllImagesHeldWaitsThenGrowsToFourThenDropsTheFrame) {
Rig rig;
for (int i = 0; i < 3; ++i) ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
const Int64 before = rig.clock.now;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0)) << "a fourth image after the idle wait";
EXPECT_GE(rig.clock.now - before, XP::Chain::kIdleWaitMs);
EXPECT_EQ(rig.chain->AllocatedBuffers(), XP::Chain::kMaxBuffers);
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0)) << "past the limit the frame is dropped";
EXPECT_FALSE(rig.chain->Broken()) << "a slow server does not end the chain";
EXPECT_EQ(rig.x.presents.size(), 4u);
// The server answers while the chain waits: no fifth image, no dropped frame.
rig.x.serverTurn = [](FakeX& x) { x.Idle(x.presents.front().pixmap); };
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.x.presents.back().pixmap, rig.x.presents.front().pixmap);
EXPECT_EQ(rig.chain->AllocatedBuffers(), XP::Chain::kMaxBuffers);
}
TEST(X11PresentChain, IntervalOneWaitsForThePreviousCompletionAndTargetsTheNextMsc) {
Rig rig;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 1));
EXPECT_EQ(rig.x.presents[0].options, 0u);
EXPECT_EQ(rig.x.presents[0].targetMsc, 0u) << "no MSC known yet";
rig.x.serverTurn = [](FakeX& x) { x.Complete(1, 500); };
ASSERT_TRUE(rig.chain->Present(Region::Full(), 1));
EXPECT_EQ(rig.x.presents[1].targetMsc, 501u);
EXPECT_TRUE(rig.x.events.empty());
// Interval 2 targets two frames on.
rig.x.serverTurn = [](FakeX& x) { x.Complete(2, 501); };
ASSERT_TRUE(rig.chain->Present(Region::Full(), 2));
EXPECT_EQ(rig.x.presents[2].targetMsc, 503u);
// A server that never completes stalls a paced swap only so long.
rig.x.serverTurn = nullptr;
const Int64 before = rig.clock.now;
rig.x.Idle(rig.x.presents[0].pixmap);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 1));
EXPECT_GE(rig.clock.now - before, XP::Chain::kCompleteGiveUpMs);
EXPECT_LT(rig.clock.now - before, XP::Chain::kCompleteGiveUpMs + XP::Chain::kIdleWaitMs);
}
TEST(X11PresentChain, IntervalZeroIsUnpacedUntilACompletionIsOverdue) {
Rig rig;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
rig.x.Idle(rig.x.presents[0].pixmap);
Int64 before = rig.clock.now;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.clock.now, before) << "the completion is outstanding but not overdue";
// Nothing of the window is shown: no completion for 250 ms.
rig.clock.now += 250;
rig.x.Idle(rig.x.presents[1].pixmap);
before = rig.clock.now;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_GE(rig.clock.now - before, 1000) << "about one frame a second while nothing is shown";
EXPECT_LE(rig.clock.now - before, 1001);
// Completions come back: unpaced again.
rig.x.Complete(1, 10);
rig.x.Idle(rig.x.presents[2].pixmap);
before = rig.clock.now;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.clock.now, before);
}
TEST(X11PresentChain, AResizeRetiresOldImagesAsTheyComeBack) {
Rig rig(24, 64, 48);
const auto newSize = std::make_pair(128u, 96u);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
const Uint32 a = rig.x.presents[0].pixmap;
const Uint32 b = rig.x.presents[1].pixmap;
// Both with the server; a comes back, b does not.
rig.x.Idle(a);
rig.chain->Resize(128, 96);
EXPECT_EQ(rig.chain->Width(), 128);
EXPECT_TRUE(rig.x.freed.empty()) << "both were still with the server at the resize";
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
const Uint32 c = rig.x.presents.back().pixmap;
EXPECT_NE(c, a);
EXPECT_NE(c, b);
EXPECT_EQ(rig.images.live.at(rig.x.pixmaps.at(c)), newSize);
EXPECT_EQ(rig.x.freed, Vector<Uint32>{a}) << "a came back at the old size and was replaced";
EXPECT_EQ(rig.x.pixmaps.count(b), 1u) << "b is still with the server";
// b comes back: the next frame that would take it gets a new-size image instead.
rig.x.Idle(b);
rig.x.Idle(c);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.x.pixmaps.count(b), 0u);
for (const auto& [id, size] : rig.images.live) EXPECT_EQ(size, newSize);
}
TEST(X11PresentChain, ConfigureNotifyReportsTheNewSizeOnce) {
Rig rig;
rig.x.Configure(300, 200);
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
Int32 w = 0, h = 0;
ASSERT_TRUE(rig.chain->TakeConfiguredSize(&w, &h));
EXPECT_EQ(w, 300);
EXPECT_EQ(h, 200);
EXPECT_FALSE(rig.chain->TakeConfiguredSize(&w, &h));
}
TEST(X11PresentChain, PartialDamageIsFlippedIntoTheUpdateRegion) {
Rig rig(24, 100, 100);
const Rect gl{10, 0, 20, 5}; // bottom-left origin: the bottom rows
ASSERT_TRUE(rig.chain->Present(Region::FromRects(&gl, 1), 0));
ASSERT_FALSE(rig.x.presents[0].update.IsFull());
ASSERT_EQ(rig.x.presents[0].update.Rects().size(), 1u);
EXPECT_EQ(rig.x.presents[0].update.Rects()[0], (Rect{10, 95, 20, 5}));
EXPECT_TRUE(rig.images.copies[0].second.IsFull()) << "a new image holds no frame yet: copied whole";
}
TEST(X11PresentChain, FailuresSendTheWindowBackToReadback) {
{
Rig rig;
rig.x.refuseImport = true;
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
EXPECT_TRUE(rig.chain->Broken());
EXPECT_TRUE(rig.images.live.empty()) << "the refused image is released";
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
}
{
Rig rig;
rig.images.failAllocate = true;
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
EXPECT_TRUE(rig.chain->Broken());
}
{
Rig rig;
rig.images.failCopy = true;
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
EXPECT_TRUE(rig.chain->Broken());
EXPECT_TRUE(rig.x.presents.empty()) << "nothing is presented that was not copied";
}
{
Rig rig;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
rig.x.presentFailed = true;
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
EXPECT_TRUE(rig.chain->Broken());
}
{
Rig rig;
ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
rig.x.Configure(64, 48, /*destroyed=*/true);
EXPECT_FALSE(rig.chain->Present(Region::Full(), 0));
EXPECT_TRUE(rig.chain->Broken());
}
{
Rig rig(16);
EXPECT_TRUE(rig.chain->Broken()) << "depth 16 has no shared-image format";
}
}
TEST(X11PresentChain, TheChainFreesEveryPixmapAndReleasesEveryImage) {
Rig rig;
for (int i = 0; i < 3; ++i) ASSERT_TRUE(rig.chain->Present(Region::Full(), 0));
EXPECT_EQ(rig.images.live.size(), 3u);
rig.chain.reset();
EXPECT_TRUE(rig.images.live.empty());
EXPECT_TRUE(rig.x.pixmaps.empty());
EXPECT_EQ(rig.x.freed.size(), 3u);
}