mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (EGL): pace Wayland window swaps by frame callbacks so clients idle while the compositor shows nothing
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user