mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (GLX): present windows as shared images through DRI3 and Present, with MIT-SHM readback as the fallback
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user