[Fix] (EGL): pace Wayland window swaps by frame callbacks so clients idle while the compositor shows nothing

This commit is contained in:
2026-10-02 23:10:33 -04:00
parent b1405e17ad
commit 1ff7ca5331
8 changed files with 178 additions and 6 deletions
+2 -1
View File
@@ -457,7 +457,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
#if MOBILEGL_WAYLAND_WINDOWS
// A Wayland window shows what is attached to it: the frame is read back and attached
// BEFORE the swap, while it is still the drawable's content.
if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end() && !it->second->Present(damage)) {
if (auto it = WaylandSurfaces().find(draw);
it != WaylandSurfaces().end() && !it->second->Present(damage, state->GetSwapInterval(dpy))) {
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
+45
View File
@@ -0,0 +1,45 @@
// MobileGL - MobileGL/MG_Impl/EGLImpl/FrameThrottle.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
// HOW A WAYLAND WINDOW'S SWAPS ARE PACED. A Wayland compositor tells a client when to draw next with
// wl_surface.frame: the callback is done when the compositor has used the frame - painted it, or
// decided it would not be seen - and a compositor whose output is off (DPMS off, nothing on screen)
// sends none at all. An EGL window surface with a swap interval above 0 asks for that callback with
// every commit, and the swap after it waits until it is done. So an application drawing as fast as
// eglSwapBuffers lets it draws at the compositor's pace, and stops - instead of burning CPU and GPU
// on frames nobody sees - while the compositor shows nothing. With interval 0 nothing waits.
//
// The bookkeeping, apart from libwayland (WaylandWindow.cpp issues the requests and dispatches).
#include <Includes.h>
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
class FrameThrottle {
public:
// Before a commit: whether it should carry a frame request. One callback is outstanding at
// a time; the commits of an interval-0 window carry none.
Bool WantsFrameRequest(Int swapInterval) const { return swapInterval > 0 && !m_outstanding; }
// A frame request went with the commit.
void Requested() { m_outstanding = true; }
// The compositor answered the request (wl_callback.done).
void Done() { m_outstanding = false; }
// Before a swap's frame is drawn into a buffer: whether it has to wait for the callback
// first. An interval of 0 never waits, even for a request a previous interval made.
Bool MustWait(Int swapInterval) const { return swapInterval > 0 && m_outstanding; }
Bool Outstanding() const { return m_outstanding; }
private:
Bool m_outstanding = false;
};
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
+67 -4
View File
@@ -7,6 +7,7 @@
// End of Source File Header
#include "WaylandWindow.h"
#include "FrameThrottle.h"
#if MOBILEGL_WAYLAND_WINDOWS
@@ -117,6 +118,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
constexpr uint32_t kBufferDestroy = 0;
constexpr uint32_t kSurfaceAttach = 1;
constexpr uint32_t kSurfaceDamage = 2;
constexpr uint32_t kSurfaceFrame = 3;
constexpr uint32_t kSurfaceCommit = 6;
constexpr uint32_t kSurfaceDamageBuffer = 9; // since wl_surface version 4
constexpr uint32_t kMarshalFlagDestroy = 1u << 0;
@@ -144,6 +146,8 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
const wl_interface* shmInterface = nullptr;
const wl_interface* shmPoolInterface = nullptr;
const wl_interface* bufferInterface = nullptr;
// Optional: without it a window's swaps are not paced by frame callbacks.
const wl_interface* callbackInterface = nullptr;
wl_proxy* (*marshalFlags)(wl_proxy*, uint32_t, const wl_interface*, uint32_t, uint32_t, ...) = nullptr;
int (*addListener)(wl_proxy*, void (**)(void), void*) = nullptr;
void (*destroy)(wl_proxy*) = nullptr;
@@ -174,6 +178,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
api.shmInterface = static_cast<const wl_interface*>(sym("wl_shm_interface"));
api.shmPoolInterface = static_cast<const wl_interface*>(sym("wl_shm_pool_interface"));
api.bufferInterface = static_cast<const wl_interface*>(sym("wl_buffer_interface"));
api.callbackInterface = static_cast<const wl_interface*>(sym("wl_callback_interface"));
api.marshalFlags = reinterpret_cast<decltype(api.marshalFlags)>(sym("wl_proxy_marshal_flags"));
api.addListener = reinterpret_cast<decltype(api.addListener)>(sym("wl_proxy_add_listener"));
api.destroy = reinterpret_cast<decltype(api.destroy)>(sym("wl_proxy_destroy"));
@@ -328,8 +333,17 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
EGLint committedWidth = 0;
EGLint committedHeight = 0;
// SWAP PACING (FrameThrottle.h). The frame request goes through a wrapper of the
// application's wl_surface that lives on this window's queue, so its callback is
// dispatched here and never by the application.
void* surfaceWrapper = nullptr;
wl_proxy* frameCallback = nullptr;
FrameThrottle throttle;
~Impl() {
const Api& api = WaylandApi();
if (frameCallback) api.destroy(frameCallback);
if (surfaceWrapper) api.wrapperDestroy(surfaceWrapper);
for (auto& buffer : buffers) Release(buffer);
for (auto& buffer : dmabufBuffers) Release(buffer);
if (dmabuf) api.marshalFlags(dmabuf, kDmabufDestroy, nullptr, api.getVersion(dmabuf), kMarshalFlagDestroy);
@@ -523,14 +537,41 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
useDmabuf = false;
}
void FrameDone(wl_proxy* callback) {
if (callback != frameCallback) return;
WaylandApi().destroy(callback);
frameCallback = nullptr;
throttle.Done();
}
// A swap with an interval above 0 waits for the callback of the frame before it: the
// compositor has used that one. It sends none while it shows nothing (its output is off),
// and then this - like any EGL on Wayland - waits until it shows something again.
void WaitForFrame(Int swapInterval) {
const Api& api = WaylandApi();
while (throttle.MustWait(swapInterval)) {
if (api.dispatchQueue(display, queue) < 0) {
// A broken connection answers nothing again; stop pacing on it.
if (frameCallback) api.destroy(frameCallback);
frameCallback = nullptr;
throttle.Done();
return;
}
}
}
// The next commit asks for a frame callback (interval above 0, none outstanding).
void RequestFrame(wl_proxy* surface, Int swapInterval);
// `wlBuffer` becomes the window's content: attach, damage, commit. `damage` is the frame's
// (GL window coordinates); a buffer's rows run top-down, so each rectangle is flipped. A
// compositor without damage_buffer (wl_surface < 4), a new size or the first commit is
// damaged whole.
void Commit(wl_proxy* wlBuffer, const MG_Util::Damage::Region& damage) {
void Commit(wl_proxy* wlBuffer, const MG_Util::Damage::Region& damage, Int swapInterval) {
const Api& api = WaylandApi();
wl_proxy* surface = window->surface;
const uint32_t surfaceVersion = api.getVersion(surface);
RequestFrame(surface, swapInterval);
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, wlBuffer, window->dx, window->dy);
const Bool whole = damage.IsFull() || surfaceVersion < 4 || committedWidth != width ||
committedHeight != height || window->dx != 0 || window->dy != 0;
@@ -581,6 +622,11 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
reinterpret_cast<void (*)(void)>(&OnRegistryGlobalRemove)};
void OnFrameDone(void* data, wl_proxy* callback, uint32_t) {
static_cast<WindowSurface::Impl*>(data)->FrameDone(callback);
}
void (*const kFrameListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnFrameDone)};
// The application destroyed its wl_egl_window first: nothing may be read from it after this.
void OnWindowDestroyed(void* data) { static_cast<WindowSurface::Impl*>(data)->window = nullptr; }
} // namespace
@@ -627,6 +673,22 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
return eglWindow->width > 0 && eglWindow->height > 0;
}
void WindowSurface::Impl::RequestFrame(wl_proxy* surface, Int swapInterval) {
const Api& api = WaylandApi();
if (api.callbackInterface == nullptr || !throttle.WantsFrameRequest(swapInterval)) return;
if (surfaceWrapper == nullptr) {
surfaceWrapper = api.createWrapper(surface);
if (surfaceWrapper == nullptr) return;
api.setQueue(static_cast<wl_proxy*>(surfaceWrapper), queue);
}
auto* wrapper = static_cast<wl_proxy*>(surfaceWrapper);
frameCallback = api.marshalFlags(wrapper, kSurfaceFrame, api.callbackInterface, api.getVersion(wrapper), 0,
nullptr);
if (frameCallback == nullptr) return;
api.addListener(frameCallback, const_cast<void (**)(void)>(kFrameListener), this);
throttle.Requested();
}
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
WindowSurface::~WindowSurface() = default;
@@ -692,9 +754,10 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
return true;
}
Bool WindowSurface::Present(const MG_Util::Damage::Region& damage) {
Bool WindowSurface::Present(const MG_Util::Damage::Region& damage, Int swapInterval) {
Impl& impl = *m_impl;
if (impl.window == nullptr) return false;
impl.WaitForFrame(swapInterval);
if (impl.useDmabuf) {
// The server copies the frame into the image GPU-side, top row first - a wl_buffer's
@@ -707,7 +770,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
const SizeT index = static_cast<SizeT>(buffer - impl.dmabufBuffers);
const MG_Util::Damage::Region copy = impl.dmabufDamage.TakeForWrite(index, damage, impl.width, impl.height);
if (backendObject->PresentToSharedImage(buffer->imageId, copy)) {
impl.Commit(buffer->buffer, damage);
impl.Commit(buffer->buffer, damage, swapInterval);
buffer->busy = true;
return true;
}
@@ -726,7 +789,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
ReadBackFrameBGRA(impl.width, impl.height, impl.scratch, static_cast<Uint8*>(buffer->map),
static_cast<SizeT>(buffer->stride));
impl.Commit(buffer->buffer, MG_Util::Damage::Region::Full());
impl.Commit(buffer->buffer, MG_Util::Damage::Region::Full(), swapInterval);
buffer->busy = true;
return true;
}
+4 -1
View File
@@ -82,7 +82,10 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// frame changed. A shared image is copied only where it differs from the frame - the
// frame's damage and every frame's since that image was last written - and the surface is
// damaged only where the frame changed, flipped to the buffer's top-left origin.
Bool Present(const MG_Util::Damage::Region& damage);
//
// `swapInterval` above 0 paces the swaps by the compositor's frame callbacks: this waits
// for the previous frame's before taking a buffer (FrameThrottle.h).
Bool Present(const MG_Util::Damage::Region& damage, Int swapInterval);
// wl_egl_window_resize's effect, taken: true (with the new size) when the application gave
// its wl_egl_window a size this presentation is not at yet. The presentation adopts it -
+6
View File
@@ -1260,6 +1260,12 @@ namespace MobileGL {
return surf && surf->Display == display;
}
EGLint EGLContext::GetSwapInterval(EGLDisplayHandle display) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* displayObject = TryGetDisplay(display);
return displayObject ? displayObject->SwapInterval : 1;
}
Bool EGLContext::SwapInterval(EGLDisplayHandle display, EGLint interval) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto* displayObject = TryGetDisplay(display);
+3
View File
@@ -105,6 +105,9 @@ namespace MobileGL {
Bool ValidateSurface(EGLSurfaceHandle surface) const;
Bool ValidateSurfaceOnDisplay(EGLDisplayHandle display, EGLSurfaceHandle surface) const;
Bool SwapInterval(EGLDisplayHandle display, EGLint interval);
// The interval eglSwapInterval last set on `display` (1, EGL's default, until then or
// for a display this state does not know).
EGLint GetSwapInterval(EGLDisplayHandle display) const;
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), MOBILEGL_IPC_SURFACE=server. A window surface whose
// window is the SERVER's: the client's native window may be NULL (a headless client
+1
View File
@@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.14)
add_executable(
EGLStateTest
EGLStateTest.cpp
FrameThrottleTest.cpp
)
target_include_directories(EGLStateTest PRIVATE
@@ -0,0 +1,50 @@
#include <gtest/gtest.h>
#include <MG_Impl/EGLImpl/FrameThrottle.h>
#include <MG_State/EGLState/Core.h>
// A Wayland window's swaps are paced by the compositor's frame callbacks when the swap interval is
// above 0 - which is what lets every client idle while the compositor shows nothing - and never
// when it is 0.
using MobileGL::MG_Impl::EGLImpl::Wayland::FrameThrottle;
TEST(WaylandFrameThrottle, IntervalOneWaitsForTheCallbackOfThePreviousCommit) {
FrameThrottle throttle;
EXPECT_FALSE(throttle.MustWait(1)); // the first swap has nothing to wait for
ASSERT_TRUE(throttle.WantsFrameRequest(1));
throttle.Requested();
EXPECT_TRUE(throttle.MustWait(1)); // the next swap waits ...
EXPECT_FALSE(throttle.WantsFrameRequest(1)); // ... and asks for no second callback meanwhile
throttle.Done();
EXPECT_FALSE(throttle.MustWait(1)); // the compositor used the frame
EXPECT_TRUE(throttle.WantsFrameRequest(1));
}
TEST(WaylandFrameThrottle, IntervalZeroNeitherAsksNorWaits) {
FrameThrottle throttle;
EXPECT_FALSE(throttle.WantsFrameRequest(0));
EXPECT_FALSE(throttle.MustWait(0));
}
TEST(WaylandFrameThrottle, DroppingToIntervalZeroStopsWaitingOnAnOutstandingCallback) {
FrameThrottle throttle;
throttle.Requested();
EXPECT_FALSE(throttle.MustWait(0));
EXPECT_TRUE(throttle.Outstanding()); // still answered later, then requested again at interval 1
EXPECT_FALSE(throttle.WantsFrameRequest(1));
throttle.Done();
EXPECT_TRUE(throttle.WantsFrameRequest(2));
}
TEST(EGLStateSwapInterval, DefaultsToOneAndFollowsEglSwapInterval) {
MobileGL::MG_State::EGLState::EGLContext state;
const EGLDisplay display = state.GetDisplay(EGL_DEFAULT_DISPLAY);
ASSERT_NE(display, EGL_NO_DISPLAY);
ASSERT_TRUE(state.InitializeDisplay(display, nullptr, nullptr));
EXPECT_EQ(state.GetSwapInterval(display), 1);
ASSERT_TRUE(state.SwapInterval(display, 0));
EXPECT_EQ(state.GetSwapInterval(display), 0);
EXPECT_FALSE(state.SwapInterval(display, 99)); // out of the config's range: refused, unchanged
EXPECT_EQ(state.GetSwapInterval(display), 0);
EXPECT_EQ(state.GetSwapInterval(EGL_NO_DISPLAY), 1);
}