diff --git a/MobileGL/MG_Impl/EGLImpl/FrameThrottle.h b/MobileGL/MG_Impl/EGLImpl/FrameThrottle.h index 4456773d..d2742644 100644 --- a/MobileGL/MG_Impl/EGLImpl/FrameThrottle.h +++ b/MobileGL/MG_Impl/EGLImpl/FrameThrottle.h @@ -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 diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp index 45a053c3..aee6095f 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp @@ -19,12 +19,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -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(sym("wl_display_dispatch_queue_pending")); api.flush = reinterpret_cast(sym("wl_display_flush")); + api.prepareReadQueue = + reinterpret_cast(sym("wl_display_prepare_read_queue")); + api.getFd = reinterpret_cast(sym("wl_display_get_fd")); + api.readEvents = reinterpret_cast(sym("wl_display_read_events")); + api.cancelRead = reinterpret_cast(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::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(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(kFrameListener), this); - throttle.Requested(); + throttle.Requested(NowMs()); } WindowSurface::WindowSurface(UniquePtr 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(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(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(buffer->map), static_cast(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; } diff --git a/MobileGL/MG_Test/EGLState/FrameThrottleTest.cpp b/MobileGL/MG_Test/EGLState/FrameThrottleTest.cpp index 1cf580f8..a0a0ae41 100644 --- a/MobileGL/MG_Test/EGLState/FrameThrottleTest.cpp +++ b/MobileGL/MG_Test/EGLState/FrameThrottleTest.cpp @@ -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) {