From e3d51fd7f731e528b0f82cbf57bc865d18d1b0b7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 12 Nov 2022 13:03:42 -0600 Subject: [PATCH] Implement a Wayland Vsync source using frame callbacks --- app/app.pro | 9 ++- .../video/ffmpeg-renderers/pacer/pacer.cpp | 47 ++++++++--- .../pacer/waylandvsyncsource.cpp | 77 +++++++++++++++++++ .../pacer/waylandvsyncsource.h | 29 +++++++ 4 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp create mode 100644 app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h diff --git a/app/app.pro b/app/app.pro index dc21b854..01e4eabe 100644 --- a/app/app.pro +++ b/app/app.pro @@ -103,7 +103,7 @@ unix:!macx { } packagesExist(wayland-client) { - DEFINES += HAS_WAYLAND + CONFIG += wayland PKGCONFIG += wayland-client } @@ -348,6 +348,13 @@ embedded { DEFINES += EMBEDDED_BUILD } +wayland { + message(Wayland extensions enabled) + + DEFINES += HAS_WAYLAND + SOURCES += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp + HEADERS += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h +} RESOURCES += \ resources.qrc \ diff --git a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp index ece43257..615e0da6 100644 --- a/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp +++ b/app/streaming/video/ffmpeg-renderers/pacer/pacer.cpp @@ -10,6 +10,12 @@ #include "dxvsyncsource.h" #endif +#ifdef HAS_WAYLAND +#include "waylandvsyncsource.h" +#endif + +#include + // Limit the number of queued frames to prevent excessive memory consumption // if the V-Sync source or renderer is blocked for a while. It's important // that the sum of all queued frames between both pacing and rendering queues @@ -154,8 +160,6 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis) // Make sure initialize() has been called SDL_assert(m_MaxVideoFps != 0); - SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS); - m_FrameQueueLock.lock(); // If the queue length history entries are large, be strict @@ -196,7 +200,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis) if (m_PacingQueue.isEmpty()) { // Wait for a frame to arrive or our V-sync timeout to expire - if (!m_PacingQueueNotEmpty.wait(&m_FrameQueueLock, timeUntilNextVsyncMillis - TIMER_SLACK_MS)) { + if (!m_PacingQueueNotEmpty.wait(&m_FrameQueueLock, SDL_max(timeUntilNextVsyncMillis, TIMER_SLACK_MS) - TIMER_SLACK_MS)) { // Wait timed out - unlock and bail m_FrameQueueLock.unlock(); return; @@ -218,17 +222,38 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing) "Frame pacing active: target %d Hz with %d FPS stream", m_DisplayFps, m_MaxVideoFps); - #if defined(Q_OS_WIN32) - // Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because - // it blocks during other concurrent DX operations (like actually rendering). - if (IsWindows8OrGreater()) { - m_VsyncSource = new DxVsyncSource(this); + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (!SDL_GetWindowWMInfo(window, &info)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_GetWindowWMInfo() failed: %s", + SDL_GetError()); + return false; } - #else - // Platforms without a VsyncSource will just render frames - // immediately like they used to. + + switch (info.subsystem) { + #ifdef Q_OS_WIN32 + case SDL_SYSWM_WINDOWS: + // Don't use D3DKMTWaitForVerticalBlankEvent() on Windows 7, because + // it blocks during other concurrent DX operations (like actually rendering). + if (IsWindows8OrGreater()) { + m_VsyncSource = new DxVsyncSource(this); + } + break; #endif + #if defined(SDL_VIDEO_DRIVER_WAYLAND) && defined(HAS_WAYLAND) + case SDL_SYSWM_WAYLAND: + m_VsyncSource = new WaylandVsyncSource(this); + break; + #endif + + default: + // Platforms without a VsyncSource will just render frames + // immediately like they used to. + break; + } + SDL_assert(m_VsyncSource != nullptr || !(m_RendererAttributes & RENDERER_ATTRIBUTE_FORCE_PACING)); if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) { diff --git a/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp new file mode 100644 index 00000000..9887a5c9 --- /dev/null +++ b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp @@ -0,0 +1,77 @@ +#include "waylandvsyncsource.h" + +#include + +const struct wl_callback_listener WaylandVsyncSource::s_FrameListener = { + .done = WaylandVsyncSource::frameDone, +}; + +WaylandVsyncSource::WaylandVsyncSource(Pacer* pacer) + : m_Pacer(pacer), + m_Display(nullptr), + m_Surface(nullptr), + m_Callback(nullptr), + m_LastFrameTime(0) +{ + +} + +WaylandVsyncSource::~WaylandVsyncSource() +{ + if (m_Callback != nullptr) { + wl_callback_destroy(m_Callback); + wl_display_roundtrip(m_Display); + } +} + +bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps) +{ + SDL_SysWMinfo info; + + SDL_VERSION(&info.version); + + if (!SDL_GetWindowWMInfo(window, &info)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "SDL_GetWindowWMInfo() failed: %s", + SDL_GetError()); + return false; + } + + // Pacer shoud not create us for non-Wayland windows + SDL_assert(info.subsystem == SDL_SYSWM_WAYLAND); + + m_DisplayFps = displayFps; + m_Display = info.info.wl.display; + m_Surface = info.info.wl.surface; + + // Enqueue our first frame callback + m_Callback = wl_surface_frame(m_Surface); + wl_callback_add_listener(m_Callback, &s_FrameListener, this); + wl_surface_commit(m_Surface); + + return true; +} + +void WaylandVsyncSource::frameDone(void* data, struct wl_callback* oldCb, uint32_t time) +{ + auto me = (WaylandVsyncSource*)data; + + // Free this callback + SDL_assert(oldCb == me->m_Callback); + wl_callback_destroy(oldCb); + + // Register for another callback before invoking Pacer to ensure we don't miss + // a frame callback if Pacer takes too long. + me->m_Callback = wl_surface_frame(me->m_Surface); + wl_callback_add_listener(me->m_Callback, &s_FrameListener, data); + wl_surface_commit(me->m_Surface); + wl_display_flush(me->m_Display); + + if (me->m_LastFrameTime != 0) { + // Assuming that the time until the next V-Sync will usually be equal to the + // time from last callback to this one but cap it at 2x the expected frame period. + me->m_Pacer->vsyncCallback(SDL_min(time - me->m_LastFrameTime, 2000 / me->m_DisplayFps)); + } + + me->m_LastFrameTime = time; +} diff --git a/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h new file mode 100644 index 00000000..23056fe3 --- /dev/null +++ b/app/streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h @@ -0,0 +1,29 @@ +#pragma once + +#include "pacer.h" + +#include +#include + +class WaylandVsyncSource : public IVsyncSource +{ +public: + WaylandVsyncSource(Pacer* pacer); + + virtual ~WaylandVsyncSource(); + + virtual bool initialize(SDL_Window* window, int displayFps); + +private: + static void frameDone(void* data, struct wl_callback* oldCb, uint32_t time); + + static const struct wl_callback_listener s_FrameListener; + + Pacer* m_Pacer; + int m_DisplayFps; + wl_display* m_Display; + wl_surface* m_Surface; + wl_callback* m_Callback; + uint32_t m_LastFrameTime; +}; +