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().
This commit is contained in:
Vlad Zahorodnii 2020-10-15 12:27:00 +03:00
parent 170c41681d
commit 9c20df5030
14 changed files with 178 additions and 2 deletions

View file

@ -777,6 +777,7 @@ add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS})
if (PipeWire_FOUND) if (PipeWire_FOUND)
target_sources(kwin_wayland target_sources(kwin_wayland
PRIVATE PRIVATE
screencast/eglnativefence.cpp
screencast/screencastmanager.cpp screencast/screencastmanager.cpp
screencast/pipewirecore.cpp screencast/pipewirecore.cpp
screencast/pipewirestream.cpp) screencast/pipewirestream.cpp)

View file

@ -425,6 +425,14 @@ bool Platform::supportsSurfacelessContext() const
return false; return false;
} }
bool Platform::supportsNativeFence() const
{
if (Compositor *compositor = Compositor::self()) {
return compositor->scene()->supportsNativeFence();
}
return false;
}
EGLDisplay KWin::Platform::sceneEglDisplay() const EGLDisplay KWin::Platform::sceneEglDisplay() const
{ {
return m_eglDisplay; return m_eglDisplay;

View file

@ -101,6 +101,10 @@ public:
* so that a sharing context could be created. * so that a sharing context could be created.
*/ */
bool supportsSurfacelessContext() const; bool supportsSurfacelessContext() const;
/**
* Whether our Compositing EGL display supports creating native EGL fences.
*/
bool supportsNativeFence() const;
/** /**
* The EGLDisplay used by the compositing scene. * The EGLDisplay used by the compositing scene.
*/ */

View file

@ -154,6 +154,7 @@ bool AbstractEglBackend::initEglAPI()
const QByteArray eglExtensions = eglQueryString(m_display, EGL_EXTENSIONS); const QByteArray eglExtensions = eglQueryString(m_display, EGL_EXTENSIONS);
setExtensions(eglExtensions.split(' ')); setExtensions(eglExtensions.split(' '));
setSupportsSurfacelessContext(hasExtension(QByteArrayLiteral("EGL_KHR_surfaceless_context"))); setSupportsSurfacelessContext(hasExtension(QByteArrayLiteral("EGL_KHR_surfaceless_context")));
setSupportsNativeFence(hasExtension(QByteArrayLiteral("EGL_ANDROID_native_fence_sync")));
return true; return true;
} }

View file

@ -170,6 +170,10 @@ public:
{ {
return m_haveSurfacelessContext; return m_haveSurfacelessContext;
} }
bool supportsNativeFence() const
{
return m_haveNativeFence;
}
/** /**
* Returns the damage that has accumulated since a buffer of the given age was presented. * Returns the damage that has accumulated since a buffer of the given age was presented.
@ -270,6 +274,11 @@ protected:
m_haveSurfacelessContext = value; m_haveSurfacelessContext = value;
} }
void setSupportsNativeFence(bool value)
{
m_haveNativeFence = value;
}
/** /**
* @return const QRegion& Damage of previously rendered frame * @return const QRegion& Damage of previously rendered frame
*/ */
@ -323,6 +332,10 @@ private:
* @brief Whether the backend supports EGL_KHR_surfaceless_context. * @brief Whether the backend supports EGL_KHR_surfaceless_context.
*/ */
bool m_haveSurfacelessContext = false; 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. * @brief Whether the initialization failed, of course default to @c false.
*/ */

View file

@ -865,6 +865,11 @@ bool SceneOpenGL::supportsSurfacelessContext() const
return m_backend->supportsSurfacelessContext(); return m_backend->supportsSurfacelessContext();
} }
bool SceneOpenGL::supportsNativeFence() const
{
return m_backend->supportsNativeFence();
}
Scene::EffectFrame *SceneOpenGL::createEffectFrame(EffectFrameImpl *frame) Scene::EffectFrame *SceneOpenGL::createEffectFrame(EffectFrameImpl *frame)
{ {
return new SceneOpenGL::EffectFrame(frame, this); return new SceneOpenGL::EffectFrame(frame, this);

View file

@ -46,6 +46,7 @@ public:
bool makeOpenGLContextCurrent() override; bool makeOpenGLContextCurrent() override;
void doneOpenGLContextCurrent() override; void doneOpenGLContextCurrent() override;
bool supportsSurfacelessContext() const override; bool supportsSurfacelessContext() const override;
bool supportsNativeFence() const override;
Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override; Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override;
void triggerFence() override; void triggerFence() override;
virtual QMatrix4x4 projectionMatrix() const = 0; virtual QMatrix4x4 projectionMatrix() const = 0;

View file

@ -686,6 +686,11 @@ bool Scene::supportsSurfacelessContext() const
return false; return false;
} }
bool Scene::supportsNativeFence() const
{
return false;
}
void Scene::triggerFence() void Scene::triggerFence()
{ {
} }

View file

@ -140,6 +140,7 @@ public:
virtual bool makeOpenGLContextCurrent(); virtual bool makeOpenGLContextCurrent();
virtual void doneOpenGLContextCurrent(); virtual void doneOpenGLContextCurrent();
virtual bool supportsSurfacelessContext() const; virtual bool supportsSurfacelessContext() const;
virtual bool supportsNativeFence() const;
virtual QMatrix4x4 screenProjectionMatrix() const; virtual QMatrix4x4 screenProjectionMatrix() const;

View file

@ -0,0 +1,50 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "eglnativefence.h"
#include <unistd.h>
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

View file

@ -0,0 +1,32 @@
/*
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <epoxy/egl.h>
#include <fixx11h.h>
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

View file

@ -9,6 +9,7 @@
#include "pipewirestream.h" #include "pipewirestream.h"
#include "cursor.h" #include "cursor.h"
#include "dmabuftexture.h" #include "dmabuftexture.h"
#include "eglnativefence.h"
#include "kwingltexture.h" #include "kwingltexture.h"
#include "kwinglutils.h" #include "kwinglutils.h"
#include "kwinscreencast_logging.h" #include "kwinscreencast_logging.h"
@ -298,6 +299,11 @@ void PipeWireStream::recordFrame(GLTexture *frameTexture, const QRegion &damaged
Q_ASSERT(!m_stopped); Q_ASSERT(!m_stopped);
Q_ASSERT(frameTexture); Q_ASSERT(frameTexture);
if (m_pendingBuffer) {
qCWarning(KWIN_SCREENCAST) << "Dropping a screencast frame because the compositor is slow";
return;
}
if (frameTexture->size() != m_resolution) { if (frameTexture->size() != m_resolution) {
m_resolution = frameTexture->size(); m_resolution = frameTexture->size();
newStreamParams(); 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))); (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 QRect PipeWireStream::cursorGeometry(Cursor *cursor) const

View file

@ -17,6 +17,7 @@
#include <QObject> #include <QObject>
#include <QSharedPointer> #include <QSharedPointer>
#include <QSize> #include <QSize>
#include <QSocketNotifier>
#include <pipewire/pipewire.h> #include <pipewire/pipewire.h>
#include <spa/param/format-utils.h> #include <spa/param/format-utils.h>
@ -28,6 +29,7 @@ namespace KWin
class Cursor; class Cursor;
class DmaBufTexture; class DmaBufTexture;
class EGLNativeFence;
class GLTexture; class GLTexture;
class PipeWireCore; class PipeWireCore;
@ -68,6 +70,8 @@ private:
void coreFailed(const QString &errorMessage); void coreFailed(const QString &errorMessage);
void sendCursorData(Cursor *cursor, spa_meta_cursor *spa_cursor); void sendCursorData(Cursor *cursor, spa_meta_cursor *spa_cursor);
void newStreamParams(); void newStreamParams();
void tryEnqueue(pw_buffer *buffer);
void enqueue();
QSharedPointer<PipeWireCore> pwCore; QSharedPointer<PipeWireCore> pwCore;
struct pw_stream *pwStream = nullptr; struct pw_stream *pwStream = nullptr;
@ -96,6 +100,10 @@ private:
QRect cursorGeometry(Cursor *cursor) const; QRect cursorGeometry(Cursor *cursor) const;
QHash<struct pw_buffer *, QSharedPointer<DmaBufTexture>> m_dmabufDataForPwBuffer; QHash<struct pw_buffer *, QSharedPointer<DmaBufTexture>> m_dmabufDataForPwBuffer;
pw_buffer *m_pendingBuffer = nullptr;
QSocketNotifier *m_pendingNotifier = nullptr;
EGLNativeFence *m_pendingFence = nullptr;
}; };
} // namespace KWin } // namespace KWin

View file

@ -78,7 +78,6 @@ private:
recordFrame(frameTexture.data(), m_damagedRegion); recordFrame(frameTexture.data(), m_damagedRegion);
frameTexture->setYInverted(wasYInverted); frameTexture->setYInverted(wasYInverted);
m_damagedRegion = {}; m_damagedRegion = {};
glFinish(); // TODO: Don't stall the whole pipeline. Use EGL_ANDROID_native_fence_sync.
} }
QRegion m_damagedRegion; QRegion m_damagedRegion;