diff --git a/CMakeLists.txt b/CMakeLists.txt index 63999257..6bdee069 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp b/MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp index d56623a6..c2326ec4 100644 --- a/MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp +++ b/MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp @@ -12,8 +12,11 @@ #include "../EGLImpl/EGLImpl.h" #include "../EGLImpl/WaylandWindow.h" #include "../GetProcAddress.h" +#include "X11Present.h" +#include "X11PresentXcb.h" #include #include +#include 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 Scratch; Vector 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 PresentConnection; + UniquePtr Images; + UniquePtr Chain; + UniquePtr 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::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(drawable), caps); + if (surface.PresentConnection) { + surface.Images = MakeUnique(); + surface.Chain = MakeUnique( + *surface.PresentConnection, *surface.Images, surface.Depth, static_cast(surface.Width), + static_cast(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(drawable), 0, nullptr); @@ -355,6 +480,15 @@ namespace MobileGL::MG_Impl::GLXImpl { const EGLint width = static_cast(surface.Width); const EGLint height = static_cast(surface.Height); const SizeT rowBytes = static_cast(width) * 4; + if (surface.Shm) { + const Bool put = surface.Shm->Put( + static_cast(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(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(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(width), static_cast(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(configuredWidth) != it->second.Width || + static_cast(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(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 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(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; } diff --git a/MobileGL/MG_Impl/GLXImpl/X11Present.cpp b/MobileGL/MG_Impl/GLXImpl/X11Present.cpp new file mode 100644 index 00000000..d0ff3d28 --- /dev/null +++ b/MobileGL/MG_Impl/GLXImpl/X11Present.cpp @@ -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 + +#if !defined(_WIN32) +#include +#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 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(&buffer - m_buffers)); + buffer = Buffer{}; + } + + Bool Chain::Allocate(Buffer& buffer) { + Drop(buffer); + Image image; + if (!m_images.Allocate(static_cast(m_width), static_cast(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(m_width) || image.Height != static_cast(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(&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(m_width) && + best->Storage.Height == static_cast(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(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(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 diff --git a/MobileGL/MG_Impl/GLXImpl/X11Present.h b/MobileGL/MG_Impl/GLXImpl/X11Present.h new file mode 100644 index 00000000..e34bd165 --- /dev/null +++ b/MobileGL/MG_Impl/GLXImpl/X11Present.h @@ -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 +#include + +#include "../EGLImpl/FrameThrottle.h" + +#include + +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 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 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 diff --git a/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp b/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp new file mode 100644 index 00000000..f22609d3 --- /dev/null +++ b/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp @@ -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 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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 + Bool Resolve(void* lib, const char* name, Fn& out) { + out = lib != nullptr ? reinterpret_cast(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(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(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(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(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(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& CapsCache() { + static auto* cache = new Vector(); + return *cache; + } + + CachedCaps Query(void* connection) { + const std::lock_guard 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(&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::steady_clock::now().time_since_epoch()) + .count(); + } + + Bool Translate(const xcb_generic_event_t* raw, Event* out) { + const auto* generic = reinterpret_cast(raw); + *out = Event{}; + switch (generic->evtype) { + case XCB_PRESENT_EVENT_IDLE_NOTIFY: { + const auto* idle = reinterpret_cast(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(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(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(image.Width), static_cast(image.Height), + image.Stride, image.Offset, 0, 0, 0, 0, 0, 0, static_cast(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(image.Width), + static_cast(image.Height), static_cast(image.Stride), + static_cast(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 rects; + for (const MG_Util::Damage::Rect& rect : update.Rects()) { + rects.push_back({static_cast(rect.X), static_cast(rect.Y), + static_cast(rect.Width), static_cast(rect.Height)}); + } + if (!rects.empty()) { + region = api.xcb_generate_id(m_c); + api.xcb_xfixes_create_region(m_c, region, static_cast(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(std::min(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& 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(width) * 4; + const SizeT size = stride * static_cast(height); + if (segment.Size < size && !Make(segment, size)) return false; + fill(static_cast(segment.Map), stride); + api.xcb_shm_put_image(m_c, drawable, gc, static_cast(width), static_cast(height), 0, + 0, static_cast(width), static_cast(height), 0, 0, + static_cast(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(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 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(Xcb(xcbConnection), window, buffers12, cached.XFixes); + if (!connection->Init()) return nullptr; + return connection; + } + + UniquePtr CreateShmPresenter(void* xcbConnection) { + const XcbApi& api = Api(); + if (!api.core || !api.shm || xcbConnection == nullptr) return nullptr; + return MakeUnique(Xcb(xcbConnection)); + } +} // namespace MobileGL::MG_Impl::GLXImpl::X11Present + +#else // !MOBILEGL_GLX_XCB_PRESENT + +namespace MobileGL::MG_Impl::GLXImpl::X11Present { + ConnectionCaps QueryConnectionCaps(void*) { return {}; } + UniquePtr CreateXcbConnection(void*, Uint32, const ConnectionCaps&) { return nullptr; } + UniquePtr CreateShmPresenter(void*) { return nullptr; } +} // namespace MobileGL::MG_Impl::GLXImpl::X11Present + +#endif diff --git a/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.h b/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.h new file mode 100644 index 00000000..db1b322a --- /dev/null +++ b/MobileGL/MG_Impl/GLXImpl/X11PresentXcb.h @@ -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() && __has_include() && \ + __has_include() && __has_include() +#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 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& fill) = 0; + }; + UniquePtr CreateShmPresenter(void* xcbConnection); +} // namespace MobileGL::MG_Impl::GLXImpl::X11Present diff --git a/MobileGL/MG_Test/Util/CMakeLists.txt b/MobileGL/MG_Test/Util/CMakeLists.txt index c3b600db..54ae51f0 100644 --- a/MobileGL/MG_Test/Util/CMakeLists.txt +++ b/MobileGL/MG_Test/Util/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Util/SharedImageFlushPolicyTest.cpp b/MobileGL/MG_Test/Util/SharedImageFlushPolicyTest.cpp new file mode 100644 index 00000000..bdd40bc3 --- /dev/null +++ b/MobileGL/MG_Test/Util/SharedImageFlushPolicyTest.cpp @@ -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 + +#include + +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")); +} diff --git a/MobileGL/MG_Test/Util/X11PresentTest.cpp b/MobileGL/MG_Test/Util/X11PresentTest.cpp new file mode 100644 index 00000000..9c943239 --- /dev/null +++ b/MobileGL/MG_Test/Util/X11PresentTest.cpp @@ -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 + +#include + +#include +#include +#include + +#include + +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> live; // id -> size + Vector released; + Vector> 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 events; + Uint32 nextPixmap = 0x400000; + std::map pixmaps; // pixmap -> image id + Vector freed; + Vector 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 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 chain; + explicit Rig(Uint32 depth = 24, Int32 w = 64, Int32 h = 48) { + chain = std::make_unique(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{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); +}