mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (EGL): slow an unpaced Wayland window to one frame a second while the compositor shows none of it
This commit is contained in:
@@ -10,11 +10,18 @@
|
||||
|
||||
// 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.
|
||||
// decided it would not be seen - and a compositor that shows nothing (its output is off, the window
|
||||
// is hidden) sends none at all. Every commit asks for one (one outstanding at a time), and the swap
|
||||
// after it may wait:
|
||||
//
|
||||
// - swap interval above 0: until the callback is done, however long that takes. An application
|
||||
// drawing as fast as eglSwapBuffers lets it then draws at the compositor's pace, and stops while
|
||||
// the compositor shows nothing (what every EGL on Wayland does).
|
||||
// - swap interval 0: not while the compositor keeps answering - an unpaced application stays
|
||||
// unpaced. But a callback left unanswered for kStarvedAfterMs means nothing of the window is
|
||||
// being shown; the swap then waits up to kStarvedWaitMs for it, so an application that draws on
|
||||
// its own clock (a browser on an animated page) drops to about one frame a second instead of
|
||||
// burning CPU and GPU on frames nobody sees, and is back at full rate with the first answer.
|
||||
//
|
||||
// The bookkeeping, apart from libwayland (WaylandWindow.cpp issues the requests and dispatches).
|
||||
|
||||
@@ -23,23 +30,34 @@
|
||||
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; }
|
||||
static constexpr Int64 kStarvedAfterMs = 200;
|
||||
static constexpr Int64 kStarvedWaitMs = 1000;
|
||||
static constexpr Int64 kForever = -1;
|
||||
|
||||
// A frame request went with the commit.
|
||||
void Requested() { m_outstanding = true; }
|
||||
// Before a commit: whether it carries a frame request (none is outstanding).
|
||||
Bool WantsFrameRequest() const { return !m_outstanding; }
|
||||
|
||||
// A frame request went with the commit made at `nowMs` (a monotonic clock).
|
||||
void Requested(Int64 nowMs) {
|
||||
m_outstanding = true;
|
||||
m_requestedAtMs = nowMs;
|
||||
}
|
||||
|
||||
// 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; }
|
||||
// Before a swap's frame is drawn into a buffer, at `nowMs`: how long it waits for the
|
||||
// outstanding callback - 0 = not at all, kForever = until it comes.
|
||||
Int64 WaitBudgetMs(Int swapInterval, Int64 nowMs) const {
|
||||
if (!m_outstanding) return 0;
|
||||
if (swapInterval > 0) return kForever;
|
||||
return nowMs - m_requestedAtMs >= kStarvedAfterMs ? kStarvedWaitMs : 0;
|
||||
}
|
||||
|
||||
Bool Outstanding() const { return m_outstanding; }
|
||||
|
||||
private:
|
||||
Bool m_outstanding = false;
|
||||
Int64 m_requestedAtMs = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <poll.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/uio.h>
|
||||
#include <unistd.h>
|
||||
@@ -161,6 +163,11 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
int (*dispatchQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*dispatchQueuePending)(void*, wl_event_queue*) = nullptr;
|
||||
int (*flush)(void*) = nullptr;
|
||||
// Optional: a bounded wait for a frame callback (FrameThrottle's interval-0 case).
|
||||
int (*prepareReadQueue)(void*, wl_event_queue*) = nullptr;
|
||||
int (*getFd)(void*) = nullptr;
|
||||
int (*readEvents)(void*) = nullptr;
|
||||
void (*cancelRead)(void*) = nullptr;
|
||||
};
|
||||
|
||||
// Loaded once. RTLD_NOLOAD first: a Wayland client has the library already, and the copy
|
||||
@@ -193,6 +200,11 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
api.dispatchQueuePending =
|
||||
reinterpret_cast<decltype(api.dispatchQueuePending)>(sym("wl_display_dispatch_queue_pending"));
|
||||
api.flush = reinterpret_cast<decltype(api.flush)>(sym("wl_display_flush"));
|
||||
api.prepareReadQueue =
|
||||
reinterpret_cast<decltype(api.prepareReadQueue)>(sym("wl_display_prepare_read_queue"));
|
||||
api.getFd = reinterpret_cast<decltype(api.getFd)>(sym("wl_display_get_fd"));
|
||||
api.readEvents = reinterpret_cast<decltype(api.readEvents)>(sym("wl_display_read_events"));
|
||||
api.cancelRead = reinterpret_cast<decltype(api.cancelRead)>(sym("wl_display_cancel_read"));
|
||||
api.loaded = api.displayInterface && api.registryInterface && api.shmInterface &&
|
||||
api.shmPoolInterface && api.bufferInterface && api.marshalFlags && api.addListener && api.destroy &&
|
||||
api.getVersion && api.createWrapper && api.wrapperDestroy && api.setQueue &&
|
||||
@@ -339,6 +351,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
void* surfaceWrapper = nullptr;
|
||||
wl_proxy* frameCallback = nullptr;
|
||||
FrameThrottle throttle;
|
||||
Int loggedInterval = -1;
|
||||
|
||||
~Impl() {
|
||||
const Api& api = WaylandApi();
|
||||
@@ -544,34 +557,73 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
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.
|
||||
static Int64 NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
// A broken connection answers nothing again; stop pacing on it.
|
||||
void AbandonFrame() {
|
||||
if (frameCallback) WaylandApi().destroy(frameCallback);
|
||||
frameCallback = nullptr;
|
||||
throttle.Done();
|
||||
}
|
||||
|
||||
// Waits, as FrameThrottle decides, for the callback of the frame before this one: the
|
||||
// compositor has used that one. It sends none while it shows nothing.
|
||||
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();
|
||||
const Int64 budget = throttle.WaitBudgetMs(swapInterval, NowMs());
|
||||
if (budget == 0) return;
|
||||
if (budget == FrameThrottle::kForever) {
|
||||
while (throttle.Outstanding()) {
|
||||
if (api.dispatchQueue(display, queue) < 0) return AbandonFrame();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!api.prepareReadQueue || !api.getFd || !api.readEvents || !api.cancelRead) return;
|
||||
const Int64 deadline = NowMs() + budget;
|
||||
while (throttle.Outstanding()) {
|
||||
// libwayland's read protocol: whatever is queued first, then one read of the
|
||||
// socket for this queue, bounded by the deadline.
|
||||
while (api.prepareReadQueue(display, queue) != 0) {
|
||||
if (api.dispatchQueuePending(display, queue) < 0) return AbandonFrame();
|
||||
}
|
||||
if (!throttle.Outstanding()) {
|
||||
api.cancelRead(display);
|
||||
return;
|
||||
}
|
||||
api.flush(display);
|
||||
const Int64 remaining = deadline - NowMs();
|
||||
if (remaining <= 0) {
|
||||
api.cancelRead(display);
|
||||
return;
|
||||
}
|
||||
pollfd fd{api.getFd(display), POLLIN, 0};
|
||||
const int ready = poll(&fd, 1, static_cast<int>(remaining));
|
||||
if (ready <= 0) {
|
||||
api.cancelRead(display);
|
||||
if (ready < 0 && errno == EINTR) continue;
|
||||
return;
|
||||
}
|
||||
if (api.readEvents(display) < 0) return AbandonFrame();
|
||||
if (api.dispatchQueuePending(display, queue) < 0) return AbandonFrame();
|
||||
}
|
||||
}
|
||||
|
||||
// The next commit asks for a frame callback (interval above 0, none outstanding).
|
||||
void RequestFrame(wl_proxy* surface, Int swapInterval);
|
||||
// The next commit asks for a frame callback (none outstanding).
|
||||
void RequestFrame(wl_proxy* surface);
|
||||
|
||||
// `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, Int swapInterval) {
|
||||
void Commit(wl_proxy* wlBuffer, const MG_Util::Damage::Region& damage) {
|
||||
const Api& api = WaylandApi();
|
||||
wl_proxy* surface = window->surface;
|
||||
const uint32_t surfaceVersion = api.getVersion(surface);
|
||||
RequestFrame(surface, swapInterval);
|
||||
RequestFrame(surface);
|
||||
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;
|
||||
@@ -673,9 +725,9 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
return eglWindow->width > 0 && eglWindow->height > 0;
|
||||
}
|
||||
|
||||
void WindowSurface::Impl::RequestFrame(wl_proxy* surface, Int swapInterval) {
|
||||
void WindowSurface::Impl::RequestFrame(wl_proxy* surface) {
|
||||
const Api& api = WaylandApi();
|
||||
if (api.callbackInterface == nullptr || !throttle.WantsFrameRequest(swapInterval)) return;
|
||||
if (api.callbackInterface == nullptr || !throttle.WantsFrameRequest()) return;
|
||||
if (surfaceWrapper == nullptr) {
|
||||
surfaceWrapper = api.createWrapper(surface);
|
||||
if (surfaceWrapper == nullptr) return;
|
||||
@@ -686,7 +738,7 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
nullptr);
|
||||
if (frameCallback == nullptr) return;
|
||||
api.addListener(frameCallback, const_cast<void (**)(void)>(kFrameListener), this);
|
||||
throttle.Requested();
|
||||
throttle.Requested(NowMs());
|
||||
}
|
||||
|
||||
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
|
||||
@@ -757,6 +809,12 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
|
||||
Bool WindowSurface::Present(const MG_Util::Damage::Region& damage, Int swapInterval) {
|
||||
Impl& impl = *m_impl;
|
||||
if (impl.window == nullptr) return false;
|
||||
if (swapInterval != impl.loggedInterval) {
|
||||
MGLOG_I("Wayland: window surface %dx%d swaps with interval %d (%s)", impl.width, impl.height,
|
||||
static_cast<int>(swapInterval),
|
||||
swapInterval > 0 ? "paced by frame callbacks" : "unpaced while the compositor shows it");
|
||||
impl.loggedInterval = swapInterval;
|
||||
}
|
||||
impl.WaitForFrame(swapInterval);
|
||||
|
||||
if (impl.useDmabuf) {
|
||||
@@ -770,7 +828,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, swapInterval);
|
||||
impl.Commit(buffer->buffer, damage);
|
||||
buffer->busy = true;
|
||||
return true;
|
||||
}
|
||||
@@ -789,7 +847,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(), swapInterval);
|
||||
impl.Commit(buffer->buffer, MG_Util::Damage::Region::Full());
|
||||
buffer->busy = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -10,30 +10,35 @@ 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
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(1, 0), 0); // the first swap has nothing to wait for
|
||||
ASSERT_TRUE(throttle.WantsFrameRequest());
|
||||
throttle.Requested(0);
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(1, 1), FrameThrottle::kForever); // the next swap waits for it ...
|
||||
EXPECT_FALSE(throttle.WantsFrameRequest()); // ... and asks for no second one
|
||||
throttle.Done();
|
||||
EXPECT_FALSE(throttle.MustWait(1)); // the compositor used the frame
|
||||
EXPECT_TRUE(throttle.WantsFrameRequest(1));
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(1, 2), 0); // the compositor used the frame
|
||||
EXPECT_TRUE(throttle.WantsFrameRequest());
|
||||
}
|
||||
|
||||
TEST(WaylandFrameThrottle, IntervalZeroNeitherAsksNorWaits) {
|
||||
TEST(WaylandFrameThrottle, IntervalZeroStaysUnpacedWhileTheCompositorAnswers) {
|
||||
FrameThrottle throttle;
|
||||
EXPECT_FALSE(throttle.WantsFrameRequest(0));
|
||||
EXPECT_FALSE(throttle.MustWait(0));
|
||||
throttle.Requested(1000);
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 1000), 0);
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 1000 + FrameThrottle::kStarvedAfterMs - 1), 0);
|
||||
throttle.Done();
|
||||
throttle.Requested(1016);
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 1030), 0);
|
||||
}
|
||||
|
||||
TEST(WaylandFrameThrottle, DroppingToIntervalZeroStopsWaitingOnAnOutstandingCallback) {
|
||||
TEST(WaylandFrameThrottle, IntervalZeroSlowsToTheStarvedPaceWhenNothingIsShown) {
|
||||
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.Requested(5000);
|
||||
// The compositor shows nothing: the callback stays unanswered, every later swap waits a while.
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 5000 + FrameThrottle::kStarvedAfterMs), FrameThrottle::kStarvedWaitMs);
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 60000), FrameThrottle::kStarvedWaitMs);
|
||||
// Shown again: the first answer ends it.
|
||||
throttle.Done();
|
||||
EXPECT_TRUE(throttle.WantsFrameRequest(2));
|
||||
EXPECT_EQ(throttle.WaitBudgetMs(0, 60001), 0);
|
||||
}
|
||||
|
||||
TEST(EGLStateSwapInterval, DefaultsToOneAndFollowsEglSwapInterval) {
|
||||
|
||||
Reference in New Issue
Block a user