From 9c20df50307489ba8ec9e0f604ef28af1599d0b3 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Thu, 15 Oct 2020 12:27:00 +0300 Subject: [PATCH] screencast: Use fences to avoid stalling the graphics pipeline Currently, we use glFinish() to ensure that stream consumers don't see corrupted or rather incomplete buffers. This is a serious issue because glFinish() not only prevents the gpu from processing new GL commands, but it also blocks the compositor. This change addresses the blocking issue by using native fences. With the proposed change, after finishing recording a frame, a fence is inserted in the command stream. When the native fence is signaled, the pending pipewire buffer will be enqueued. If the EGL_ANDROID_native_fence_sync extension is not supported, we'll fall back to using glFinish(). --- CMakeLists.txt | 1 + platform.cpp | 8 +++ platform.h | 4 ++ .../scenes/opengl/abstract_egl_backend.cpp | 1 + platformsupport/scenes/opengl/backend.h | 13 +++++ plugins/scenes/opengl/scene_opengl.cpp | 5 ++ plugins/scenes/opengl/scene_opengl.h | 1 + scene.cpp | 5 ++ scene.h | 1 + screencast/eglnativefence.cpp | 50 +++++++++++++++++++ screencast/eglnativefence.h | 32 ++++++++++++ screencast/pipewirestream.cpp | 50 ++++++++++++++++++- screencast/pipewirestream.h | 8 +++ screencast/screencastmanager.cpp | 1 - 14 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 screencast/eglnativefence.cpp create mode 100644 screencast/eglnativefence.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d07858987..87c7ed7f21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -777,6 +777,7 @@ add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) if (PipeWire_FOUND) target_sources(kwin_wayland PRIVATE + screencast/eglnativefence.cpp screencast/screencastmanager.cpp screencast/pipewirecore.cpp screencast/pipewirestream.cpp) diff --git a/platform.cpp b/platform.cpp index 98f8ef6fde..08e47609df 100644 --- a/platform.cpp +++ b/platform.cpp @@ -425,6 +425,14 @@ bool Platform::supportsSurfacelessContext() const return false; } +bool Platform::supportsNativeFence() const +{ + if (Compositor *compositor = Compositor::self()) { + return compositor->scene()->supportsNativeFence(); + } + return false; +} + EGLDisplay KWin::Platform::sceneEglDisplay() const { return m_eglDisplay; diff --git a/platform.h b/platform.h index 506801af3d..3c02afd455 100644 --- a/platform.h +++ b/platform.h @@ -101,6 +101,10 @@ public: * so that a sharing context could be created. */ bool supportsSurfacelessContext() const; + /** + * Whether our Compositing EGL display supports creating native EGL fences. + */ + bool supportsNativeFence() const; /** * The EGLDisplay used by the compositing scene. */ diff --git a/platformsupport/scenes/opengl/abstract_egl_backend.cpp b/platformsupport/scenes/opengl/abstract_egl_backend.cpp index abcc7ec9f4..36feab1070 100644 --- a/platformsupport/scenes/opengl/abstract_egl_backend.cpp +++ b/platformsupport/scenes/opengl/abstract_egl_backend.cpp @@ -154,6 +154,7 @@ bool AbstractEglBackend::initEglAPI() const QByteArray eglExtensions = eglQueryString(m_display, EGL_EXTENSIONS); setExtensions(eglExtensions.split(' ')); setSupportsSurfacelessContext(hasExtension(QByteArrayLiteral("EGL_KHR_surfaceless_context"))); + setSupportsNativeFence(hasExtension(QByteArrayLiteral("EGL_ANDROID_native_fence_sync"))); return true; } diff --git a/platformsupport/scenes/opengl/backend.h b/platformsupport/scenes/opengl/backend.h index c7fdb564de..6f50d2c890 100644 --- a/platformsupport/scenes/opengl/backend.h +++ b/platformsupport/scenes/opengl/backend.h @@ -170,6 +170,10 @@ public: { return m_haveSurfacelessContext; } + bool supportsNativeFence() const + { + return m_haveNativeFence; + } /** * Returns the damage that has accumulated since a buffer of the given age was presented. @@ -270,6 +274,11 @@ protected: m_haveSurfacelessContext = value; } + void setSupportsNativeFence(bool value) + { + m_haveNativeFence = value; + } + /** * @return const QRegion& Damage of previously rendered frame */ @@ -323,6 +332,10 @@ private: * @brief Whether the backend supports EGL_KHR_surfaceless_context. */ bool m_haveSurfacelessContext = false; + /** + * @brief Whether the backend supports EGL_ANDROID_native_fence_sync. + */ + bool m_haveNativeFence = false; /** * @brief Whether the initialization failed, of course default to @c false. */ diff --git a/plugins/scenes/opengl/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp index 79f3445784..7cfbf91d18 100644 --- a/plugins/scenes/opengl/scene_opengl.cpp +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -865,6 +865,11 @@ bool SceneOpenGL::supportsSurfacelessContext() const return m_backend->supportsSurfacelessContext(); } +bool SceneOpenGL::supportsNativeFence() const +{ + return m_backend->supportsNativeFence(); +} + Scene::EffectFrame *SceneOpenGL::createEffectFrame(EffectFrameImpl *frame) { return new SceneOpenGL::EffectFrame(frame, this); diff --git a/plugins/scenes/opengl/scene_opengl.h b/plugins/scenes/opengl/scene_opengl.h index d2c281b84a..60dca99e82 100644 --- a/plugins/scenes/opengl/scene_opengl.h +++ b/plugins/scenes/opengl/scene_opengl.h @@ -46,6 +46,7 @@ public: bool makeOpenGLContextCurrent() override; void doneOpenGLContextCurrent() override; bool supportsSurfacelessContext() const override; + bool supportsNativeFence() const override; Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override; void triggerFence() override; virtual QMatrix4x4 projectionMatrix() const = 0; diff --git a/scene.cpp b/scene.cpp index 7b3373e992..fe4105cfc3 100644 --- a/scene.cpp +++ b/scene.cpp @@ -686,6 +686,11 @@ bool Scene::supportsSurfacelessContext() const return false; } +bool Scene::supportsNativeFence() const +{ + return false; +} + void Scene::triggerFence() { } diff --git a/scene.h b/scene.h index 6f846a99b7..fc25ae5cc9 100644 --- a/scene.h +++ b/scene.h @@ -140,6 +140,7 @@ public: virtual bool makeOpenGLContextCurrent(); virtual void doneOpenGLContextCurrent(); virtual bool supportsSurfacelessContext() const; + virtual bool supportsNativeFence() const; virtual QMatrix4x4 screenProjectionMatrix() const; diff --git a/screencast/eglnativefence.cpp b/screencast/eglnativefence.cpp new file mode 100644 index 0000000000..c10afc3e5c --- /dev/null +++ b/screencast/eglnativefence.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglnativefence.h" + +#include + +namespace KWin +{ + +#ifndef EGL_ANDROID_native_fence_sync +#define EGL_SYNC_NATIVE_FENCE_ANDROID 0x3144 +#define EGL_NO_NATIVE_FENCE_FD_ANDROID -1 +#endif // EGL_ANDROID_native_fence_sync + +EGLNativeFence::EGLNativeFence(EGLDisplay display) + : m_display(display) +{ + m_sync = eglCreateSyncKHR(m_display, EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr); + if (m_sync != EGL_NO_SYNC_KHR) { + // The native fence will get a valid sync file fd only after a flush. + glFlush(); + m_fileDescriptor = eglDupNativeFenceFDANDROID(m_display, m_sync); + } +} + +EGLNativeFence::~EGLNativeFence() +{ + if (m_fileDescriptor != EGL_NO_NATIVE_FENCE_FD_ANDROID) { + close(m_fileDescriptor); + } + if (m_sync != EGL_NO_SYNC_KHR) { + eglDestroySyncKHR(m_display, m_sync); + } +} + +bool EGLNativeFence::isValid() const +{ + return m_sync != EGL_NO_SYNC_KHR && m_fileDescriptor != EGL_NO_NATIVE_FENCE_FD_ANDROID; +} + +int EGLNativeFence::fileDescriptor() const +{ + return m_fileDescriptor; +} + +} // namespace KWin diff --git a/screencast/eglnativefence.h b/screencast/eglnativefence.h new file mode 100644 index 0000000000..c8dfd278a0 --- /dev/null +++ b/screencast/eglnativefence.h @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace KWin +{ + +class EGLNativeFence +{ +public: + explicit EGLNativeFence(EGLDisplay display); + ~EGLNativeFence(); + + bool isValid() const; + int fileDescriptor() const; + +private: + EGLSyncKHR m_sync = EGL_NO_SYNC_KHR; + EGLDisplay m_display = EGL_NO_DISPLAY; + int m_fileDescriptor = -1; + + Q_DISABLE_COPY(EGLNativeFence) +}; + +} // namespace KWin diff --git a/screencast/pipewirestream.cpp b/screencast/pipewirestream.cpp index 26779ebaf1..44f92c5eda 100644 --- a/screencast/pipewirestream.cpp +++ b/screencast/pipewirestream.cpp @@ -9,6 +9,7 @@ #include "pipewirestream.h" #include "cursor.h" #include "dmabuftexture.h" +#include "eglnativefence.h" #include "kwingltexture.h" #include "kwinglutils.h" #include "kwinscreencast_logging.h" @@ -298,6 +299,11 @@ void PipeWireStream::recordFrame(GLTexture *frameTexture, const QRegion &damaged Q_ASSERT(!m_stopped); Q_ASSERT(frameTexture); + if (m_pendingBuffer) { + qCWarning(KWIN_SCREENCAST) << "Dropping a screencast frame because the compositor is slow"; + return; + } + if (frameTexture->size() != m_resolution) { m_resolution = frameTexture->size(); newStreamParams(); @@ -409,7 +415,49 @@ void PipeWireStream::recordFrame(GLTexture *frameTexture, const QRegion &damaged (spa_meta_cursor *) spa_buffer_find_meta_data (spa_buffer, SPA_META_Cursor, sizeof (spa_meta_cursor))); } - pw_stream_queue_buffer(pwStream, buffer); + tryEnqueue(buffer); +} + +void PipeWireStream::tryEnqueue(pw_buffer *buffer) +{ + m_pendingBuffer = buffer; + + // The GPU doesn't necessarily process draw commands as soon as they are issued. Thus, + // we need to insert a fence into the command stream and enqueue the pipewire buffer + // only after the fence is signaled; otherwise stream consumers will most likely see + // a corrupted buffer. + if (kwinApp()->platform()->supportsNativeFence()) { + Q_ASSERT_X(eglGetCurrentContext(), "tryEnqueue", "no current context"); + m_pendingFence = new EGLNativeFence(kwinApp()->platform()->sceneEglDisplay()); + if (!m_pendingFence->isValid()) { + qCWarning(KWIN_SCREENCAST) << "Failed to create a native EGL fence"; + glFinish(); + enqueue(); + } else { + m_pendingNotifier = new QSocketNotifier(m_pendingFence->fileDescriptor(), + QSocketNotifier::Read, this); + connect(m_pendingNotifier, &QSocketNotifier::activated, this, &PipeWireStream::enqueue); + } + } else { + // The compositing backend doesn't support native fences. We don't have any other choice + // but stall the graphics pipeline. Otherwise stream consumers may see an incomplete buffer. + glFinish(); + enqueue(); + } +} + +void PipeWireStream::enqueue() +{ + Q_ASSERT_X(m_pendingBuffer, "enqueue", "pending buffer must be valid"); + + delete m_pendingFence; + delete m_pendingNotifier; + + pw_stream_queue_buffer(pwStream, m_pendingBuffer); + + m_pendingBuffer = nullptr; + m_pendingFence = nullptr; + m_pendingNotifier = nullptr; } QRect PipeWireStream::cursorGeometry(Cursor *cursor) const diff --git a/screencast/pipewirestream.h b/screencast/pipewirestream.h index b920c959fe..c1568623b8 100644 --- a/screencast/pipewirestream.h +++ b/screencast/pipewirestream.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ namespace KWin class Cursor; class DmaBufTexture; +class EGLNativeFence; class GLTexture; class PipeWireCore; @@ -68,6 +70,8 @@ private: void coreFailed(const QString &errorMessage); void sendCursorData(Cursor *cursor, spa_meta_cursor *spa_cursor); void newStreamParams(); + void tryEnqueue(pw_buffer *buffer); + void enqueue(); QSharedPointer pwCore; struct pw_stream *pwStream = nullptr; @@ -96,6 +100,10 @@ private: QRect cursorGeometry(Cursor *cursor) const; QHash> m_dmabufDataForPwBuffer; + + pw_buffer *m_pendingBuffer = nullptr; + QSocketNotifier *m_pendingNotifier = nullptr; + EGLNativeFence *m_pendingFence = nullptr; }; } // namespace KWin diff --git a/screencast/screencastmanager.cpp b/screencast/screencastmanager.cpp index 0cae792ea1..691a5efafe 100644 --- a/screencast/screencastmanager.cpp +++ b/screencast/screencastmanager.cpp @@ -78,7 +78,6 @@ private: recordFrame(frameTexture.data(), m_damagedRegion); frameTexture->setYInverted(wasYInverted); m_damagedRegion = {}; - glFinish(); // TODO: Don't stall the whole pipeline. Use EGL_ANDROID_native_fence_sync. } QRegion m_damagedRegion;