From b8a70e62d53591130a284adea9a56eb20e6d8060 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Thu, 19 Nov 2020 10:52:29 +0200 Subject: [PATCH] Introduce RenderLoop At the moment, our frame scheduling infrastructure is still heavily based on Xinerama-style rendering. Specifically, we assume that painting is driven by a single timer, etc. This change introduces a new type - RenderLoop. Its main purpose is to drive compositing on a specific output, or in case of X11, on the overlay window. With RenderLoop, compositing is synchronized to vblank events. It exposes the last and the next estimated presentation timestamp. The expected presentation timestamp can be used by effects to ensure that animations are synchronized with the upcoming vblank event. On Wayland, every outputs has its own render loop. On X11, per screen rendering is not possible, therefore the platform exposes the render loop for the overlay window. Ideally, the Scene has to expose the RenderLoop, but as the first step towards better compositing scheduling it's good as is for the time being. The RenderLoop tries to minimize the latency by delaying compositing as close as possible to the next vblank event. One tricky thing about it is that if compositing is too close to the next vblank event, animations may become a little bit choppy. However, increasing the latency reduces the choppiness. Given that, there is no any "silver bullet" solution for the choppiness issue, a new option has been added in the Compositing KCM to specify the amount of latency. By default, it's "Medium," but if a user is not satisfied with the upstream default, they can tweak it. --- CMakeLists.txt | 1 + abstract_output.cpp | 5 + abstract_output.h | 8 + .../integration/buffer_size_change_test.cpp | 17 +- composite.cpp | 249 +++++++----------- composite.h | 54 ++-- kcmkwin/kwincompositing/compositing.ui | 46 +++- .../kwincompositing_setting.kcfg | 11 + kwin.kcfg | 19 +- libkwineffects/kwinglobals.h | 2 + main.cpp | 1 + options.cpp | 97 ++----- options.h | 56 ++-- platform.cpp | 5 + platform.h | 7 + platformsupport/CMakeLists.txt | 1 + platformsupport/scenes/opengl/backend.cpp | 3 +- platformsupport/scenes/opengl/backend.h | 24 -- .../vsyncconvenience/CMakeLists.txt | 8 + .../vsyncconvenience/softwarevsyncmonitor.cpp | 60 +++++ .../vsyncconvenience/softwarevsyncmonitor.h | 46 ++++ .../vsyncconvenience/vsyncmonitor.cpp | 17 ++ .../vsyncconvenience/vsyncmonitor.h | 36 +++ .../drm/abstract_egl_drm_backend.cpp | 1 - plugins/platforms/drm/drm_backend.cpp | 72 +++-- plugins/platforms/drm/drm_gpu.cpp | 12 + plugins/platforms/drm/drm_gpu.h | 7 + plugins/platforms/drm/drm_output.cpp | 8 + plugins/platforms/drm/drm_output.h | 4 + plugins/platforms/drm/egl_multi_backend.cpp | 1 - plugins/platforms/fbdev/CMakeLists.txt | 3 +- plugins/platforms/fbdev/fb_backend.cpp | 48 +++- plugins/platforms/fbdev/fb_backend.h | 15 +- plugins/platforms/fbdev/fbvsyncmonitor.cpp | 77 ++++++ plugins/platforms/fbdev/fbvsyncmonitor.h | 63 +++++ .../fbdev/scene_qpainter_fb_backend.cpp | 37 ++- .../fbdev/scene_qpainter_fb_backend.h | 3 + plugins/platforms/virtual/CMakeLists.txt | 3 +- plugins/platforms/virtual/egl_gbm_backend.cpp | 9 +- .../scene_qpainter_virtual_backend.cpp | 8 +- plugins/platforms/virtual/virtual_output.cpp | 29 +- plugins/platforms/virtual/virtual_output.h | 9 + .../platforms/wayland/egl_wayland_backend.cpp | 1 - .../scene_qpainter_wayland_backend.cpp | 2 - plugins/platforms/wayland/wayland_backend.cpp | 28 +- plugins/platforms/wayland/wayland_backend.h | 2 - plugins/platforms/wayland/wayland_output.cpp | 13 +- plugins/platforms/wayland/wayland_output.h | 3 + .../platforms/x11/common/eglonxbackend.cpp | 2 - .../platforms/x11/standalone/CMakeLists.txt | 4 +- .../platforms/x11/standalone/eglbackend.cpp | 36 ++- plugins/platforms/x11/standalone/eglbackend.h | 9 +- .../platforms/x11/standalone/glxbackend.cpp | 102 +++++-- plugins/platforms/x11/standalone/glxbackend.h | 13 +- .../standalone/omlsynccontrolvsyncmonitor.cpp | 132 ++++++++++ .../standalone/omlsynccontrolvsyncmonitor.h | 75 ++++++ .../standalone/sgivideosyncvsyncmonitor.cpp | 134 ++++++++++ .../x11/standalone/sgivideosyncvsyncmonitor.h | 75 ++++++ .../platforms/x11/standalone/x11_platform.cpp | 57 +++- .../platforms/x11/standalone/x11_platform.h | 2 + .../x11/standalone/x11xrenderbackend.cpp | 25 ++ .../x11/standalone/x11xrenderbackend.h | 8 +- plugins/platforms/x11/windowed/CMakeLists.txt | 4 +- .../x11/windowed/egl_x11_backend.cpp | 7 + .../windowed/scene_qpainter_x11_backend.cpp | 9 +- .../x11/windowed/x11windowed_output.cpp | 29 +- .../x11/windowed/x11windowed_output.h | 9 +- plugins/scenes/opengl/scene_opengl.cpp | 8 - plugins/scenes/opengl/scene_opengl.h | 1 - renderloop.cpp | 207 +++++++++++++++ renderloop.h | 111 ++++++++ renderloop_p.h | 42 +++ scene.cpp | 10 - scene.h | 3 - 74 files changed, 1837 insertions(+), 508 deletions(-) create mode 100644 platformsupport/vsyncconvenience/CMakeLists.txt create mode 100644 platformsupport/vsyncconvenience/softwarevsyncmonitor.cpp create mode 100644 platformsupport/vsyncconvenience/softwarevsyncmonitor.h create mode 100644 platformsupport/vsyncconvenience/vsyncmonitor.cpp create mode 100644 platformsupport/vsyncconvenience/vsyncmonitor.h create mode 100644 plugins/platforms/fbdev/fbvsyncmonitor.cpp create mode 100644 plugins/platforms/fbdev/fbvsyncmonitor.h create mode 100644 plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.cpp create mode 100644 plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.h create mode 100644 plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.cpp create mode 100644 plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.h create mode 100644 renderloop.cpp create mode 100644 renderloop.h create mode 100644 renderloop_p.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 571c1b6e78..f08c1b39e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -506,6 +506,7 @@ set(kwin_SRCS pluginmanager.cpp pointer_input.cpp popup_input_filter.cpp + renderloop.cpp rootinfo_filter.cpp rulebooksettings.cpp rules.cpp diff --git a/abstract_output.cpp b/abstract_output.cpp index c33a64a60c..25560bbace 100644 --- a/abstract_output.cpp +++ b/abstract_output.cpp @@ -118,4 +118,9 @@ QString AbstractOutput::serialNumber() const return QString(); } +RenderLoop *AbstractOutput::renderLoop() const +{ + return nullptr; +} + } // namespace KWin diff --git a/abstract_output.h b/abstract_output.h index 017cd76361..3538e4b56e 100644 --- a/abstract_output.h +++ b/abstract_output.h @@ -24,6 +24,8 @@ class OutputChangeSet; namespace KWin { +class RenderLoop; + class KWIN_EXPORT GammaRamp { public: @@ -177,6 +179,12 @@ public: */ virtual QString serialNumber() const; + /** + * Returns the RenderLoop for this output. This function returns @c null if the + * underlying platform doesn't support per-screen rendering mode. + */ + virtual RenderLoop *renderLoop() const; + Q_SIGNALS: /** * This signal is emitted when the geometry of this output has changed. diff --git a/autotests/integration/buffer_size_change_test.cpp b/autotests/integration/buffer_size_change_test.cpp index 66b48e1023..a65683809d 100644 --- a/autotests/integration/buffer_size_change_test.cpp +++ b/autotests/integration/buffer_size_change_test.cpp @@ -10,6 +10,7 @@ #include "abstract_client.h" #include "composite.h" +#include "scene.h" #include "wayland_server.h" #include @@ -54,10 +55,10 @@ void BufferSizeChangeTest::testShmBufferSizeChange() QVERIFY(client); // add a first repaint - QSignalSpy swapSpy(Compositor::self(), &Compositor::bufferSwapCompleted); - QVERIFY(swapSpy.isValid()); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); Compositor::self()->addRepaintFull(); - QVERIFY(swapSpy.wait()); + QVERIFY(frameRenderedSpy.wait()); // now change buffer size Test::render(surface.data(), QSize(30, 10), Qt::red); @@ -66,7 +67,7 @@ void BufferSizeChangeTest::testShmBufferSizeChange() QVERIFY(damagedSpy.isValid()); QVERIFY(damagedSpy.wait()); KWin::Compositor::self()->addRepaintFull(); - QVERIFY(swapSpy.wait()); + QVERIFY(frameRenderedSpy.wait()); } void BufferSizeChangeTest::testShmBufferSizeChangeOnSubSurface() @@ -91,10 +92,10 @@ void BufferSizeChangeTest::testShmBufferSizeChangeOnSubSurface() QVERIFY(parent); // add a first repaint - QSignalSpy swapSpy(Compositor::self(), &Compositor::bufferSwapCompleted); - QVERIFY(swapSpy.isValid()); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &Scene::frameRendered); + QVERIFY(frameRenderedSpy.isValid()); Compositor::self()->addRepaintFull(); - QVERIFY(swapSpy.wait()); + QVERIFY(frameRenderedSpy.wait()); // change buffer size of sub surface QSignalSpy damagedParentSpy(parent, &AbstractClient::damaged); @@ -106,7 +107,7 @@ void BufferSizeChangeTest::testShmBufferSizeChangeOnSubSurface() // add a second repaint KWin::Compositor::self()->addRepaintFull(); - QVERIFY(swapSpy.wait()); + QVERIFY(frameRenderedSpy.wait()); } } diff --git a/composite.cpp b/composite.cpp index 20eb9b60aa..c0a433f656 100644 --- a/composite.cpp +++ b/composite.cpp @@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "composite.h" - +#include "abstract_output.h" #include "dbusinterface.h" #include "x11client.h" #include "decorations/decoratedclient.h" @@ -16,6 +16,7 @@ #include "internal_client.h" #include "overlaywindow.h" #include "platform.h" +#include "renderloop.h" #include "scene.h" #include "screens.h" #include "shadow.h" @@ -60,7 +61,6 @@ namespace KWin extern int screen_number; extern bool is_multihead; -extern int currentRefreshRate(); Compositor *Compositor::s_compositor = nullptr; Compositor *Compositor::self() @@ -104,25 +104,15 @@ private: bool m_owning; }; -static inline qint64 milliToNano(int milli) { return qint64(milli) * 1000 * 1000; } -static inline qint64 nanoToMilli(int nano) { return nano / (1000*1000); } - Compositor::Compositor(QObject* workspace) : QObject(workspace) , m_state(State::Off) , m_selectionOwner(nullptr) - , vBlankInterval(0) - , fpsInterval(0) - , m_timeSinceLastVBlank(0) , m_scene(nullptr) - , m_bufferSwapPending(false) - , m_composeAtSwapCompletion(false) { connect(options, &Options::configChanged, this, &Compositor::configChanged); connect(options, &Options::animationSpeedChanged, this, &Compositor::configChanged); - m_monotonicClock.start(); - // 2 sec which should be enough to restart the compositor. static const int compositorLostMessageDelay = 2000; @@ -336,16 +326,18 @@ void Compositor::startupWithWorkspace() Workspace::self()->markXStackingOrderAsDirty(); Q_ASSERT(m_scene); - connect(workspace(), &Workspace::destroyed, this, [this] { compositeTimer.stop(); }); - fpsInterval = options->maxFpsInterval(); - - if (m_scene->syncsToVBlank()) { - // If we do vsync, set the fps to the next multiple of the vblank rate. - vBlankInterval = milliToNano(1000) / currentRefreshRate(); - fpsInterval = qMax((fpsInterval / vBlankInterval) * vBlankInterval, vBlankInterval); + const Platform *platform = kwinApp()->platform(); + if (platform->isPerScreenRenderingEnabled()) { + const QVector outputs = platform->enabledOutputs(); + for (AbstractOutput *output : outputs) { + registerRenderLoop(output->renderLoop(), output); + } + connect(platform, &Platform::outputEnabled, + this, &Compositor::handleOutputEnabled); + connect(platform, &Platform::outputDisabled, + this, &Compositor::handleOutputDisabled); } else { - // No vsync - DO NOT set "0", would cause div-by-zero segfaults. - vBlankInterval = milliToNano(1); + registerRenderLoop(platform->renderLoop(), nullptr); } // Sets also the 'effects' pointer. @@ -383,13 +375,47 @@ void Compositor::startupWithWorkspace() // Render at least once. addRepaintFull(); - performCompositing(); +} + +void Compositor::registerRenderLoop(RenderLoop *renderLoop, AbstractOutput *output) +{ + Q_ASSERT(!m_renderLoops.contains(renderLoop)); + m_renderLoops.insert(renderLoop, output); + connect(renderLoop, &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested); +} + +void Compositor::unregisterRenderLoop(RenderLoop *renderLoop) +{ + Q_ASSERT(m_renderLoops.contains(renderLoop)); + m_renderLoops.remove(renderLoop); + disconnect(renderLoop, &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested); +} + +void Compositor::handleOutputEnabled(AbstractOutput *output) +{ + registerRenderLoop(output->renderLoop(), output); +} + +void Compositor::handleOutputDisabled(AbstractOutput *output) +{ + unregisterRenderLoop(output->renderLoop()); +} + +int Compositor::screenForRenderLoop(RenderLoop *renderLoop) const +{ + Q_ASSERT(m_renderLoops.contains(renderLoop)); + AbstractOutput *output = m_renderLoops.value(renderLoop); + if (!output) { + return -1; + } + return kwinApp()->platform()->enabledOutputs().indexOf(output); } void Compositor::scheduleRepaint() { - if (!compositeTimer.isActive()) - setCompositeTimer(); + for (auto it = m_renderLoops.constBegin(); it != m_renderLoops.constEnd(); ++it) { + it.key()->scheduleRepaint(); + } } void Compositor::stop() @@ -445,9 +471,17 @@ void Compositor::stop() } } + while (!m_renderLoops.isEmpty()) { + unregisterRenderLoop(m_renderLoops.firstKey()); + } + + disconnect(kwinApp()->platform(), &Platform::outputEnabled, + this, &Compositor::handleOutputEnabled); + disconnect(kwinApp()->platform(), &Platform::outputDisabled, + this, &Compositor::handleOutputDisabled); + delete m_scene; m_scene = nullptr; - compositeTimer.stop(); m_state = State::Off; emit compositingToggled(false); @@ -550,49 +584,16 @@ void Compositor::addRepaintFull() addRepaint(screens()->geometry()); } -void Compositor::timerEvent(QTimerEvent *te) +void Compositor::handleFrameRequested(RenderLoop *renderLoop) { - if (te->timerId() == compositeTimer.timerId()) { - performCompositing(); - } else - QObject::timerEvent(te); -} - -void Compositor::aboutToSwapBuffers() -{ - m_bufferSwapPending = true; -} - -void Compositor::bufferSwapComplete() -{ - Q_ASSERT(m_bufferSwapPending); - m_bufferSwapPending = false; - - emit bufferSwapCompleted(); - - if (m_composeAtSwapCompletion) { - m_composeAtSwapCompletion = false; - performCompositing(); - } -} - -void Compositor::performCompositing() -{ - // If a buffer swap is still pending, we return to the event loop and - // continue processing events until the swap has completed. - if (m_bufferSwapPending) { - m_composeAtSwapCompletion = true; - compositeTimer.stop(); - return; - } - // If outputs are disabled, we return to the event loop and // continue processing events until the outputs are enabled again if (!kwinApp()->platform()->areOutputsEnabled()) { - compositeTimer.stop(); return; } + const int screenId = screenForRenderLoop(renderLoop); + // Create a list of all windows in the stacking order QList windows = Workspace::self()->xStackingOrder(); QList damaged; @@ -650,28 +651,20 @@ void Compositor::performCompositing() } } - const std::chrono::nanoseconds now = std::chrono::steady_clock::now().time_since_epoch(); - const std::chrono::milliseconds presentTime = - std::chrono::duration_cast(now); - if (m_framesToTestForSafety > 0 && (m_scene->compositingType() & OpenGLCompositing)) { kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PreFrame); } - m_renderTimer.start(); - if (kwinApp()->platform()->isPerScreenRenderingEnabled()) { - for (int screenId = 0; screenId < screens()->count(); ++screenId) { - const QRegion repaints = m_scene->repaints(screenId); - m_scene->resetRepaints(screenId); - m_scene->paint(screenId, repaints, windows, presentTime); - } - } else { - const QRegion repaints = m_scene->repaints(-1); - m_scene->resetRepaints(-1); + const std::chrono::milliseconds presentTime = + std::chrono::duration_cast(renderLoop->nextPresentationTimestamp()); + + const QRegion repaints = m_scene->repaints(screenId); + m_scene->resetRepaints(screenId); + + renderLoop->beginFrame(); + m_scene->paint(screenId, repaints, windows, presentTime); + renderLoop->endFrame(); - m_scene->paint(-1, repaints, windows, presentTime); - } - m_timeSinceLastVBlank = m_renderTimer.elapsed(); if (m_framesToTestForSafety > 0) { if (m_scene->compositingType() & OpenGLCompositing) { kwinApp()->platform()->createOpenGLSafePoint(Platform::OpenGLSafePoint::PostFrame); @@ -684,77 +677,28 @@ void Compositor::performCompositing() } if (waylandServer()) { - const auto currentTime = static_cast(m_monotonicClock.elapsed()); - for (Toplevel *win : qAsConst(windows)) { - if (auto surface = win->surface()) { - surface->frameRendered(currentTime); + const std::chrono::milliseconds frameTime = + std::chrono::duration_cast(renderLoop->lastPresentationTimestamp()); + + for (Toplevel *window : qAsConst(windows)) { + if (!window->readyForPainting()) { + continue; + } + if (waylandServer()->isScreenLocked() && + !(window->isLockScreen() || window->isInputMethod())) { + continue; + } + if (!window->isOnScreen(screenId)) { + continue; + } + if (auto surface = window->surface()) { + surface->frameRendered(frameTime.count()); } } if (!kwinApp()->platform()->isCursorHidden()) { Cursors::self()->currentCursor()->markAsRendered(); } } - - // Stop here to ensure *we* cause the next repaint schedule - not some effect - // through m_scene->paint(). - compositeTimer.stop(); - - // Trigger at least one more pass even if there would be nothing to paint, so that scene->idle() - // is called the next time. If there would be nothing pending, it will not restart the timer and - // scheduleRepaint() would restart it again somewhen later, called from functions that - // would again add something pending. - if (m_bufferSwapPending && m_scene->syncsToVBlank()) { - m_composeAtSwapCompletion = true; - } else { - scheduleRepaint(); - } -} - -void Compositor::setCompositeTimer() -{ - if (m_state != State::On) { - return; - } - - // Don't start the timer if we're waiting for a swap event - if (m_bufferSwapPending && m_composeAtSwapCompletion) - return; - - // Don't start the timer if all outputs are disabled - if (!kwinApp()->platform()->areOutputsEnabled()) { - return; - } - - uint waitTime = 1; - - if (fpsInterval > m_timeSinceLastVBlank) { - waitTime = nanoToMilli(fpsInterval - m_timeSinceLastVBlank); - if (!waitTime) { - // Will ensure we don't block out the eventloop - the system's just not faster ... - waitTime = 1; - } - } - /* else if (m_scene->syncsToVBlank() && m_timeSinceLastVBlank - fpsInterval < (vBlankInterval<<1)) { - // NOTICE - "for later" ------------------------------------------------------------------ - // It can happen that we push two frames within one refresh cycle. - // Swapping will then block even with triple buffering when the GPU does not discard but - // queues frames - // now here's the mean part: if we take that as "OMG, we're late - next frame ASAP", - // there'll immediately be 2 frames in the pipe, swapping will block, we think we're - // late ... ewww - // so instead we pad to the clock again and add 2ms safety to ensure the pipe is really - // free - // NOTICE: obviously m_timeSinceLastVBlank can be too big because we're too slow as well - // So if this code was enabled, we'd needlessly half the framerate once more (15 instead of 30) - waitTime = nanoToMilli(vBlankInterval - (m_timeSinceLastVBlank - fpsInterval)%vBlankInterval) + 2; - }*/ - else { - // "0" would be sufficient here, but the compositor isn't the WMs only task. - waitTime = 1; - } - - // Force 4fps minimum: - compositeTimer.start(qMin(waitTime, 250u), this); } bool Compositor::isActive() @@ -789,18 +733,9 @@ void WaylandCompositor::start() } } -int WaylandCompositor::refreshRate() const -{ - // TODO: This makes no sense on Wayland. First step would be to atleast - // set the refresh rate to the highest available one. Second step - // would be to not use a uniform value at all but per screen. - return KWin::currentRefreshRate(); -} - X11Compositor::X11Compositor(QObject *parent) : Compositor(parent) , m_suspended(options->isUseCompositing() ? NoReasonSuspend : UserSuspend) - , m_xrrRefreshRate(0) { } @@ -882,16 +817,15 @@ void X11Compositor::start() // Internal setup failed, abort. return; } - m_xrrRefreshRate = KWin::currentRefreshRate(); startupWithWorkspace(); } -void X11Compositor::performCompositing() +void X11Compositor::handleFrameRequested(RenderLoop *renderLoop) { if (scene()->usesOverlayWindow() && !isOverlayWindowVisible()) { // Return since nothing is visible. return; } - Compositor::performCompositing(); + Compositor::handleFrameRequested(renderLoop); } bool X11Compositor::checkForOverlayWindow(WId w) const @@ -919,11 +853,6 @@ bool X11Compositor::isOverlayWindowVisible() const return scene()->overlayWindow()->isVisible(); } -int X11Compositor::refreshRate() const -{ - return m_xrrRefreshRate; -} - void X11Compositor::updateClientCompositeBlocking(X11Client *c) { if (c) { diff --git a/composite.h b/composite.h index 195e2bc042..a904fd114b 100644 --- a/composite.h +++ b/composite.h @@ -12,14 +12,15 @@ #include #include -#include #include -#include #include namespace KWin { + +class AbstractOutput; class CompositorSelectionOwner; +class RenderLoop; class Scene; class X11Client; @@ -49,18 +50,6 @@ public: */ void scheduleRepaint(); - /** - * Notifies the compositor that SwapBuffers() is about to be called. - * Rendering of the next frame will be deferred until bufferSwapComplete() - * is called. - */ - void aboutToSwapBuffers(); - - /** - * Notifies the compositor that a pending buffer swap has completed. - */ - void bufferSwapComplete(); - /** * Toggles compositing, that is if the Compositor is suspended it will be resumed * and if the Compositor is active it will be suspended. @@ -79,7 +68,6 @@ public: * not shutting down itself. */ bool isActive(); - virtual int refreshRate() const = 0; Scene *scene() const { return m_scene; @@ -103,11 +91,9 @@ Q_SIGNALS: void aboutToDestroy(); void aboutToToggleCompositing(); void sceneCreated(); - void bufferSwapCompleted(); protected: explicit Compositor(QObject *parent = nullptr); - void timerEvent(QTimerEvent *te) override; virtual void start() = 0; void stop(); @@ -121,7 +107,6 @@ protected: * Continues the startup after Scene And Workspace are created */ void startupWithWorkspace(); - virtual void performCompositing(); virtual void configChanged(); @@ -129,34 +114,33 @@ protected: static Compositor *s_compositor; +protected Q_SLOTS: + virtual void handleFrameRequested(RenderLoop *renderLoop); + +private Q_SLOTS: + void handleOutputEnabled(AbstractOutput *output); + void handleOutputDisabled(AbstractOutput *output); + private: void initializeX11(); void cleanupX11(); - void setCompositeTimer(); - void releaseCompositorSelection(); void deleteUnusedSupportProperties(); + int screenForRenderLoop(RenderLoop *renderLoop) const; + void registerRenderLoop(RenderLoop *renderLoop, AbstractOutput *output); + void unregisterRenderLoop(RenderLoop *renderLoop); + State m_state; - QBasicTimer compositeTimer; CompositorSelectionOwner *m_selectionOwner; QTimer m_releaseSelectionTimer; QList m_unusedSupportProperties; QTimer m_unusedSupportPropertyTimer; - qint64 vBlankInterval, fpsInterval; - - qint64 m_timeSinceLastVBlank; - Scene *m_scene; - - bool m_bufferSwapPending; - bool m_composeAtSwapCompletion; - int m_framesToTestForSafety = 3; - QElapsedTimer m_renderTimer; - QElapsedTimer m_monotonicClock; + QMap m_renderLoops; }; class KWIN_EXPORT WaylandCompositor : public Compositor @@ -165,8 +149,6 @@ class KWIN_EXPORT WaylandCompositor : public Compositor public: static WaylandCompositor *create(QObject *parent = nullptr); - int refreshRate() const override; - void toggleCompositing() override; protected: @@ -239,15 +221,13 @@ public: */ bool isOverlayWindowVisible() const; - int refreshRate() const override; - void updateClientCompositeBlocking(X11Client *client = nullptr); static X11Compositor *self(); protected: void start() override; - void performCompositing() override; + void handleFrameRequested(RenderLoop *renderLoop) override; private: explicit X11Compositor(QObject *parent); @@ -255,8 +235,6 @@ private: * Whether the Compositor is currently suspended, 8 bits encoding the reason */ SuspendReasons m_suspended; - - int m_xrrRefreshRate; }; } diff --git a/kcmkwin/kwincompositing/compositing.ui b/kcmkwin/kwincompositing/compositing.ui index b73e46d15c..58eee8199b 100644 --- a/kcmkwin/kwincompositing/compositing.ui +++ b/kcmkwin/kwincompositing/compositing.ui @@ -218,14 +218,14 @@ Alternatively, you might want to use the XRender backend instead. - + Tearing prevention ("vsync"): - + @@ -254,14 +254,14 @@ Alternatively, you might want to use the XRender backend instead. - + Keep window thumbnails: - + @@ -280,7 +280,7 @@ Alternatively, you might want to use the XRender backend instead. - + Applications can set a hint to block compositing when the window is open. @@ -292,6 +292,42 @@ Alternatively, you might want to use the XRender backend instead. + + + + Latency: + + + + + + + + Force lowest latency (may cause dropped frames) + + + + + Prefer lower latency + + + + + Balance of latency and smoothness + + + + + Prefer smoother animations + + + + + Force smoothest animations + + + + diff --git a/kcmkwin/kwincompositing/kwincompositing_setting.kcfg b/kcmkwin/kwincompositing/kwincompositing_setting.kcfg index 162c82b2c7..2cbe193077 100644 --- a/kcmkwin/kwincompositing/kwincompositing_setting.kcfg +++ b/kcmkwin/kwincompositing/kwincompositing_setting.kcfg @@ -69,6 +69,17 @@ true + + + + + + + + + LatencyMedium + + diff --git a/kwin.kcfg b/kwin.kcfg index 2b06efe52d..ea03032d23 100644 --- a/kwin.kcfg +++ b/kwin.kcfg @@ -214,15 +214,6 @@ - - 60 - - - 0 - - - 6144 - OpenGL @@ -254,6 +245,16 @@ true + + + + + + + + + LatencyMedium + diff --git a/libkwineffects/kwinglobals.h b/libkwineffects/kwinglobals.h index 8eece41909..50f3154167 100644 --- a/libkwineffects/kwinglobals.h +++ b/libkwineffects/kwinglobals.h @@ -191,6 +191,8 @@ private: } // namespace +Q_DECLARE_METATYPE(std::chrono::nanoseconds) + #define KWIN_SINGLETON_VARIABLE(ClassName, variableName) \ public: \ static ClassName *create(QObject *parent = nullptr);\ diff --git a/main.cpp b/main.cpp index a4e2c640ee..61a39fd0c1 100644 --- a/main.cpp +++ b/main.cpp @@ -103,6 +103,7 @@ Application::Application(Application::OperationMode mode, int &argc, char **argv qRegisterMetaType(); qRegisterMetaType("KWaylandServer::SurfaceInterface *"); qRegisterMetaType(); + qRegisterMetaType(); } void Application::setConfigLock(bool lock) diff --git a/options.cpp b/options.cpp index 8014f8b784..0ed1ad765a 100644 --- a/options.cpp +++ b/options.cpp @@ -30,50 +30,6 @@ namespace KWin #ifndef KCMRULES -int currentRefreshRate() -{ - return Options::currentRefreshRate(); -} - -int Options::currentRefreshRate() -{ - int rate = -1; - QString syncScreenName(QLatin1String("primary screen")); - if (options->refreshRate() > 0) { // use manually configured refresh rate - rate = options->refreshRate(); - } else if (Screens::self()->count() > 0) { - // prefer the refreshrate calculated from the screens mode information - // at least the nvidia driver reports 50Hz BS ... *again*! - int syncScreen = 0; - if (Screens::self()->count() > 1) { - const QByteArray syncDisplayDevice(qgetenv("__GL_SYNC_DISPLAY_DEVICE")); - // if __GL_SYNC_DISPLAY_DEVICE is exported, the GPU shall sync to that device - // so we try to use its refresh rate - if (!syncDisplayDevice.isEmpty()) { - for (int i = 0; i < Screens::self()->count(); ++i) { - if (Screens::self()->name(i) == syncDisplayDevice) { - syncScreenName = Screens::self()->name(i); - syncScreen = i; - break; - } - } - } - } - rate = qRound(Screens::self()->refreshRate(syncScreen)); // TODO forward float precision? - } - - // 0Hz or less is invalid, so we fallback to a default rate - if (rate <= 0) - rate = 60; // and not shitty 50Hz for sure! *grrr* - - // QTimer gives us 1msec (1000Hz) at best, so we ignore anything higher; - // however, additional throttling prevents very high rates from taking place anyway - else if (rate > 1000) - rate = 1000; - qCDebug(KWIN_CORE) << "Vertical Refresh rate " << rate << "Hz (" << syncScreenName << ")"; - return rate; -} - Options::Options(QObject *parent) : QObject(parent) , m_settings(new Settings(kwinApp()->config())) @@ -97,14 +53,12 @@ Options::Options(QObject *parent) , m_hideUtilityWindowsForInactive(false) , m_xwaylandCrashPolicy(Options::defaultXwaylandCrashPolicy()) , m_xwaylandMaxCrashCount(Options::defaultXwaylandMaxCrashCount()) + , m_latencyPolicy(Options::defaultLatencyPolicy()) , m_compositingMode(Options::defaultCompositingMode()) , m_useCompositing(Options::defaultUseCompositing()) , m_hiddenPreviews(Options::defaultHiddenPreviews()) , m_glSmoothScale(Options::defaultGlSmoothScale()) , m_xrenderSmoothScale(Options::defaultXrenderSmoothScale()) - , m_maxFpsInterval(Options::defaultMaxFpsInterval()) - , m_refreshRate(Options::defaultRefreshRate()) - , m_vBlankTime(Options::defaultVBlankTime()) , m_glStrictBinding(Options::defaultGlStrictBinding()) , m_glStrictBindingFollowsDriver(Options::defaultGlStrictBindingFollowsDriver()) , m_glCoreProfile(Options::defaultGLCoreProfile()) @@ -617,33 +571,6 @@ void Options::setXrenderSmoothScale(bool xrenderSmoothScale) emit xrenderSmoothScaleChanged(); } -void Options::setMaxFpsInterval(qint64 maxFpsInterval) -{ - if (m_maxFpsInterval == maxFpsInterval) { - return; - } - m_maxFpsInterval = maxFpsInterval; - emit maxFpsIntervalChanged(); -} - -void Options::setRefreshRate(uint refreshRate) -{ - if (m_refreshRate == refreshRate) { - return; - } - m_refreshRate = refreshRate; - emit refreshRateChanged(); -} - -void Options::setVBlankTime(qint64 vBlankTime) -{ - if (m_vBlankTime == vBlankTime) { - return; - } - m_vBlankTime = vBlankTime; - emit vBlankTimeChanged(); -} - void Options::setGlStrictBinding(bool glStrictBinding) { if (m_glStrictBinding == glStrictBinding) { @@ -707,6 +634,20 @@ void Options::setGlPreferBufferSwap(char glPreferBufferSwap) emit glPreferBufferSwapChanged(); } +LatencyPolicy Options::latencyPolicy() const +{ + return m_latencyPolicy; +} + +void Options::setLatencyPolicy(LatencyPolicy policy) +{ + if (m_latencyPolicy == policy) { + return; + } + m_latencyPolicy = policy; + emit latencyPolicyChanged(); +} + void Options::setGlPlatformInterface(OpenGLPlatformInterface interface) { // check environment variable @@ -800,12 +741,6 @@ void Options::loadConfig() setCommandAll2(mouseCommand(config.readEntry("CommandAll2", "Toggle raise and lower"), false)); setCommandAll3(mouseCommand(config.readEntry("CommandAll3", "Resize"), false)); - // TODO: should they be moved into reloadCompositingSettings? - config = KConfigGroup(m_settings->config(), "Compositing"); - setMaxFpsInterval(1 * 1000 * 1000 * 1000 / config.readEntry("MaxFPS", Options::defaultMaxFps())); - setRefreshRate(config.readEntry("RefreshRate", Options::defaultRefreshRate())); - setVBlankTime(config.readEntry("VBlankTime", Options::defaultVBlankTime()) * 1000); // config in micro, value in nano resolution - // Modifier Only Shortcuts config = KConfigGroup(m_settings->config(), "ModifierOnlyShortcuts"); m_modifierOnlyShortcuts.clear(); @@ -860,7 +795,7 @@ void Options::syncFromKcfgc() setElectricBorderCornerRatio(m_settings->electricBorderCornerRatio()); setWindowsBlockCompositing(m_settings->windowsBlockCompositing()); setMoveMinimizedWindowsToEndOfTabBoxFocusChain(m_settings->moveMinimizedWindowsToEndOfTabBoxFocusChain()); - + setLatencyPolicy(m_settings->latencyPolicy()); } bool Options::loadCompositingConfig (bool force) diff --git a/options.h b/options.h index 7969e83b79..041848f468 100644 --- a/options.h +++ b/options.h @@ -42,12 +42,24 @@ enum XwaylandCrashPolicy { Restart, }; +/** + * This enum type specifies the latency level configured by the user. + */ +enum LatencyPolicy { + LatencyExteremelyLow, + LatencyLow, + LatencyMedium, + LatencyHigh, + LatencyExtremelyHigh, +}; + class Settings; class KWIN_EXPORT Options : public QObject { Q_OBJECT Q_ENUMS(XwaylandCrashPolicy) + Q_ENUMS(LatencyPolicy) Q_PROPERTY(FocusPolicy focusPolicy READ focusPolicy WRITE setFocusPolicy NOTIFY focusPolicyChanged) Q_PROPERTY(XwaylandCrashPolicy xwaylandCrashPolicy READ xwaylandCrashPolicy WRITE setXwaylandCrashPolicy NOTIFY xwaylandCrashPolicyChanged) Q_PROPERTY(int xwaylandMaxCrashCount READ xwaylandMaxCrashCount WRITE setXwaylandMaxCrashCount NOTIFY xwaylandMaxCrashCountChanged) @@ -164,9 +176,6 @@ class KWIN_EXPORT Options : public QObject */ Q_PROPERTY(int glSmoothScale READ glSmoothScale WRITE setGlSmoothScale NOTIFY glSmoothScaleChanged) Q_PROPERTY(bool xrenderSmoothScale READ isXrenderSmoothScale WRITE setXrenderSmoothScale NOTIFY xrenderSmoothScaleChanged) - Q_PROPERTY(qint64 maxFpsInterval READ maxFpsInterval WRITE setMaxFpsInterval NOTIFY maxFpsIntervalChanged) - Q_PROPERTY(uint refreshRate READ refreshRate WRITE setRefreshRate NOTIFY refreshRateChanged) - Q_PROPERTY(qint64 vBlankTime READ vBlankTime WRITE setVBlankTime NOTIFY vBlankTimeChanged) Q_PROPERTY(bool glStrictBinding READ isGlStrictBinding WRITE setGlStrictBinding NOTIFY glStrictBindingChanged) /** * Whether strict binding follows the driver or has been overwritten by a user defined config value. @@ -178,6 +187,7 @@ class KWIN_EXPORT Options : public QObject Q_PROPERTY(GlSwapStrategy glPreferBufferSwap READ glPreferBufferSwap WRITE setGlPreferBufferSwap NOTIFY glPreferBufferSwapChanged) Q_PROPERTY(KWin::OpenGLPlatformInterface glPlatformInterface READ glPlatformInterface WRITE setGlPlatformInterface NOTIFY glPlatformInterfaceChanged) Q_PROPERTY(bool windowsBlockCompositing READ windowsBlockCompositing WRITE setWindowsBlockCompositing NOTIFY windowsBlockCompositingChanged) + Q_PROPERTY(LatencyPolicy latencyPolicy READ latencyPolicy WRITE setLatencyPolicy NOTIFY latencyPolicyChanged) public: explicit Options(QObject *parent = nullptr); @@ -555,16 +565,7 @@ public: return m_xrenderSmoothScale; } - qint64 maxFpsInterval() const { - return m_maxFpsInterval; - } // Settings that should be auto-detected - uint refreshRate() const { - return m_refreshRate; - } - qint64 vBlankTime() const { - return m_vBlankTime; - } bool isGlStrictBinding() const { return m_glStrictBinding; } @@ -594,6 +595,7 @@ public: } QStringList modifierOnlyDBusShortcut(Qt::KeyboardModifier mod) const; + LatencyPolicy latencyPolicy() const; // setters void setFocusPolicy(FocusPolicy focusPolicy); @@ -645,9 +647,6 @@ public: void setHiddenPreviews(int hiddenPreviews); void setGlSmoothScale(int glSmoothScale); void setXrenderSmoothScale(bool xrenderSmoothScale); - void setMaxFpsInterval(qint64 maxFpsInterval); - void setRefreshRate(uint refreshRate); - void setVBlankTime(qint64 vBlankTime); void setGlStrictBinding(bool glStrictBinding); void setGlStrictBindingFollowsDriver(bool glStrictBindingFollowsDriver); void setGLCoreProfile(bool glCoreProfile); @@ -655,6 +654,7 @@ public: void setGlPlatformInterface(OpenGLPlatformInterface interface); void setWindowsBlockCompositing(bool set); void setMoveMinimizedWindowsToEndOfTabBoxFocusChain(bool set); + void setLatencyPolicy(LatencyPolicy policy); // default values static WindowOperation defaultOperationTitlebarDblClick() { @@ -732,18 +732,6 @@ public: static bool defaultXrenderSmoothScale() { return false; } - static qint64 defaultMaxFpsInterval() { - return (1 * 1000 * 1000 * 1000) /60.0; // nanoseconds / Hz - } - static int defaultMaxFps() { - return 60; - } - static uint defaultRefreshRate() { - return 0; - } - static uint defaultVBlankTime() { - return 6000; // 6ms - } static bool defaultGlStrictBinding() { return true; } @@ -765,6 +753,9 @@ public: static int defaultXwaylandMaxCrashCount() { return 3; } + static LatencyPolicy defaultLatencyPolicy() { + return LatencyMedium; + } /** * Performs loading all settings except compositing related. */ @@ -775,8 +766,6 @@ public: bool loadCompositingConfig(bool force); void reparseConfiguration(); - static int currentRefreshRate(); - //---------------------- Q_SIGNALS: // for properties @@ -830,9 +819,6 @@ Q_SIGNALS: void hiddenPreviewsChanged(); void glSmoothScaleChanged(); void xrenderSmoothScaleChanged(); - void maxFpsIntervalChanged(); - void refreshRateChanged(); - void vBlankTimeChanged(); void glStrictBindingChanged(); void glStrictBindingFollowsDriverChanged(); void glCoreProfileChanged(); @@ -840,7 +826,7 @@ Q_SIGNALS: void glPlatformInterfaceChanged(); void windowsBlockCompositingChanged(); void animationSpeedChanged(); - + void latencyPolicyChanged(); void configChanged(); private: @@ -869,16 +855,14 @@ private: bool m_hideUtilityWindowsForInactive; XwaylandCrashPolicy m_xwaylandCrashPolicy; int m_xwaylandMaxCrashCount; + LatencyPolicy m_latencyPolicy; CompositingType m_compositingMode; bool m_useCompositing; HiddenPreviews m_hiddenPreviews; int m_glSmoothScale; bool m_xrenderSmoothScale; - qint64 m_maxFpsInterval; // Settings that should be auto-detected - uint m_refreshRate; - qint64 m_vBlankTime; bool m_glStrictBinding; bool m_glStrictBindingFollowsDriver; bool m_glCoreProfile; diff --git a/platform.cpp b/platform.cpp index d84ddee644..9ed997c6bf 100644 --- a/platform.cpp +++ b/platform.cpp @@ -445,6 +445,11 @@ void Platform::setPerScreenRenderingEnabled(bool enabled) m_isPerScreenRenderingEnabled = enabled; } +RenderLoop *Platform::renderLoop() const +{ + return nullptr; +} + void Platform::warpPointer(const QPointF &globalPos) { Q_UNUSED(globalPos) diff --git a/platform.h b/platform.h index 54af5c3689..7274e5879f 100644 --- a/platform.h +++ b/platform.h @@ -36,6 +36,7 @@ class OpenGLBackend; class Outline; class OutlineVisual; class QPainterBackend; +class RenderLoop; class Scene; class ScreenEdges; class Toplevel; @@ -461,6 +462,12 @@ public: */ bool isPerScreenRenderingEnabled() const; + /** + * If the Platform doesn't support per screen rendering, this function returns the + * RenderLoop that drives compositing. + */ + virtual RenderLoop *renderLoop() const; + public Q_SLOTS: void pointerMotion(const QPointF &position, quint32 time); void pointerButtonPressed(quint32 button, quint32 time); diff --git a/platformsupport/CMakeLists.txt b/platformsupport/CMakeLists.txt index f395a74656..8eb00085bc 100644 --- a/platformsupport/CMakeLists.txt +++ b/platformsupport/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(scenes) +add_subdirectory(vsyncconvenience) diff --git a/platformsupport/scenes/opengl/backend.cpp b/platformsupport/scenes/opengl/backend.cpp index 021b9d7e9b..833ee793cf 100644 --- a/platformsupport/scenes/opengl/backend.cpp +++ b/platformsupport/scenes/opengl/backend.cpp @@ -19,8 +19,7 @@ namespace KWin { OpenGLBackend::OpenGLBackend() - : m_syncsToVBlank(false) - , m_directRendering(false) + : m_directRendering(false) , m_haveBufferAge(false) , m_failed(false) { diff --git a/platformsupport/scenes/opengl/backend.h b/platformsupport/scenes/opengl/backend.h index ac80bc1665..884def564b 100644 --- a/platformsupport/scenes/opengl/backend.h +++ b/platformsupport/scenes/opengl/backend.h @@ -83,16 +83,6 @@ public: bool isFailed() const { return m_failed; } - /** - * @brief Whether the Backend provides VSync. - * - * Currently only the GLX backend can provide VSync. - * - * @return bool @c true if VSync support is available, @c false otherwise - */ - bool syncsToVBlank() const { - return m_syncsToVBlank; - } /** * @brief Whether the backend uses direct rendering. * @@ -170,16 +160,6 @@ protected: * @param reason The reason why the initialization failed. */ void setFailed(const QString &reason); - /** - * @brief Sets whether the backend provides VSync. - * - * Should be called by the concrete subclass once it is determined whether VSync is supported. - * If the subclass does not call this method, the backend defaults to @c false. - * @param enabled @c true if VSync support available, @c false otherwise. - */ - void setSyncsToVBlank(bool enabled) { - m_syncsToVBlank = enabled; - } /** * @brief Sets whether the OpenGL context is direct. * @@ -227,10 +207,6 @@ protected: } private: - /** - * @brief Whether VSync is available and used, defaults to @c false. - */ - bool m_syncsToVBlank; /** * @brief Whether direct rendering is used, defaults to @c false. */ diff --git a/platformsupport/vsyncconvenience/CMakeLists.txt b/platformsupport/vsyncconvenience/CMakeLists.txt new file mode 100644 index 0000000000..29ae9ad5a8 --- /dev/null +++ b/platformsupport/vsyncconvenience/CMakeLists.txt @@ -0,0 +1,8 @@ +set(vsyncconvenience_SOURCES + softwarevsyncmonitor.cpp + vsyncmonitor.cpp +) + +add_library(VsyncSupport OBJECT ${vsyncconvenience_SOURCES}) +target_link_libraries(VsyncSupport Qt5::Core Qt5::Gui) +target_include_directories(VsyncSupport PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/platformsupport/vsyncconvenience/softwarevsyncmonitor.cpp b/platformsupport/vsyncconvenience/softwarevsyncmonitor.cpp new file mode 100644 index 0000000000..484e445ace --- /dev/null +++ b/platformsupport/vsyncconvenience/softwarevsyncmonitor.cpp @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "softwarevsyncmonitor.h" + +namespace KWin +{ + +SoftwareVsyncMonitor *SoftwareVsyncMonitor::create(QObject *parent) +{ + return new SoftwareVsyncMonitor(parent); +} + +SoftwareVsyncMonitor::SoftwareVsyncMonitor(QObject *parent) + : VsyncMonitor(parent) + , m_softwareClock(new QTimer(this)) +{ + connect(m_softwareClock, &QTimer::timeout, this, &SoftwareVsyncMonitor::handleSyntheticVsync); + m_softwareClock->setSingleShot(true); +} + +int SoftwareVsyncMonitor::refreshRate() const +{ + return m_refreshRate; +} + +void SoftwareVsyncMonitor::setRefreshRate(int refreshRate) +{ + m_refreshRate = refreshRate; +} + +void SoftwareVsyncMonitor::handleSyntheticVsync() +{ + emit vblankOccurred(m_vblankTimestamp); +} + +template +T alignTimestamp(const T ×tamp, const T &alignment) +{ + return timestamp + ((alignment - (timestamp % alignment)) % alignment); +} + +void SoftwareVsyncMonitor::arm() +{ + if (m_softwareClock->isActive()) { + return; + } + + const std::chrono::nanoseconds currentTime(std::chrono::steady_clock::now().time_since_epoch()); + const std::chrono::nanoseconds vblankInterval(1'000'000'000'000ull / m_refreshRate); + + m_vblankTimestamp = alignTimestamp(currentTime, vblankInterval); + + m_softwareClock->start(std::chrono::duration_cast(m_vblankTimestamp - currentTime)); +} + +} // namespace KWin diff --git a/platformsupport/vsyncconvenience/softwarevsyncmonitor.h b/platformsupport/vsyncconvenience/softwarevsyncmonitor.h new file mode 100644 index 0000000000..9a29a67428 --- /dev/null +++ b/platformsupport/vsyncconvenience/softwarevsyncmonitor.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "vsyncmonitor.h" + +#include + +namespace KWin +{ + +/** + * The SoftwareVsyncMonitor class provides synthetic vblank events with constant interval. + * + * The software vsync monitor can never fail and it is always available. It can be used as + * fallback if hardware based approaches to monitor vsync events are unavailable. + * + * The vblank interval can be changed by calling the setRefreshRate() function. + */ +class KWIN_EXPORT SoftwareVsyncMonitor : public VsyncMonitor +{ + Q_OBJECT + +public: + static SoftwareVsyncMonitor *create(QObject *parent); + + int refreshRate() const; + void setRefreshRate(int refreshRate); + +public Q_SLOTS: + void arm() override; + +private: + explicit SoftwareVsyncMonitor(QObject *parent = nullptr); + void handleSyntheticVsync(); + + QTimer *m_softwareClock = nullptr; + int m_refreshRate = 60000; + std::chrono::nanoseconds m_vblankTimestamp = std::chrono::nanoseconds::zero(); +}; + +} // namespace KWin diff --git a/platformsupport/vsyncconvenience/vsyncmonitor.cpp b/platformsupport/vsyncconvenience/vsyncmonitor.cpp new file mode 100644 index 0000000000..660a957dd4 --- /dev/null +++ b/platformsupport/vsyncconvenience/vsyncmonitor.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "vsyncmonitor.h" + +namespace KWin +{ + +VsyncMonitor::VsyncMonitor(QObject *parent) + : QObject(parent) +{ +} + +} // namespace KWin diff --git a/platformsupport/vsyncconvenience/vsyncmonitor.h b/platformsupport/vsyncconvenience/vsyncmonitor.h new file mode 100644 index 0000000000..21641fee15 --- /dev/null +++ b/platformsupport/vsyncconvenience/vsyncmonitor.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwinglobals.h" + +#include + +#include + +namespace KWin +{ + +/** + * The VsyncMonitor class provides a convenient way to monitor vblank events. + */ +class KWIN_EXPORT VsyncMonitor : public QObject +{ + Q_OBJECT + +public: + explicit VsyncMonitor(QObject *parent = nullptr); + +public Q_SLOTS: + virtual void arm() = 0; + +signals: + void errorOccurred(); + void vblankOccurred(std::chrono::nanoseconds timestamp); +}; + +} // namespace KWin diff --git a/plugins/platforms/drm/abstract_egl_drm_backend.cpp b/plugins/platforms/drm/abstract_egl_drm_backend.cpp index 3b650cda86..08f337f6f2 100644 --- a/plugins/platforms/drm/abstract_egl_drm_backend.cpp +++ b/plugins/platforms/drm/abstract_egl_drm_backend.cpp @@ -20,7 +20,6 @@ AbstractEglDrmBackend::AbstractEglDrmBackend(DrmBackend *drmBackend, DrmGpu *gpu m_gpu->setEglBackend(this); // Egl is always direct rendering. setIsDirectRendering(true); - setSyncsToVBlank(true); connect(m_gpu, &DrmGpu::outputEnabled, this, &AbstractEglDrmBackend::addOutput); connect(m_gpu, &DrmGpu::outputDisabled, this, &AbstractEglDrmBackend::removeOutput); } diff --git a/plugins/platforms/drm/drm_backend.cpp b/plugins/platforms/drm/drm_backend.cpp index 562c59b965..8f78d6469c 100644 --- a/plugins/platforms/drm/drm_backend.cpp +++ b/plugins/platforms/drm/drm_backend.cpp @@ -16,6 +16,7 @@ #include "logging.h" #include "logind.h" #include "main.h" +#include "renderloop_p.h" #include "scene_qpainter_drm_backend.h" #include "udev.h" #include "wayland_server.h" @@ -188,8 +189,11 @@ void DrmBackend::reactivate() } // restart compositor m_pageFlipsPending = 0; + + for (DrmOutput *output : qAsConst(m_outputs)) { + output->renderLoop()->uninhibit(); + } if (Compositor *compositor = Compositor::self()) { - compositor->bufferSwapComplete(); compositor->addRepaintFull(); } } @@ -199,36 +203,61 @@ void DrmBackend::deactivate() if (!m_active) { return; } - // block compositor - if (m_pageFlipsPending == 0 && Compositor::self()) { - Compositor::self()->aboutToSwapBuffers(); - } - // hide cursor and disable - for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { - DrmOutput *o = *it; - o->hideCursor(); + + for (DrmOutput *output : qAsConst(m_outputs)) { + output->hideCursor(); + output->renderLoop()->inhibit(); } + m_active = false; } +static std::chrono::nanoseconds convertTimestamp(const timespec ×tamp) +{ + return std::chrono::seconds(timestamp.tv_sec) + std::chrono::nanoseconds(timestamp.tv_nsec); +} + +static std::chrono::nanoseconds convertTimestamp(clockid_t sourceClock, clockid_t targetClock, + const timespec ×tamp) +{ + if (sourceClock == targetClock) { + return convertTimestamp(timestamp); + } + + timespec sourceCurrentTime = {}; + timespec targetCurrentTime = {}; + + clock_gettime(sourceClock, &sourceCurrentTime); + clock_gettime(targetClock, &targetCurrentTime); + + const auto delta = convertTimestamp(sourceCurrentTime) - convertTimestamp(timestamp); + return convertTimestamp(targetCurrentTime) - delta; +} + void DrmBackend::pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data) { Q_UNUSED(fd) Q_UNUSED(frame) - Q_UNUSED(sec) - Q_UNUSED(usec) - auto output = reinterpret_cast(data); + + auto output = static_cast(data); + + DrmGpu *gpu = output->gpu(); + DrmBackend *backend = output->m_backend; + + std::chrono::nanoseconds timestamp = convertTimestamp(gpu->presentationClock(), + CLOCK_MONOTONIC, + { sec, usec * 1000 }); + if (timestamp == std::chrono::nanoseconds::zero()) { + qCDebug(KWIN_DRM, "Got invalid timestamp (sec: %u, usec: %u) on output %s", + sec, usec, qPrintable(output->name())); + timestamp = std::chrono::steady_clock::now().time_since_epoch(); + } output->pageFlipped(); - output->m_backend->m_pageFlipsPending--; - if (output->m_backend->m_pageFlipsPending == 0) { - // TODO: improve, this currently means we wait for all page flips or all outputs. - // It would be better to driver the repaint per output + backend->m_pageFlipsPending--; - if (Compositor::self()) { - Compositor::self()->bufferSwapComplete(); - } - } + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(output->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(timestamp); } void DrmBackend::openDrm() @@ -526,9 +555,6 @@ bool DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) if (output->present(buffer)) { m_pageFlipsPending++; - if (m_pageFlipsPending == 1 && Compositor::self()) { - Compositor::self()->aboutToSwapBuffers(); - } return true; } else if (output->gpu()->deleteBufferAfterPageFlip()) { delete buffer; diff --git a/plugins/platforms/drm/drm_gpu.cpp b/plugins/platforms/drm/drm_gpu.cpp index 85bf97de63..fc7be0b3ed 100644 --- a/plugins/platforms/drm/drm_gpu.cpp +++ b/plugins/platforms/drm/drm_gpu.cpp @@ -48,6 +48,13 @@ DrmGpu::DrmGpu(DrmBackend *backend, QByteArray devNode, int fd, int drmId) : m_b m_cursorSize.setHeight(64); } + int ret = drmGetCap(fd, DRM_CAP_TIMESTAMP_MONOTONIC, &capability); + if (ret == 0 && capability == 1) { + m_presentationClock = CLOCK_MONOTONIC; + } else { + m_presentationClock = CLOCK_REALTIME; + } + // find out if this GPU is using the NVidia proprietary driver DrmScopedPointer version(drmGetVersion(fd)); m_useEglStreams = strstr(version->name, "nvidia-drm"); @@ -69,6 +76,11 @@ DrmGpu::~DrmGpu() close(m_fd); } +clockid_t DrmGpu::presentationClock() const +{ + return m_presentationClock; +} + void DrmGpu::tryAMS() { m_atomicModeSetting = false; diff --git a/plugins/platforms/drm/drm_gpu.h b/plugins/platforms/drm/drm_gpu.h index ca8c0c6f6d..11bacaf445 100644 --- a/plugins/platforms/drm/drm_gpu.h +++ b/plugins/platforms/drm/drm_gpu.h @@ -101,6 +101,12 @@ public: m_eglBackend = eglBackend; } + /** + * Returns the clock from which presentation timestamps are sourced. The returned value + * can be either CLOCK_MONOTONIC or CLOCK_REALTIME. + */ + clockid_t presentationClock() const; + Q_SIGNALS: void outputAdded(DrmOutput *output); void outputRemoved(DrmOutput *output); @@ -128,6 +134,7 @@ private: bool m_deleteBufferAfterPageFlip; gbm_device* m_gbmDevice; EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + clockid_t m_presentationClock; // all available planes: primarys, cursors and overlays QVector m_planes; diff --git a/plugins/platforms/drm/drm_output.cpp b/plugins/platforms/drm/drm_output.cpp index c28200d6cc..fddf6765d2 100644 --- a/plugins/platforms/drm/drm_output.cpp +++ b/plugins/platforms/drm/drm_output.cpp @@ -17,6 +17,7 @@ #include "logging.h" #include "main.h" #include "screens.h" +#include "renderloop.h" #include "wayland_server.h" // KWayland #include @@ -43,6 +44,7 @@ DrmOutput::DrmOutput(DrmBackend *backend, DrmGpu *gpu) : AbstractWaylandOutput(backend) , m_backend(backend) , m_gpu(gpu) + , m_renderLoop(new RenderLoop(this)) { } @@ -52,6 +54,11 @@ DrmOutput::~DrmOutput() teardown(); } +RenderLoop *DrmOutput::renderLoop() const +{ + return m_renderLoop; +} + void DrmOutput::teardown() { if (m_deleted) { @@ -869,6 +876,7 @@ bool DrmOutput::presentAtomically(DrmBuffer *buffer) m_lastWorkingState.planeTransformations = m_primaryPlane->transformation(); } m_lastWorkingState.valid = true; + m_renderLoop->setRefreshRate(refreshRateForMode(&m_mode)); } m_pageFlipPending = true; return true; diff --git a/plugins/platforms/drm/drm_output.h b/plugins/platforms/drm/drm_output.h index 936c2c0fda..8f2f4bfd4b 100644 --- a/plugins/platforms/drm/drm_output.h +++ b/plugins/platforms/drm/drm_output.h @@ -39,6 +39,9 @@ class KWIN_EXPORT DrmOutput : public AbstractWaylandOutput public: ///deletes the output, calling this whilst a page flip is pending will result in an error ~DrmOutput() override; + + RenderLoop *renderLoop() const override; + ///queues deleting the output after a page flip has completed. void teardown(); void releaseGbm(); @@ -153,6 +156,7 @@ private: DpmsMode m_dpmsMode = DpmsMode::On; DpmsMode m_dpmsModePending = DpmsMode::On; QByteArray m_uuid; + RenderLoop *m_renderLoop; uint32_t m_blobId = 0; DrmPlane *m_primaryPlane = nullptr; diff --git a/plugins/platforms/drm/egl_multi_backend.cpp b/plugins/platforms/drm/egl_multi_backend.cpp index cbcf16530b..00aeb0fd49 100644 --- a/plugins/platforms/drm/egl_multi_backend.cpp +++ b/plugins/platforms/drm/egl_multi_backend.cpp @@ -17,7 +17,6 @@ EglMultiBackend::EglMultiBackend(AbstractEglDrmBackend *backend0) : OpenGLBacken { m_backends.append(backend0); setIsDirectRendering(true); - setSyncsToVBlank(true); } EglMultiBackend::~EglMultiBackend() diff --git a/plugins/platforms/fbdev/CMakeLists.txt b/plugins/platforms/fbdev/CMakeLists.txt index 3cfa5f42c1..2391eb24cc 100644 --- a/plugins/platforms/fbdev/CMakeLists.txt +++ b/plugins/platforms/fbdev/CMakeLists.txt @@ -1,12 +1,13 @@ set(FBDEV_SOURCES fb_backend.cpp + fbvsyncmonitor.cpp logging.cpp scene_qpainter_fb_backend.cpp ) add_library(KWinWaylandFbdevBackend MODULE ${FBDEV_SOURCES}) set_target_properties(KWinWaylandFbdevBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") -target_link_libraries(KWinWaylandFbdevBackend kwin SceneQPainterBackend) +target_link_libraries(KWinWaylandFbdevBackend kwin SceneQPainterBackend VsyncSupport) install( TARGETS diff --git a/plugins/platforms/fbdev/fb_backend.cpp b/plugins/platforms/fbdev/fb_backend.cpp index 43405986bb..fac99e1dcf 100644 --- a/plugins/platforms/fbdev/fb_backend.cpp +++ b/plugins/platforms/fbdev/fb_backend.cpp @@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "fb_backend.h" +#include "fbvsyncmonitor.h" #include "composite.h" #include "logging.h" #include "logind.h" +#include "renderloop_p.h" #include "scene_qpainter_fb_backend.h" +#include "softwarevsyncmonitor.h" #include "virtual_terminal.h" #include "udev.h" // system @@ -25,22 +28,56 @@ namespace KWin { -FramebufferOutput::FramebufferOutput(QObject *parent): - AbstractWaylandOutput(parent) +FramebufferOutput::FramebufferOutput(FramebufferBackend *backend, QObject *parent) + : AbstractWaylandOutput(parent) + , m_renderLoop(new RenderLoop(this)) { setName("FB-0"); + + if (!qEnvironmentVariableIsSet("KWIN_FB_NO_HW_VSYNC")) { + m_vsyncMonitor = FramebufferVsyncMonitor::create(backend->fileDescriptor(), this); + } + if (!m_vsyncMonitor) { + SoftwareVsyncMonitor *monitor = SoftwareVsyncMonitor::create(this); + monitor->setRefreshRate(m_renderLoop->refreshRate()); + connect(m_renderLoop, &RenderLoop::refreshRateChanged, this, [this, monitor]() { + monitor->setRefreshRate(m_renderLoop->refreshRate()); + }); + m_vsyncMonitor = monitor; + } + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &FramebufferOutput::vblank); +} + +RenderLoop *FramebufferOutput::renderLoop() const +{ + return m_renderLoop; +} + +VsyncMonitor *FramebufferOutput::vsyncMonitor() const +{ + return m_vsyncMonitor; } void FramebufferOutput::init(const QSize &pixelSize, const QSize &physicalSize) { + const int refreshRate = 60000; // TODO: get actual refresh rate of fb device? + m_renderLoop->setRefreshRate(refreshRate); + KWaylandServer::OutputDeviceInterface::Mode mode; mode.id = 0; mode.size = pixelSize; mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; - mode.refreshRate = 60000; // TODO: get actual refresh rate of fb device? + mode.refreshRate = refreshRate; initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", physicalSize, { mode }, {}); } +void FramebufferOutput::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_renderLoop); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + FramebufferBackend::FramebufferBackend(QObject *parent) : Platform(parent) { @@ -80,6 +117,11 @@ void FramebufferBackend::init() VirtualTerminal::create(this); } +int FramebufferBackend::fileDescriptor() const +{ + return m_fd; +} + void FramebufferBackend::openFrameBuffer() { VirtualTerminal::self()->init(); diff --git a/plugins/platforms/fbdev/fb_backend.h b/plugins/platforms/fbdev/fb_backend.h index e1a45bf344..e6d586d07c 100644 --- a/plugins/platforms/fbdev/fb_backend.h +++ b/plugins/platforms/fbdev/fb_backend.h @@ -18,15 +18,27 @@ namespace KWin { +class FramebufferBackend; +class VsyncMonitor; + class FramebufferOutput : public AbstractWaylandOutput { Q_OBJECT public: - FramebufferOutput(QObject *parent = nullptr); + explicit FramebufferOutput(FramebufferBackend *backend, QObject *parent = nullptr); ~FramebufferOutput() override = default; + RenderLoop *renderLoop() const override; + VsyncMonitor *vsyncMonitor() const; + void init(const QSize &pixelSize, const QSize &physicalSize); + +private: + void vblank(std::chrono::nanoseconds timestamp); + + RenderLoop *m_renderLoop = nullptr; + VsyncMonitor *m_vsyncMonitor = nullptr; }; class KWIN_EXPORT FramebufferBackend : public Platform @@ -44,6 +56,7 @@ public: void init() override; + int fileDescriptor() const; bool isValid() const { return m_fd >= 0; } diff --git a/plugins/platforms/fbdev/fbvsyncmonitor.cpp b/plugins/platforms/fbdev/fbvsyncmonitor.cpp new file mode 100644 index 0000000000..d49617677c --- /dev/null +++ b/plugins/platforms/fbdev/fbvsyncmonitor.cpp @@ -0,0 +1,77 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fbvsyncmonitor.h" + +#include + +#include +#include +#include + +namespace KWin +{ + +FramebufferVsyncMonitorHelper::FramebufferVsyncMonitorHelper(int fileDescriptor, QObject *parent) + : QObject(parent) + , m_fileDescriptor(fileDescriptor) +{ +} + +FramebufferVsyncMonitorHelper::~FramebufferVsyncMonitorHelper() +{ + close(m_fileDescriptor); +} + +void FramebufferVsyncMonitorHelper::poll() +{ + if (ioctl(m_fileDescriptor, FBIO_WAITFORVSYNC)) { + emit errorOccurred(); + } else { + emit vblankOccurred(std::chrono::steady_clock::now().time_since_epoch()); + } +} + +FramebufferVsyncMonitor::FramebufferVsyncMonitor(int fileDescriptor, QObject *parent) + : VsyncMonitor(parent) + , m_thread(new QThread) + , m_helper(new FramebufferVsyncMonitorHelper(fileDescriptor)) +{ + m_helper->moveToThread(m_thread); + + connect(m_helper, &FramebufferVsyncMonitorHelper::errorOccurred, + this, &FramebufferVsyncMonitor::errorOccurred); + connect(m_helper, &FramebufferVsyncMonitorHelper::vblankOccurred, + this, &FramebufferVsyncMonitor::vblankOccurred); + + m_thread->setObjectName(QStringLiteral("vsync event monitor")); + m_thread->start(); +} + +FramebufferVsyncMonitor::~FramebufferVsyncMonitor() +{ + m_thread->quit(); + m_thread->wait(); + + delete m_helper; + delete m_thread; +} + +void FramebufferVsyncMonitor::arm() +{ + QMetaObject::invokeMethod(m_helper, &FramebufferVsyncMonitorHelper::poll); +} + +FramebufferVsyncMonitor *FramebufferVsyncMonitor::create(int fileDescriptor, QObject *parent) +{ + const int threadFileDescriptor = dup(fileDescriptor); + if (threadFileDescriptor == -1) { + return nullptr; + } + return new FramebufferVsyncMonitor(threadFileDescriptor, parent); +} + +} // namespace KWin diff --git a/plugins/platforms/fbdev/fbvsyncmonitor.h b/plugins/platforms/fbdev/fbvsyncmonitor.h new file mode 100644 index 0000000000..306b8771b0 --- /dev/null +++ b/plugins/platforms/fbdev/fbvsyncmonitor.h @@ -0,0 +1,63 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "vsyncmonitor.h" + +namespace KWin +{ + +/** + * The FramebufferVsyncMonitorHelper class is responsible for waiting vsync events using the + * FBIO_WAITFORVSYNC ioctl. Note that the helper runs on a separate thread. + */ +class FramebufferVsyncMonitorHelper : public QObject +{ + Q_OBJECT + +public: + explicit FramebufferVsyncMonitorHelper(int fileDescriptor, QObject *parent = nullptr); + ~FramebufferVsyncMonitorHelper() override; + +public Q_SLOTS: + void poll(); + +Q_SIGNALS: + void errorOccurred(); + void vblankOccurred(std::chrono::nanoseconds timestamp); + +private: + int m_fileDescriptor; +}; + +/** + * The FramebufferVsyncMonitor class monitors vblank events using the FBIO_WAITFORVSYNC ioctl. + */ +class FramebufferVsyncMonitor : public VsyncMonitor +{ + Q_OBJECT + +public: + /** + * Creates a fbdev vsync monitor for the device with the specified file descriptor @p fd. + * This function returns @c null if an error has occurred. + */ + static FramebufferVsyncMonitor *create(int fd, QObject *parent); + + ~FramebufferVsyncMonitor() override; + +public Q_SLOTS: + void arm() override; + +private: + explicit FramebufferVsyncMonitor(int fileDescriptor, QObject *parent = nullptr); + + QThread *m_thread; + FramebufferVsyncMonitorHelper *m_helper; +}; + +} // namespace KWin diff --git a/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp b/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp index 539f2ab33f..f40ef43695 100644 --- a/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp @@ -11,7 +11,9 @@ #include "composite.h" #include "logind.h" #include "cursor.h" +#include "renderloop.h" #include "virtual_terminal.h" +#include "vsyncmonitor.h" // Qt #include @@ -33,16 +35,30 @@ FramebufferQPainterBackend::FramebufferQPainterBackend(FramebufferBackend *backe m_backend->bytesPerLine(), m_backend->imageFormat()); m_backBuffer.fill(Qt::black); - connect(VirtualTerminal::self(), &VirtualTerminal::activeChanged, this, - [] (bool active) { - if (active) { - Compositor::self()->bufferSwapComplete(); - Compositor::self()->addRepaintFull(); - } else { - Compositor::self()->aboutToSwapBuffers(); - } + connect(VirtualTerminal::self(), &VirtualTerminal::activeChanged, this, [this](bool active) { + if (active) { + reactivate(); + } else { + deactivate(); } - ); + }); +} + +void FramebufferQPainterBackend::reactivate() +{ + const QVector outputs = m_backend->outputs(); + for (AbstractOutput *output : outputs) { + output->renderLoop()->uninhibit(); + } + Compositor::self()->addRepaintFull(); +} + +void FramebufferQPainterBackend::deactivate() +{ + const QVector outputs = m_backend->outputs(); + for (AbstractOutput *output : outputs) { + output->renderLoop()->inhibit(); + } } FramebufferQPainterBackend::~FramebufferQPainterBackend() = default; @@ -76,6 +92,9 @@ void FramebufferQPainterBackend::endFrame(int screenId, int mask, const QRegion } m_needsFullRepaint = false; + FramebufferOutput *output = static_cast(m_backend->findOutput(screenId)); + output->vsyncMonitor()->arm(); + QPainter p(&m_backBuffer); p.drawImage(QPoint(0, 0), m_backend->isBGR() ? m_renderBuffer.rgbSwapped() : m_renderBuffer); } diff --git a/plugins/platforms/fbdev/scene_qpainter_fb_backend.h b/plugins/platforms/fbdev/scene_qpainter_fb_backend.h index 79d119595f..d76ded4f78 100644 --- a/plugins/platforms/fbdev/scene_qpainter_fb_backend.h +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.h @@ -30,6 +30,9 @@ public: void endFrame(int screenId, int mask, const QRegion &damage) override; private: + void reactivate(); + void deactivate(); + /** * @brief mapped memory buffer on fb device */ diff --git a/plugins/platforms/virtual/CMakeLists.txt b/plugins/platforms/virtual/CMakeLists.txt index d13aa8caae..4feb17844c 100644 --- a/plugins/platforms/virtual/CMakeLists.txt +++ b/plugins/platforms/virtual/CMakeLists.txt @@ -6,12 +6,13 @@ set(VIRTUAL_SOURCES ) include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) + include(ECMQtDeclareLoggingCategory) ecm_qt_declare_logging_category(VIRTUAL_SOURCES HEADER logging.h IDENTIFIER KWIN_VIRTUAL CATEGORY_NAME kwin_platform_virtual DEFAULT_SEVERITY Critical) add_library(KWinWaylandVirtualBackend MODULE ${VIRTUAL_SOURCES}) set_target_properties(KWinWaylandVirtualBackend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") -target_link_libraries(KWinWaylandVirtualBackend kwin SceneQPainterBackend SceneOpenGLBackend) +target_link_libraries(KWinWaylandVirtualBackend kwin SceneQPainterBackend SceneOpenGLBackend VsyncSupport) install( TARGETS diff --git a/plugins/platforms/virtual/egl_gbm_backend.cpp b/plugins/platforms/virtual/egl_gbm_backend.cpp index a248879819..1554e1a97f 100644 --- a/plugins/platforms/virtual/egl_gbm_backend.cpp +++ b/plugins/platforms/virtual/egl_gbm_backend.cpp @@ -12,6 +12,8 @@ #include "virtual_backend.h" #include "options.h" #include "screens.h" +#include "softwarevsyncmonitor.h" +#include "virtual_output.h" #include // kwin libs #include @@ -194,10 +196,13 @@ static void convertFromGLImage(QImage &img, int w, int h) void EglGbmBackend::endFrame(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) { - Q_UNUSED(screenId) Q_UNUSED(renderedRegion) Q_UNUSED(damagedRegion) glFlush(); + + VirtualOutput *output = static_cast(m_backend->findOutput(screenId)); + output->vsyncMonitor()->arm(); + if (m_backend->saveFrames()) { QImage img = QImage(QSize(m_backBuffer->width(), m_backBuffer->height()), QImage::Format_ARGB32); glReadnPixels(0, 0, m_backBuffer->width(), m_backBuffer->height(), GL_RGBA, GL_UNSIGNED_BYTE, img.sizeInBytes(), (GLvoid*)img.bits()); @@ -206,9 +211,7 @@ void EglGbmBackend::endFrame(int screenId, const QRegion &renderedRegion, const } GLRenderTarget::popRenderTarget(); - Compositor::self()->aboutToSwapBuffers(); eglSwapBuffers(eglDisplay(), surface()); - Compositor::self()->bufferSwapComplete(); } bool EglGbmBackend::usesOverlayWindow() const diff --git a/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp b/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp index da6d7e709b..d8e32e0958 100644 --- a/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp +++ b/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp @@ -7,9 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "scene_qpainter_virtual_backend.h" -#include "virtual_backend.h" #include "cursor.h" #include "screens.h" +#include "softwarevsyncmonitor.h" +#include "virtual_backend.h" +#include "virtual_output.h" #include @@ -55,6 +57,10 @@ void VirtualQPainterBackend::endFrame(int screenId, int mask, const QRegion &dam { Q_UNUSED(mask) Q_UNUSED(damage) + + VirtualOutput *output = static_cast(m_backend->findOutput(screenId)); + output->vsyncMonitor()->arm(); + if (m_backend->saveFrames()) { m_backBuffers[screenId].save(QStringLiteral("%1/screen%2-%3.png").arg(m_backend->screenshotDirPath(), QString::number(screenId), QString::number(m_frameCounter++))); } diff --git a/plugins/platforms/virtual/virtual_output.cpp b/plugins/platforms/virtual/virtual_output.cpp index 01c2d4b2ca..6d1393885d 100644 --- a/plugins/platforms/virtual/virtual_output.cpp +++ b/plugins/platforms/virtual/virtual_output.cpp @@ -7,14 +7,21 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "virtual_output.h" +#include "renderloop_p.h" +#include "softwarevsyncmonitor.h" namespace KWin { VirtualOutput::VirtualOutput(QObject *parent) : AbstractWaylandOutput() + , m_renderLoop(new RenderLoop(this)) + , m_vsyncMonitor(SoftwareVsyncMonitor::create(this)) { Q_UNUSED(parent); + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &VirtualOutput::vblank); + static int identifier = -1; identifier++; setName("Virtual-" + QString::number(identifier)); @@ -24,13 +31,27 @@ VirtualOutput::~VirtualOutput() { } +RenderLoop *VirtualOutput::renderLoop() const +{ + return m_renderLoop; +} + +SoftwareVsyncMonitor *VirtualOutput::vsyncMonitor() const +{ + return m_vsyncMonitor; +} + void VirtualOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) { + const int refreshRate = 60000; // TODO: Make the refresh rate configurable. + m_renderLoop->setRefreshRate(refreshRate); + m_vsyncMonitor->setRefreshRate(refreshRate); + KWaylandServer::OutputDeviceInterface::Mode mode; mode.id = 0; mode.size = pixelSize; mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; - mode.refreshRate = 60000; // TODO + mode.refreshRate = refreshRate; initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", pixelSize, { mode }, {}); setGeometry(QRect(logicalPosition, pixelSize)); } @@ -41,4 +62,10 @@ void VirtualOutput::setGeometry(const QRect &geo) setGlobalPos(geo.topLeft()); } +void VirtualOutput::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_renderLoop); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + } diff --git a/plugins/platforms/virtual/virtual_output.h b/plugins/platforms/virtual/virtual_output.h index 56e8ad7e39..9767e5829c 100644 --- a/plugins/platforms/virtual/virtual_output.h +++ b/plugins/platforms/virtual/virtual_output.h @@ -16,6 +16,8 @@ namespace KWin { + +class SoftwareVsyncMonitor; class VirtualBackend; class VirtualOutput : public AbstractWaylandOutput @@ -26,6 +28,9 @@ public: VirtualOutput(QObject *parent = nullptr); ~VirtualOutput() override; + RenderLoop *renderLoop() const override; + SoftwareVsyncMonitor *vsyncMonitor() const; + void init(const QPoint &logicalPosition, const QSize &pixelSize); void setGeometry(const QRect &geo); @@ -39,9 +44,13 @@ public: } private: + void vblank(std::chrono::nanoseconds timestamp); + Q_DISABLE_COPY(VirtualOutput); friend class VirtualBackend; + RenderLoop *m_renderLoop; + SoftwareVsyncMonitor *m_vsyncMonitor; int m_gammaSize = 200; bool m_gammaResult = true; }; diff --git a/plugins/platforms/wayland/egl_wayland_backend.cpp b/plugins/platforms/wayland/egl_wayland_backend.cpp index 90e35ba253..20b67749f8 100644 --- a/plugins/platforms/wayland/egl_wayland_backend.cpp +++ b/plugins/platforms/wayland/egl_wayland_backend.cpp @@ -314,7 +314,6 @@ void EglWaylandBackend::presentOnSurface(EglWaylandOutput *output, const QRegion waylandOutput->surface()->setupFrameCallback(); waylandOutput->surface()->setScale(waylandOutput->scale()); - Compositor::self()->aboutToSwapBuffers(); Q_EMIT waylandOutput->outputChange(damage); if (supportsSwapBuffersWithDamage() && !output->m_damageHistory.isEmpty()) { diff --git a/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp b/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp index bc15d9231a..210e6edfdc 100644 --- a/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp +++ b/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp @@ -171,8 +171,6 @@ void WaylandQPainterBackend::endFrame(int screenId, int mask, const QRegion &dam WaylandQPainterOutput *rendererOutput = m_outputs.value(screenId); Q_ASSERT(rendererOutput); - Compositor::self()->aboutToSwapBuffers(); - rendererOutput->setNeedsFullRepaint(false); rendererOutput->present(rendererOutput->mapToLocal(damage)); } diff --git a/plugins/platforms/wayland/wayland_backend.cpp b/plugins/platforms/wayland/wayland_backend.cpp index e7ca409e9a..38c2a4ad0c 100644 --- a/plugins/platforms/wayland/wayland_backend.cpp +++ b/plugins/platforms/wayland/wayland_backend.cpp @@ -18,6 +18,7 @@ #endif #endif #include "logging.h" +#include "renderloop_p.h" #include "scene_qpainter_wayland_backend.h" #include "wayland_output.h" @@ -717,7 +718,15 @@ void WaylandBackend::createOutputs() updateScreenSize(waylandOutput); Compositor::self()->addRepaintFull(); }); - connect(waylandOutput, &WaylandOutput::frameRendered, this, &WaylandBackend::checkBufferSwap); + connect(waylandOutput, &WaylandOutput::frameRendered, this, [waylandOutput]() { + waylandOutput->resetRendered(); + + // The current time of the monotonic clock is a pretty good estimate when the frame + // has been presented, however it will be much better if we check whether the host + // compositor supports the wp_presentation protocol. + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(waylandOutput->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(std::chrono::steady_clock::now().time_since_epoch()); + }); logicalWidthSum += logicalWidth; m_outputs << waylandOutput; @@ -740,23 +749,6 @@ QPainterBackend *WaylandBackend::createQPainterBackend() return new WaylandQPainterBackend(this); } -void WaylandBackend::checkBufferSwap() -{ - const bool allRendered = std::all_of(m_outputs.constBegin(), m_outputs.constEnd(), [](WaylandOutput *o) { - return o->rendered(); - }); - if (!allRendered) { - // need to wait more - // TODO: what if one does not need to be rendered (no damage)? - return; - } - Compositor::self()->bufferSwapComplete(); - - for (auto *output : qAsConst(m_outputs)) { - output->resetRendered(); - } -} - void WaylandBackend::flush() { if (m_connectionThreadObject) { diff --git a/plugins/platforms/wayland/wayland_backend.h b/plugins/platforms/wayland/wayland_backend.h index b26bdc71ed..74ba17f82b 100644 --- a/plugins/platforms/wayland/wayland_backend.h +++ b/plugins/platforms/wayland/wayland_backend.h @@ -192,8 +192,6 @@ public: QVector supportedCompositors() const override; - void checkBufferSwap(); - WaylandOutput* getOutputAt(const QPointF &globalPosition); Outputs outputs() const override; Outputs enabledOutputs() const override; diff --git a/plugins/platforms/wayland/wayland_output.cpp b/plugins/platforms/wayland/wayland_output.cpp index aa76574093..5350d34a90 100644 --- a/plugins/platforms/wayland/wayland_output.cpp +++ b/plugins/platforms/wayland/wayland_output.cpp @@ -7,8 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "wayland_output.h" +#include "renderloop.h" #include "wayland_backend.h" - #include "wayland_server.h" #include @@ -27,6 +27,7 @@ using namespace KWayland::Client; WaylandOutput::WaylandOutput(Surface *surface, WaylandBackend *backend) : AbstractWaylandOutput(backend) + , m_renderLoop(new RenderLoop(this)) , m_surface(surface) , m_backend(backend) { @@ -46,13 +47,21 @@ WaylandOutput::~WaylandOutput() delete m_surface; } +RenderLoop *WaylandOutput::renderLoop() const +{ + return m_renderLoop; +} + void WaylandOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) { + const int refreshRate = 60000; // TODO: can we get refresh rate data from Wayland host? + m_renderLoop->setRefreshRate(refreshRate); + KWaylandServer::OutputDeviceInterface::Mode mode; mode.id = 0; mode.size = pixelSize; mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; - mode.refreshRate = 60000; // TODO: can we get refresh rate data from Wayland host? + mode.refreshRate = refreshRate; initInterfaces("model_TODO", "manufacturer_TODO", "UUID_TODO", pixelSize, { mode }, {}); setGeometry(logicalPosition, pixelSize); setScale(backend()->initialOutputScale()); diff --git a/plugins/platforms/wayland/wayland_output.h b/plugins/platforms/wayland/wayland_output.h index ffaa439783..9f3bfde9ec 100644 --- a/plugins/platforms/wayland/wayland_output.h +++ b/plugins/platforms/wayland/wayland_output.h @@ -42,6 +42,8 @@ public: WaylandOutput(KWayland::Client::Surface *surface, WaylandBackend *backend); ~WaylandOutput() override; + RenderLoop *renderLoop() const override; + void init(const QPoint &logicalPosition, const QSize &pixelSize); virtual void lockPointer(KWayland::Client::Pointer *pointer, bool lock) { @@ -79,6 +81,7 @@ protected: } private: + RenderLoop *m_renderLoop; KWayland::Client::Surface *m_surface; WaylandBackend *m_backend; diff --git a/plugins/platforms/x11/common/eglonxbackend.cpp b/plugins/platforms/x11/common/eglonxbackend.cpp index fd3b6b1312..14fd7ab230 100644 --- a/plugins/platforms/x11/common/eglonxbackend.cpp +++ b/plugins/platforms/x11/common/eglonxbackend.cpp @@ -99,7 +99,6 @@ void EglOnXBackend::init() } } - setSyncsToVBlank(false); if (surfaceHasSubPost) { qCDebug(KWIN_CORE) << "EGL implementation and surface support eglPostSubBufferNV, let's use it"; @@ -110,7 +109,6 @@ void EglOnXBackend::init() if (val >= 1) { if (eglSwapInterval(eglDisplay(), 1)) { qCDebug(KWIN_CORE) << "Enabled v-sync"; - setSyncsToVBlank(true); } } else { qCWarning(KWIN_CORE) << "Cannot enable v-sync as max. swap interval is" << val; diff --git a/plugins/platforms/x11/standalone/CMakeLists.txt b/plugins/platforms/x11/standalone/CMakeLists.txt index bf1486590d..bc69868890 100644 --- a/plugins/platforms/x11/standalone/CMakeLists.txt +++ b/plugins/platforms/x11/standalone/CMakeLists.txt @@ -5,8 +5,10 @@ set(X11PLATFORM_SOURCES eglbackend.cpp logging.cpp non_composited_outline.cpp + omlsynccontrolvsyncmonitor.cpp overlaywindow_x11.cpp screenedges_filter.cpp + sgivideosyncvsyncmonitor.cpp windowselector.cpp x11_decoration_renderer.cpp x11_output.cpp @@ -29,7 +31,7 @@ include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/xrender) add_library(KWinX11Platform MODULE ${X11PLATFORM_SOURCES}) set_target_properties(KWinX11Platform PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.platforms/") -target_link_libraries(KWinX11Platform eglx11common kwin kwinxrenderutils SceneOpenGLBackend Qt5::X11Extras XCB::CURSOR KF5::Crash ) +target_link_libraries(KWinX11Platform eglx11common kwin kwinxrenderutils SceneOpenGLBackend VsyncSupport Qt5::X11Extras XCB::CURSOR KF5::Crash ) if (X11_Xinput_FOUND) target_link_libraries(KWinX11Platform ${X11_Xinput_LIB}) endif() diff --git a/plugins/platforms/x11/standalone/eglbackend.cpp b/plugins/platforms/x11/standalone/eglbackend.cpp index f68f6c2de2..7596bb224d 100644 --- a/plugins/platforms/x11/standalone/eglbackend.cpp +++ b/plugins/platforms/x11/standalone/eglbackend.cpp @@ -8,15 +8,38 @@ #include "eglbackend.h" #include "options.h" #include "overlaywindow.h" +#include "platform.h" +#include "renderloop_p.h" #include "scene.h" #include "screens.h" +#include "softwarevsyncmonitor.h" +#include "x11_platform.h" namespace KWin { -EglBackend::EglBackend(Display *display) +EglBackend::EglBackend(Display *display, X11StandalonePlatform *backend) : EglOnXBackend(display) + , m_backend(backend) { + // There is no any way to determine when a buffer swap completes with EGL. Fallback + // to software vblank events. Could we use the Present extension to get notified when + // the overlay window is actually presented on the screen? + m_vsyncMonitor = SoftwareVsyncMonitor::create(this); + connect(backend->renderLoop(), &RenderLoop::refreshRateChanged, this, [this, backend]() { + m_vsyncMonitor->setRefreshRate(backend->renderLoop()->refreshRate()); + }); + m_vsyncMonitor->setRefreshRate(backend->renderLoop()->refreshRate()); + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &EglBackend::vblank); +} + +EglBackend::~EglBackend() +{ + // No completion events will be received for in-flight frames, this may lock the + // render loop. We need to ensure that the render loop is back to its initial state + // if the render backend is about to be destroyed. + RenderLoopPrivate::get(kwinApp()->platform()->renderLoop())->invalidate(); } SceneOpenGLTexturePrivate *EglBackend::createBackendTexture(SceneOpenGLTexture *texture) @@ -37,6 +60,7 @@ void EglBackend::screenGeometryChanged(const QSize &size) QRegion EglBackend::beginFrame(int screenId) { Q_UNUSED(screenId) + QRegion repaint; if (supportsBufferAge()) repaint = accumulatedDamageHistory(m_bufferAge); @@ -50,6 +74,10 @@ void EglBackend::endFrame(int screenId, const QRegion &renderedRegion, const QRe { Q_UNUSED(screenId) + // Start the software vsync monitor. There is no any reliable way to determine when + // eglSwapBuffers() or eglSwapBuffersWithDamageEXT() completes. + m_vsyncMonitor->arm(); + presentSurface(surface(), renderedRegion, screens()->geometry()); if (overlayWindow() && overlayWindow()->window()) { // show the window only after the first pass, @@ -80,6 +108,12 @@ void EglBackend::presentSurface(EGLSurface surface, const QRegion &damage, const } } +void EglBackend::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_backend->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + /************************************************ * EglTexture ************************************************/ diff --git a/plugins/platforms/x11/standalone/eglbackend.h b/plugins/platforms/x11/standalone/eglbackend.h index ae8a46e27f..5d75d16b5b 100644 --- a/plugins/platforms/x11/standalone/eglbackend.h +++ b/plugins/platforms/x11/standalone/eglbackend.h @@ -11,12 +11,16 @@ namespace KWin { +class SoftwareVsyncMonitor; +class X11StandalonePlatform; + class EglBackend : public EglOnXBackend { Q_OBJECT public: - explicit EglBackend(Display *display); + EglBackend(Display *display, X11StandalonePlatform *platform); + ~EglBackend() override; SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; QRegion beginFrame(int screenId) override; @@ -25,7 +29,10 @@ public: private: void presentSurface(EGLSurface surface, const QRegion &damage, const QRect &screenGeometry); + void vblank(std::chrono::nanoseconds timestamp); + X11StandalonePlatform *m_backend; + SoftwareVsyncMonitor *m_vsyncMonitor; int m_bufferAge = 0; }; diff --git a/plugins/platforms/x11/standalone/glxbackend.cpp b/plugins/platforms/x11/standalone/glxbackend.cpp index 400ae1f2f2..85a0c0a9e8 100644 --- a/plugins/platforms/x11/standalone/glxbackend.cpp +++ b/plugins/platforms/x11/standalone/glxbackend.cpp @@ -15,11 +15,16 @@ #include "glxbackend.h" #include "logging.h" #include "glx_context_attribute_builder.h" +#include "omlsynccontrolvsyncmonitor.h" +#include "sgivideosyncvsyncmonitor.h" +#include "softwarevsyncmonitor.h" +#include "x11_platform.h" // kwin #include "options.h" #include "overlaywindow.h" #include "composite.h" #include "platform.h" +#include "renderloop_p.h" #include "scene.h" #include "screens.h" #include "xcbutils.h" @@ -73,26 +78,23 @@ SwapEventFilter::SwapEventFilter(xcb_drawable_t drawable, xcb_glx_drawable_t glx bool SwapEventFilter::event(xcb_generic_event_t *event) { - xcb_glx_buffer_swap_complete_event_t *ev = + const xcb_glx_buffer_swap_complete_event_t *swapEvent = reinterpret_cast(event); - - // The drawable field is the X drawable when the event was synthesized - // by a WireToEvent handler, and the GLX drawable when the event was - // received over the wire - if (ev->drawable == m_drawable || ev->drawable == m_glxDrawable) { - Compositor::self()->bufferSwapComplete(); - return true; + if (swapEvent->drawable != m_drawable && swapEvent->drawable != m_glxDrawable) { + return false; } - return false; + // The clock for the UST timestamp is left unspecified in the spec, however, usually, + // it's CLOCK_MONOTONIC, so no special conversions are needed. + const std::chrono::microseconds timestamp((uint64_t(swapEvent->ust_hi) << 32) | swapEvent->ust_lo); + + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(kwinApp()->platform()->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(timestamp); + + return true; } - -// ----------------------------------------------------------------------- - - - -GlxBackend::GlxBackend(Display *display) +GlxBackend::GlxBackend(Display *display, X11StandalonePlatform *backend) : OpenGLBackend() , m_overlayWindow(kwinApp()->platform()->createOverlayWindow()) , window(None) @@ -101,6 +103,7 @@ GlxBackend::GlxBackend(Display *display) , ctx(nullptr) , m_bufferAge(0) , m_x11Display(display) + , m_backend(backend) { // Force initialization of GLX integration in the Qt's xcb backend // to make it call XESetWireToEvent callbacks, which is required @@ -110,6 +113,13 @@ GlxBackend::GlxBackend(Display *display) GlxBackend::~GlxBackend() { + delete m_vsyncMonitor; + + // No completion events will be received for in-flight frames, this may lock the + // render loop. We need to ensure that the render loop is back to its initial state + // if the render backend is about to be destroyed. + RenderLoopPrivate::get(kwinApp()->platform()->renderLoop())->invalidate(); + if (isFailed()) { m_overlayWindow->destroy(); } @@ -194,14 +204,8 @@ void GlxBackend::init() m_haveMESASwapControl = hasExtension(QByteArrayLiteral("GLX_MESA_swap_control")); m_haveEXTSwapControl = hasExtension(QByteArrayLiteral("GLX_EXT_swap_control")); m_haveSGISwapControl = hasExtension(QByteArrayLiteral("GLX_SGI_swap_control")); - // only enable Intel swap event if env variable is set, see BUG 342582 m_haveINTELSwapEvent = hasExtension(QByteArrayLiteral("GLX_INTEL_swap_event")) - && qgetenv("KWIN_USE_INTEL_SWAP_EVENT") == QByteArrayLiteral("1"); - - if (m_haveINTELSwapEvent) { - m_swapEventFilter = std::make_unique(window, glxWindow); - glXSelectEvent(display(), glxWindow, GLX_BUFFER_SWAP_COMPLETE_INTEL_MASK); - } + && qgetenv("KWIN_USE_INTEL_SWAP_EVENT") != QByteArrayLiteral("0"); bool haveSwapInterval = m_haveMESASwapControl || m_haveEXTSwapControl || m_haveSGISwapControl; @@ -214,12 +218,16 @@ void GlxBackend::init() setSupportsBufferAge(true); } - setSyncsToVBlank(false); + // If the buffer age extension is unsupported, glXSwapBuffers() is not guaranteed to + // be called. Therefore, there is no point for creating the swap event filter. + if (!supportsBufferAge()) { + m_haveINTELSwapEvent = false; + } + const bool wantSync = options->glPreferBufferSwap() != Options::NoSwapEncourage; if (wantSync && glXIsDirect(display(), ctx)) { if (haveSwapInterval) { // glXSwapInterval is preferred being more reliable setSwapInterval(1); - setSyncsToVBlank(true); } else { qCWarning(KWIN_X11STANDALONE) << "glSwapInterval is unsupported"; } @@ -234,6 +242,36 @@ void GlxBackend::init() glXQueryDrawable = nullptr; } + if (m_haveINTELSwapEvent) { + // Nice, the GLX_INTEL_swap_event extension is available. We are going to receive + // the presentation timestamp (UST) after glXSwapBuffers() via the X command stream. + m_swapEventFilter = std::make_unique(window, glxWindow); + glXSelectEvent(display(), glxWindow, GLX_BUFFER_SWAP_COMPLETE_INTEL_MASK); + } else { + // If the GLX_INTEL_swap_event extension is unavailble, we are going to wait for + // the next vblank event after swapping buffers. This is a bit racy solution, e.g. + // the vblank may occur right in between querying video sync counter and the act + // of swapping buffers, but on the other hand, there is no any better alternative + // option. NVIDIA doesn't provide any extension such as GLX_INTEL_swap_event. + if (!m_vsyncMonitor) { + m_vsyncMonitor = SGIVideoSyncVsyncMonitor::create(this); + } + if (!m_vsyncMonitor) { + m_vsyncMonitor = OMLSyncControlVsyncMonitor::create(this); + } + if (!m_vsyncMonitor) { + SoftwareVsyncMonitor *monitor = SoftwareVsyncMonitor::create(this); + RenderLoop *renderLoop = m_backend->renderLoop(); + monitor->setRefreshRate(renderLoop->refreshRate()); + connect(renderLoop, &RenderLoop::refreshRateChanged, this, [this, monitor]() { + monitor->setRefreshRate(m_backend->renderLoop()->refreshRate()); + }); + m_vsyncMonitor = monitor; + } + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &GlxBackend::vblank); + } + setIsDirectRendering(bool(glXIsDirect(display(), ctx))); qCDebug(KWIN_X11STANDALONE) << "Direct rendering:" << isDirectRendering(); @@ -670,9 +708,6 @@ void GlxBackend::present(const QRegion &damage) const bool fullRepaint = supportsBufferAge() || (damage == displayRegion); if (fullRepaint) { - if (m_haveINTELSwapEvent) - Compositor::self()->aboutToSwapBuffers(); - glXSwapBuffers(display(), glxWindow); if (supportsBufferAge()) { glXQueryDrawable(display(), glxWindow, GLX_BACK_BUFFER_AGE_EXT, (GLuint *) &m_bufferAge); @@ -718,6 +753,7 @@ SceneOpenGLTexturePrivate *GlxBackend::createBackendTexture(SceneOpenGLTexture * QRegion GlxBackend::beginFrame(int screenId) { Q_UNUSED(screenId) + QRegion repaint; makeCurrent(); @@ -733,6 +769,12 @@ void GlxBackend::endFrame(int screenId, const QRegion &renderedRegion, const QRe { Q_UNUSED(screenId) + // If the GLX_INTEL_swap_event extension is not used for getting presentation feedback, + // assume that the frame will be presented at the next vblank event, this is racy. + if (m_vsyncMonitor) { + m_vsyncMonitor->arm(); + } + present(renderedRegion); if (overlayWindow()->window()) // show the window only after the first pass, @@ -743,6 +785,12 @@ void GlxBackend::endFrame(int screenId, const QRegion &renderedRegion, const QRe addToDamageHistory(damagedRegion); } +void GlxBackend::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_backend->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + bool GlxBackend::makeCurrent() { if (QOpenGLContext *context = QOpenGLContext::currentContext()) { diff --git a/plugins/platforms/x11/standalone/glxbackend.h b/plugins/platforms/x11/standalone/glxbackend.h index 26811c8195..ca77354293 100644 --- a/plugins/platforms/x11/standalone/glxbackend.h +++ b/plugins/platforms/x11/standalone/glxbackend.h @@ -15,11 +15,13 @@ #include #include #include -#include namespace KWin { +class VsyncMonitor; +class X11StandalonePlatform; + // GLX_MESA_swap_interval using glXSwapIntervalMESA_func = int (*)(unsigned int interval); extern glXSwapIntervalMESA_func glXSwapIntervalMESA; @@ -53,10 +55,12 @@ private: /** * @brief OpenGL Backend using GLX over an X overlay window. */ -class GlxBackend : public OpenGLBackend +class GlxBackend : public QObject, public OpenGLBackend { + Q_OBJECT + public: - GlxBackend(Display *display); + GlxBackend(Display *display, X11StandalonePlatform *backend); ~GlxBackend() override; void screenGeometryChanged(const QSize &size) override; SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; @@ -69,6 +73,7 @@ public: void init() override; private: + void vblank(std::chrono::nanoseconds timestamp); void present(const QRegion &damage); bool initBuffer(); bool checkVersion(); @@ -102,6 +107,8 @@ private: bool m_haveSGISwapControl = false; bool m_haveINTELSwapEvent = false; Display *m_x11Display; + X11StandalonePlatform *m_backend; + VsyncMonitor *m_vsyncMonitor = nullptr; friend class GlxTexture; }; diff --git a/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.cpp b/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.cpp new file mode 100644 index 0000000000..02af539e03 --- /dev/null +++ b/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.cpp @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "omlsynccontrolvsyncmonitor.h" +#include "logging.h" + +#include + +namespace KWin +{ + +OMLSyncControlVsyncMonitor *OMLSyncControlVsyncMonitor::create(QObject *parent) +{ + const char *extensions = glXQueryExtensionsString(QX11Info::display(), + QX11Info::appScreen()); + if (!strstr(extensions, "GLX_OML_sync_control")) { + return nullptr; // GLX_OML_sync_control is unsupported. + } + + OMLSyncControlVsyncMonitor *monitor = new OMLSyncControlVsyncMonitor(parent); + if (monitor->isValid()) { + return monitor; + } + delete monitor; + return nullptr; +} + +OMLSyncControlVsyncMonitorHelper::OMLSyncControlVsyncMonitorHelper(QObject *parent) + : QObject(parent) +{ + // Establish a new X11 connection to avoid locking up the main X11 connection. + m_display = XOpenDisplay(DisplayString(QX11Info::display())); + if (!m_display) { + qCDebug(KWIN_X11STANDALONE) << "Failed to establish vsync monitor X11 connection"; + return; + } + + const int attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + 0 + }; + + int count; + GLXFBConfig *configs = glXChooseFBConfig(m_display, DefaultScreen(m_display), + attribs, &count); + if (!count) { + qCDebug(KWIN_X11STANDALONE) << "Couldn't find any suitable FBConfig for vsync monitor"; + return; + } + + GLXFBConfig config = configs[0]; + XFree(configs); + + m_localContext = glXCreateNewContext(m_display, config, GLX_RGBA_TYPE, 0, true); + if (!m_localContext) { + qCDebug(KWIN_X11STANDALONE) << "Failed to create opengl context for vsync monitor"; + return; + } + + m_drawable = DefaultRootWindow(m_display); +} + +OMLSyncControlVsyncMonitorHelper::~OMLSyncControlVsyncMonitorHelper() +{ + if (m_localContext) { + glXDestroyContext(m_display, m_localContext); + } + if (m_display) { + XCloseDisplay(m_display); + } +} + +bool OMLSyncControlVsyncMonitorHelper::isValid() const +{ + return m_display && m_localContext && m_drawable; +} + +void OMLSyncControlVsyncMonitorHelper::poll() +{ + if (!glXMakeCurrent(m_display, m_drawable, m_localContext)) { + qCDebug(KWIN_X11STANDALONE) << "Failed to make vsync monitor OpenGL context current"; + return; + } + + int64_t ust, msc, sbc; + + glXGetSyncValuesOML(m_display, m_drawable, &ust, &msc, &sbc); + glXWaitForMscOML(m_display, m_drawable, 0, 2, (msc + 1) % 2, &ust, &msc, &sbc); + + emit vblankOccurred(std::chrono::microseconds(ust)); +} + +OMLSyncControlVsyncMonitor::OMLSyncControlVsyncMonitor(QObject *parent) + : VsyncMonitor(parent) + , m_thread(new QThread) + , m_helper(new OMLSyncControlVsyncMonitorHelper) +{ + m_helper->moveToThread(m_thread); + + connect(m_helper, &OMLSyncControlVsyncMonitorHelper::errorOccurred, + this, &OMLSyncControlVsyncMonitor::errorOccurred); + connect(m_helper, &OMLSyncControlVsyncMonitorHelper::vblankOccurred, + this, &OMLSyncControlVsyncMonitor::vblankOccurred); + + m_thread->setObjectName(QStringLiteral("vsync event monitor")); + m_thread->start(); +} + +OMLSyncControlVsyncMonitor::~OMLSyncControlVsyncMonitor() +{ + m_thread->quit(); + m_thread->wait(); + + delete m_helper; + delete m_thread; +} + +bool OMLSyncControlVsyncMonitor::isValid() const +{ + return m_helper->isValid(); +} + +void OMLSyncControlVsyncMonitor::arm() +{ + QMetaObject::invokeMethod(m_helper, &OMLSyncControlVsyncMonitorHelper::poll); +} + +} // namespace KWin diff --git a/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.h b/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.h new file mode 100644 index 0000000000..fda11a72d8 --- /dev/null +++ b/plugins/platforms/x11/standalone/omlsynccontrolvsyncmonitor.h @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "vsyncmonitor.h" + +#include +#include +#include "fixx11h.h" + +#include + +namespace KWin +{ + +/** + * The OMLSyncControlVsyncMonitorHelper class is responsible for waiting vsync events on the + * root window. Note that the helper runs on a separate thread. + */ +class OMLSyncControlVsyncMonitorHelper : public QObject +{ + Q_OBJECT + +public: + explicit OMLSyncControlVsyncMonitorHelper(QObject *parent = nullptr); + ~OMLSyncControlVsyncMonitorHelper() override; + + bool isValid() const; + +public Q_SLOTS: + void poll(); + +Q_SIGNALS: + void errorOccurred(); + void vblankOccurred(std::chrono::nanoseconds timestamp); + +private: + Display *m_display = nullptr; + GLXContext m_localContext = 0; + GLXDrawable m_drawable = 0; +}; + +/** + * The OMLSyncControlVsyncMonitor class monitors vblank events using the GLX_OML_sync_control + * extension. + * + * Vblank events are monitored in a separated thread to avoid blocking the main thread. In + * order to avoid locking up the main X11 connection, the worker thread establishes its own + * X11 connection. + */ +class OMLSyncControlVsyncMonitor : public VsyncMonitor +{ + Q_OBJECT + +public: + static OMLSyncControlVsyncMonitor *create(QObject *parent); + ~OMLSyncControlVsyncMonitor() override; + + bool isValid() const; + +public Q_SLOTS: + void arm() override; + +private: + explicit OMLSyncControlVsyncMonitor(QObject *parent = nullptr); + + QThread *m_thread = nullptr; + OMLSyncControlVsyncMonitorHelper *m_helper = nullptr; +}; + +} // namespace KWin diff --git a/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.cpp b/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.cpp new file mode 100644 index 0000000000..13ddc190c0 --- /dev/null +++ b/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.cpp @@ -0,0 +1,134 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "sgivideosyncvsyncmonitor.h" +#include "logging.h" + +#include + +namespace KWin +{ + +SGIVideoSyncVsyncMonitor *SGIVideoSyncVsyncMonitor::create(QObject *parent) +{ + const char *extensions = glXQueryExtensionsString(QX11Info::display(), + QX11Info::appScreen()); + if (!strstr(extensions, "GLX_SGI_video_sync")) { + return nullptr; // GLX_SGI_video_sync is unsupported. + } + + SGIVideoSyncVsyncMonitor *monitor = new SGIVideoSyncVsyncMonitor(parent); + if (monitor->isValid()) { + return monitor; + } + delete monitor; + return nullptr; +} + +SGIVideoSyncVsyncMonitorHelper::SGIVideoSyncVsyncMonitorHelper(QObject *parent) + : QObject(parent) +{ + // Establish a new X11 connection to avoid locking up the main X11 connection. + m_display = XOpenDisplay(DisplayString(QX11Info::display())); + if (!m_display) { + qCDebug(KWIN_X11STANDALONE) << "Failed to establish vsync monitor X11 connection"; + return; + } + + const int attribs[] = { + GLX_RENDER_TYPE, GLX_RGBA_BIT, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + 0 + }; + + int count; + GLXFBConfig *configs = glXChooseFBConfig(m_display, DefaultScreen(m_display), + attribs, &count); + if (!count) { + qCDebug(KWIN_X11STANDALONE) << "Couldn't find any suitable FBConfig for vsync monitor"; + return; + } + + GLXFBConfig config = configs[0]; + XFree(configs); + + m_localContext = glXCreateNewContext(m_display, config, GLX_RGBA_TYPE, 0, true); + if (!m_localContext) { + qCDebug(KWIN_X11STANDALONE) << "Failed to create opengl context for vsync monitor"; + return; + } + + m_drawable = DefaultRootWindow(m_display); +} + +SGIVideoSyncVsyncMonitorHelper::~SGIVideoSyncVsyncMonitorHelper() +{ + if (m_localContext) { + glXDestroyContext(m_display, m_localContext); + } + if (m_display) { + XCloseDisplay(m_display); + } +} + +bool SGIVideoSyncVsyncMonitorHelper::isValid() const +{ + return m_display && m_localContext && m_drawable; +} + +void SGIVideoSyncVsyncMonitorHelper::poll() +{ + if (!glXMakeCurrent(m_display, m_drawable, m_localContext)) { + qCDebug(KWIN_X11STANDALONE) << "Failed to make vsync monitor OpenGL context current"; + emit errorOccurred(); + return; + } + + uint count; + + glXGetVideoSyncSGI(&count); + glXWaitVideoSyncSGI(2, (count + 1) % 2, &count); + + // Using monotonic clock is inaccurate, but it's still a pretty good estimate. + emit vblankOccurred(std::chrono::steady_clock::now().time_since_epoch()); +} + +SGIVideoSyncVsyncMonitor::SGIVideoSyncVsyncMonitor(QObject *parent) + : VsyncMonitor(parent) + , m_thread(new QThread) + , m_helper(new SGIVideoSyncVsyncMonitorHelper) +{ + m_helper->moveToThread(m_thread); + + connect(m_helper, &SGIVideoSyncVsyncMonitorHelper::errorOccurred, + this, &SGIVideoSyncVsyncMonitor::errorOccurred); + connect(m_helper, &SGIVideoSyncVsyncMonitorHelper::vblankOccurred, + this, &SGIVideoSyncVsyncMonitor::vblankOccurred); + + m_thread->setObjectName(QStringLiteral("vsync event monitor")); + m_thread->start(); +} + +SGIVideoSyncVsyncMonitor::~SGIVideoSyncVsyncMonitor() +{ + m_thread->quit(); + m_thread->wait(); + + delete m_helper; + delete m_thread; +} + +bool SGIVideoSyncVsyncMonitor::isValid() const +{ + return m_helper->isValid(); +} + +void SGIVideoSyncVsyncMonitor::arm() +{ + QMetaObject::invokeMethod(m_helper, &SGIVideoSyncVsyncMonitorHelper::poll); +} + +} // namespace KWin diff --git a/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.h b/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.h new file mode 100644 index 0000000000..3698ea6622 --- /dev/null +++ b/plugins/platforms/x11/standalone/sgivideosyncvsyncmonitor.h @@ -0,0 +1,75 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "vsyncmonitor.h" + +#include +#include +#include "fixx11h.h" + +#include + +namespace KWin +{ + +/** + * The SGIVideoSyncVsyncMonitorHelper class is responsible for waiting for vsync events on the + * root window. Note that the helper runs on a separate thread. + */ +class SGIVideoSyncVsyncMonitorHelper : public QObject +{ + Q_OBJECT + +public: + explicit SGIVideoSyncVsyncMonitorHelper(QObject *parent = nullptr); + ~SGIVideoSyncVsyncMonitorHelper() override; + + bool isValid() const; + +public Q_SLOTS: + void poll(); + +Q_SIGNALS: + void errorOccurred(); + void vblankOccurred(std::chrono::nanoseconds timestamp); + +private: + Display *m_display = nullptr; + GLXContext m_localContext = 0; + GLXDrawable m_drawable = 0; +}; + +/** + * The SGIVideoSyncVsyncMonitor class monitors vblank events using the GLX_SGI_video_sync + * extension. + * + * Vblank events are monitored in a separated thread to avoid blocking the main thread. In + * order to avoid locking up the main X11 connection, the worker thread establishes its own + * X11 connection. + */ +class SGIVideoSyncVsyncMonitor : public VsyncMonitor +{ + Q_OBJECT + +public: + static SGIVideoSyncVsyncMonitor *create(QObject *parent); + ~SGIVideoSyncVsyncMonitor() override; + + bool isValid() const; + +public Q_SLOTS: + void arm() override; + +private: + explicit SGIVideoSyncVsyncMonitor(QObject *parent = nullptr); + + QThread *m_thread = nullptr; + SGIVideoSyncVsyncMonitorHelper *m_helper = nullptr; +}; + +} // namespace KWin diff --git a/plugins/platforms/x11/standalone/x11_platform.cpp b/plugins/platforms/x11/standalone/x11_platform.cpp index e49f31e511..f880d7c5af 100644 --- a/plugins/platforms/x11/standalone/x11_platform.cpp +++ b/plugins/platforms/x11/standalone/x11_platform.cpp @@ -23,7 +23,6 @@ #include "xinputintegration.h" #endif #include "abstract_client.h" -#include "composite.h" #include "effects_x11.h" #include "eglbackend.h" #include "keyboard_input.h" @@ -36,6 +35,7 @@ #include "x11_decoration_renderer.h" #include "x11_output.h" #include "xcbutils.h" +#include "renderloop.h" #include @@ -95,6 +95,7 @@ X11StandalonePlatform::X11StandalonePlatform(QObject *parent) : Platform(parent) , m_updateOutputsTimer(new QTimer(this)) , m_x11Display(QX11Info::display()) + , m_renderLoop(new RenderLoop(this)) { #if HAVE_X11_XINPUT if (!qEnvironmentVariableIsSet("KWIN_NO_XI2")) { @@ -152,7 +153,7 @@ OpenGLBackend *X11StandalonePlatform::createOpenGLBackend() #if HAVE_EPOXY_GLX case GlxPlatformInterface: if (hasGlx()) { - return new GlxBackend(m_x11Display); + return new GlxBackend(m_x11Display, this); } else { qCWarning(KWIN_X11STANDALONE) << "Glx not available, trying EGL instead."; // no break, needs fall-through @@ -160,7 +161,7 @@ OpenGLBackend *X11StandalonePlatform::createOpenGLBackend() } #endif case EglPlatformInterface: - return new EglBackend(m_x11Display); + return new EglBackend(m_x11Display, this); default: // no backend available return nullptr; @@ -612,18 +613,50 @@ Outputs X11StandalonePlatform::enabledOutputs() const return m_outputs; } +RenderLoop *X11StandalonePlatform::renderLoop() const +{ + return m_renderLoop; +} + +static bool refreshRate_compare(const AbstractOutput *first, const AbstractOutput *smallest) +{ + return first->refreshRate() < smallest->refreshRate(); +} + +static int currentRefreshRate() +{ + const int refreshRate = qEnvironmentVariableIntValue("KWIN_X11_REFRESH_RATE"); + if (refreshRate) { + return refreshRate; + } + + const QVector outputs = kwinApp()->platform()->enabledOutputs(); + if (outputs.isEmpty()) { + return 60000; + } + + const QString syncDisplayDevice = qEnvironmentVariable("__GL_SYNC_DISPLAY_DEVICE"); + if (!syncDisplayDevice.isEmpty()) { + for (const AbstractOutput *output : outputs) { + if (output->name() == syncDisplayDevice) { + return output->refreshRate(); + } + } + } + + auto syncIt = std::min_element(outputs.begin(), outputs.end(), refreshRate_compare); + return (*syncIt)->refreshRate(); +} + void X11StandalonePlatform::updateRefreshRate() { - if (!workspace() || !workspace()->compositing()) { - return; + int refreshRate = currentRefreshRate(); + if (refreshRate <= 0) { + qCWarning(KWIN_X11STANDALONE) << "Bogus refresh rate" << refreshRate; + refreshRate = 60000; } - if (Compositor::self()->refreshRate() == Options::currentRefreshRate()) { - return; - } - // desktopResized() should take care of when the size or - // shape of the desktop has changed, but we also want to - // catch refresh rate changes - Compositor::self()->reinitialize(); + + m_renderLoop->setRefreshRate(refreshRate); } } diff --git a/plugins/platforms/x11/standalone/x11_platform.h b/plugins/platforms/x11/standalone/x11_platform.h index 26803cd9f5..37f1c59a3b 100644 --- a/plugins/platforms/x11/standalone/x11_platform.h +++ b/plugins/platforms/x11/standalone/x11_platform.h @@ -67,6 +67,7 @@ public: void scheduleUpdateOutputs(); void updateOutputs(); + RenderLoop *renderLoop() const override; Outputs outputs() const override; Outputs enabledOutputs() const override; @@ -98,6 +99,7 @@ private: QScopedPointer m_windowSelector; QScopedPointer m_screenEdgesFilter; QScopedPointer m_randrEventFilter; + RenderLoop *m_renderLoop; QVector m_outputs; }; diff --git a/plugins/platforms/x11/standalone/x11xrenderbackend.cpp b/plugins/platforms/x11/standalone/x11xrenderbackend.cpp index a4f40e60d9..0967a8e8a9 100644 --- a/plugins/platforms/x11/standalone/x11xrenderbackend.cpp +++ b/plugins/platforms/x11/standalone/x11xrenderbackend.cpp @@ -13,8 +13,10 @@ #include "main.h" #include "platform.h" #include "overlaywindow.h" +#include "renderloop_p.h" #include "scene.h" #include "screens.h" +#include "softwarevsyncmonitor.h" #include "utils.h" #include "x11_platform.h" @@ -30,11 +32,26 @@ X11XRenderBackend::X11XRenderBackend(X11StandalonePlatform *backend) , m_front(XCB_RENDER_PICTURE_NONE) , m_format(0) { + // Fallback to software vblank events for now. Maybe use the Present extension or + // something to get notified when the overlay window is actually presented? + m_vsyncMonitor = SoftwareVsyncMonitor::create(this); + connect(backend->renderLoop(), &RenderLoop::refreshRateChanged, this, [this, backend]() { + m_vsyncMonitor->setRefreshRate(backend->renderLoop()->refreshRate()); + }); + m_vsyncMonitor->setRefreshRate(backend->renderLoop()->refreshRate()); + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &X11XRenderBackend::vblank); + init(true); } X11XRenderBackend::~X11XRenderBackend() { + // No completion events will be received for in-flight frames, this may lock the + // render loop. We need to ensure that the render loop is back to its initial state + // if the render backend is about to be destroyed. + RenderLoopPrivate::get(kwinApp()->platform()->renderLoop())->invalidate(); + if (m_front) { xcb_render_free_picture(connection(), m_front); } @@ -100,6 +117,8 @@ void X11XRenderBackend::createBuffer() void X11XRenderBackend::present(int mask, const QRegion &damage) { + m_vsyncMonitor->arm(); + const auto displaySize = screens()->displaySize(); if (mask & Scene::PAINT_SCREEN_REGION) { // Use the damage region as the clip region for the root window @@ -119,6 +138,12 @@ void X11XRenderBackend::present(int mask, const QRegion &damage) xcb_flush(connection()); } +void X11XRenderBackend::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_backend->renderLoop()); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + void X11XRenderBackend::screenGeometryChanged(const QSize &size) { Q_UNUSED(size) diff --git a/plugins/platforms/x11/standalone/x11xrenderbackend.h b/plugins/platforms/x11/standalone/x11xrenderbackend.h index c159548211..210cf4588f 100644 --- a/plugins/platforms/x11/standalone/x11xrenderbackend.h +++ b/plugins/platforms/x11/standalone/x11xrenderbackend.h @@ -14,13 +14,16 @@ namespace KWin { +class SoftwareVsyncMonitor; class X11StandalonePlatform; /** * @brief XRenderBackend using an X11 Overlay Window as compositing target. */ -class X11XRenderBackend : public XRenderBackend +class X11XRenderBackend : public QObject, public XRenderBackend { + Q_OBJECT + public: explicit X11XRenderBackend(X11StandalonePlatform *backend); ~X11XRenderBackend() override; @@ -34,7 +37,10 @@ public: private: void init(bool createOverlay); void createBuffer(); + void vblank(std::chrono::nanoseconds timestamp); + X11StandalonePlatform *m_backend; + SoftwareVsyncMonitor *m_vsyncMonitor; QScopedPointer m_overlayWindow; xcb_render_picture_t m_front; xcb_render_pictformat_t m_format; diff --git a/plugins/platforms/x11/windowed/CMakeLists.txt b/plugins/platforms/x11/windowed/CMakeLists.txt index 815c0ee1cb..b4908d686d 100644 --- a/plugins/platforms/x11/windowed/CMakeLists.txt +++ b/plugins/platforms/x11/windowed/CMakeLists.txt @@ -7,9 +7,11 @@ set(X11BACKEND_SOURCES ) include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/vsyncconvenience) + add_library(KWinWaylandX11Backend MODULE ${X11BACKEND_SOURCES}) set_target_properties(KWinWaylandX11Backend PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/org.kde.kwin.waylandbackends/") -target_link_libraries(KWinWaylandX11Backend eglx11common kwin kwinxrenderutils X11::XCB SceneQPainterBackend SceneOpenGLBackend) +target_link_libraries(KWinWaylandX11Backend eglx11common kwin kwinxrenderutils X11::XCB SceneQPainterBackend SceneOpenGLBackend VsyncSupport) if (X11_Xinput_FOUND) target_link_libraries(KWinWaylandX11Backend ${X11_Xinput_LIB}) endif() diff --git a/plugins/platforms/x11/windowed/egl_x11_backend.cpp b/plugins/platforms/x11/windowed/egl_x11_backend.cpp index 875a2f8650..cf5fdb203b 100644 --- a/plugins/platforms/x11/windowed/egl_x11_backend.cpp +++ b/plugins/platforms/x11/windowed/egl_x11_backend.cpp @@ -8,8 +8,11 @@ */ #include "egl_x11_backend.h" // kwin +#include "main.h" #include "screens.h" +#include "softwarevsyncmonitor.h" #include "x11windowed_backend.h" +#include "x11windowed_output.h" // kwin libs #include @@ -82,6 +85,10 @@ void EglX11Backend::setupViewport(int screenId) void EglX11Backend::endFrame(int screenId, const QRegion &renderedRegion, const QRegion &damagedRegion) { Q_UNUSED(damagedRegion) + + X11WindowedOutput *output = static_cast(kwinApp()->platform()->findOutput(screenId)); + output->vsyncMonitor()->arm(); + const QRect &outputGeometry = screens()->geometry(screenId); presentSurface(m_surfaces.at(screenId), renderedRegion, outputGeometry); } diff --git a/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp index 3a91e9d590..1b8a12d7e0 100644 --- a/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp +++ b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp @@ -7,8 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "scene_qpainter_x11_backend.h" -#include "x11windowed_backend.h" +#include "main.h" #include "screens.h" +#include "softwarevsyncmonitor.h" +#include "x11windowed_backend.h" +#include "x11windowed_output.h" namespace KWin { @@ -62,6 +65,10 @@ void X11WindowedQPainterBackend::endFrame(int screenId, int mask, const QRegion { Q_UNUSED(mask) Q_UNUSED(damage) + + X11WindowedOutput *output = static_cast(kwinApp()->platform()->findOutput(screenId)); + output->vsyncMonitor()->arm(); + xcb_connection_t *c = m_backend->connection(); const xcb_window_t window = m_backend->window(); if (m_gc == XCB_NONE) { diff --git a/plugins/platforms/x11/windowed/x11windowed_output.cpp b/plugins/platforms/x11/windowed/x11windowed_output.cpp index 38a1e70937..e858ba8ac9 100644 --- a/plugins/platforms/x11/windowed/x11windowed_output.cpp +++ b/plugins/platforms/x11/windowed/x11windowed_output.cpp @@ -7,7 +7,8 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "x11windowed_output.h" - +#include "renderloop_p.h" +#include "softwarevsyncmonitor.h" #include "x11windowed_backend.h" #include @@ -23,6 +24,8 @@ namespace KWin X11WindowedOutput::X11WindowedOutput(X11WindowedBackend *backend) : AbstractWaylandOutput(backend) + , m_renderLoop(new RenderLoop(this)) + , m_vsyncMonitor(SoftwareVsyncMonitor::create(this)) , m_backend(backend) { m_window = xcb_generate_id(m_backend->connection()); @@ -30,6 +33,8 @@ X11WindowedOutput::X11WindowedOutput(X11WindowedBackend *backend) static int identifier = -1; identifier++; setName("X11-" + QString::number(identifier)); + + connect(m_vsyncMonitor, &VsyncMonitor::vblankOccurred, this, &X11WindowedOutput::vblank); } X11WindowedOutput::~X11WindowedOutput() @@ -40,13 +45,27 @@ X11WindowedOutput::~X11WindowedOutput() xcb_flush(m_backend->connection()); } +RenderLoop *X11WindowedOutput::renderLoop() const +{ + return m_renderLoop; +} + +SoftwareVsyncMonitor *X11WindowedOutput::vsyncMonitor() const +{ + return m_vsyncMonitor; +} + void X11WindowedOutput::init(const QPoint &logicalPosition, const QSize &pixelSize) { + const int refreshRate = 60000; // TODO: get refresh rate via randr + m_renderLoop->setRefreshRate(refreshRate); + m_vsyncMonitor->setRefreshRate(refreshRate); + KWaylandServer::OutputDeviceInterface::Mode mode; mode.id = 0; mode.size = pixelSize; mode.flags = KWaylandServer::OutputDeviceInterface::ModeFlag::Current; - mode.refreshRate = 60000; // TODO: get refresh rate via randr + mode.refreshRate = refreshRate; // Physicial size must be adjusted, such that QPA calculates correct sizes of // internal elements. @@ -154,4 +173,10 @@ QPointF X11WindowedOutput::mapFromGlobal(const QPointF &pos) const return (pos - hostPosition() + internalPosition()) / scale(); } +void X11WindowedOutput::vblank(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(m_renderLoop); + renderLoopPrivate->notifyFrameCompleted(timestamp); +} + } diff --git a/plugins/platforms/x11/windowed/x11windowed_output.h b/plugins/platforms/x11/windowed/x11windowed_output.h index 7e9c5155b4..bfd24da964 100644 --- a/plugins/platforms/x11/windowed/x11windowed_output.h +++ b/plugins/platforms/x11/windowed/x11windowed_output.h @@ -22,6 +22,8 @@ class NETWinInfo; namespace KWin { + +class SoftwareVsyncMonitor; class X11WindowedBackend; /** @@ -34,6 +36,9 @@ public: explicit X11WindowedOutput(X11WindowedBackend *backend); ~X11WindowedOutput() override; + RenderLoop *renderLoop() const override; + SoftwareVsyncMonitor *vsyncMonitor() const; + void init(const QPoint &logicalPosition, const QSize &pixelSize); xcb_window_t window() const { @@ -62,10 +67,12 @@ public: private: void initXInputForWindow(); + void vblank(std::chrono::nanoseconds timestamp); xcb_window_t m_window = XCB_WINDOW_NONE; NETWinInfo *m_winInfo = nullptr; - + RenderLoop *m_renderLoop; + SoftwareVsyncMonitor *m_vsyncMonitor; QPoint m_hostPosition; X11WindowedBackend *m_backend; diff --git a/plugins/scenes/opengl/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp index 5b3cfcdc63..a9526e9838 100644 --- a/plugins/scenes/opengl/scene_opengl.cpp +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -75,9 +75,6 @@ namespace KWin { -extern int currentRefreshRate(); - - /** * SyncObject represents a fence used to synchronize operations in * the kwin command stream with operations in the X command stream. @@ -482,11 +479,6 @@ OverlayWindow *SceneOpenGL::overlayWindow() const return m_backend->overlayWindow(); } -bool SceneOpenGL::syncsToVBlank() const -{ - return m_backend->syncsToVBlank(); -} - bool SceneOpenGL::initFailed() const { return !init_ok; diff --git a/plugins/scenes/opengl/scene_opengl.h b/plugins/scenes/opengl/scene_opengl.h index ba294005cd..ff45c14d79 100644 --- a/plugins/scenes/opengl/scene_opengl.h +++ b/plugins/scenes/opengl/scene_opengl.h @@ -41,7 +41,6 @@ public: void screenGeometryChanged(const QSize &size) override; OverlayWindow *overlayWindow() const override; bool usesOverlayWindow() const override; - bool syncsToVBlank() const override; bool makeOpenGLContextCurrent() override; void doneOpenGLContextCurrent() override; bool supportsSurfacelessContext() const override; diff --git a/renderloop.cpp b/renderloop.cpp new file mode 100644 index 0000000000..f6e90d7939 --- /dev/null +++ b/renderloop.cpp @@ -0,0 +1,207 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "renderloop.h" +#include "options.h" +#include "renderloop_p.h" +#include "utils.h" + +namespace KWin +{ + +template +T alignTimestamp(const T ×tamp, const T &alignment) +{ + return timestamp + ((alignment - (timestamp % alignment)) % alignment); +} + +RenderLoopPrivate *RenderLoopPrivate::get(RenderLoop *loop) +{ + return loop->d.data(); +} + +RenderLoopPrivate::RenderLoopPrivate(RenderLoop *q) + : q(q) +{ + compositeTimer.setSingleShot(true); + QObject::connect(&compositeTimer, &QTimer::timeout, q, [this]() { dispatch(); }); +} + +void RenderLoopPrivate::scheduleRepaint() +{ + if (compositeTimer.isActive()) { + return; + } + + const std::chrono::nanoseconds currentTime(std::chrono::steady_clock::now().time_since_epoch()); + const std::chrono::nanoseconds vblankInterval(1'000'000'000'000ull / refreshRate); + + // Estimate when the next presentation will occur. Note that this is a prediction. + nextPresentationTimestamp = lastPresentationTimestamp + + alignTimestamp(currentTime - lastPresentationTimestamp, vblankInterval); + + // Estimate when it's a good time to perform the next compositing cycle. + const std::chrono::nanoseconds safetyMargin = std::chrono::milliseconds(3); + + std::chrono::nanoseconds renderTime; + switch (options->latencyPolicy()) { + case LatencyExteremelyLow: + renderTime = std::chrono::nanoseconds(long(vblankInterval.count() * 0.1)); + break; + case LatencyLow: + renderTime = std::chrono::nanoseconds(long(vblankInterval.count() * 0.25)); + break; + case LatencyMedium: + renderTime = std::chrono::nanoseconds(long(vblankInterval.count() * 0.5)); + break; + case LatencyHigh: + renderTime = std::chrono::nanoseconds(long(vblankInterval.count() * 0.75)); + break; + case LatencyExtremelyHigh: + renderTime = std::chrono::nanoseconds(long(vblankInterval.count() * 0.9)); + break; + } + + std::chrono::nanoseconds nextRenderTimestamp = nextPresentationTimestamp - renderTime - safetyMargin; + + // If we can't render the frame before the deadline, start compositing immediately. + if (nextRenderTimestamp < currentTime) { + nextRenderTimestamp = currentTime; + } + + const std::chrono::nanoseconds waitInterval = nextRenderTimestamp - currentTime; + compositeTimer.start(std::chrono::duration_cast(waitInterval)); +} + +void RenderLoopPrivate::delayScheduleRepaint() +{ + pendingReschedule = true; +} + +void RenderLoopPrivate::maybeScheduleRepaint() +{ + if (pendingReschedule) { + scheduleRepaint(); + pendingReschedule = false; + } +} + +void RenderLoopPrivate::notifyFrameCompleted(std::chrono::nanoseconds timestamp) +{ + Q_ASSERT(pendingFrameCount > 0); + pendingFrameCount--; + + if (lastPresentationTimestamp <= timestamp) { + lastPresentationTimestamp = timestamp; + } else { + qCWarning(KWIN_CORE, "Got invalid presentation timestamp: %ld (current %ld)", + timestamp.count(), lastPresentationTimestamp.count()); + lastPresentationTimestamp = std::chrono::steady_clock::now().time_since_epoch(); + } + + if (!inhibitCount) { + maybeScheduleRepaint(); + } + + emit q->framePresented(q, timestamp); +} + +void RenderLoopPrivate::dispatch() +{ + // On X11, we want to ignore repaints that are scheduled by windows right before + // the Compositor starts repainting. + pendingRepaint = true; + + emit q->frameRequested(q); + + // The Compositor may decide to not repaint when the frameRequested() signal is + // emitted, in which case the pending repaint flag has to be reset manually. + pendingRepaint = false; +} + +void RenderLoopPrivate::invalidate() +{ + pendingReschedule = false; + pendingFrameCount = 0; + compositeTimer.stop(); +} + +RenderLoop::RenderLoop(QObject *parent) + : QObject(parent) + , d(new RenderLoopPrivate(this)) +{ +} + +RenderLoop::~RenderLoop() +{ +} + +void RenderLoop::inhibit() +{ + d->inhibitCount++; + + if (d->inhibitCount == 1) { + d->compositeTimer.stop(); + } +} + +void RenderLoop::uninhibit() +{ + Q_ASSERT(d->inhibitCount > 0); + d->inhibitCount--; + + if (d->inhibitCount == 0) { + d->maybeScheduleRepaint(); + } +} + +void RenderLoop::beginFrame() +{ + d->pendingRepaint = false; + d->pendingFrameCount++; +} + +void RenderLoop::endFrame() +{ +} + +int RenderLoop::refreshRate() const +{ + return d->refreshRate; +} + +void RenderLoop::setRefreshRate(int refreshRate) +{ + if (d->refreshRate == refreshRate) { + return; + } + d->refreshRate = refreshRate; + emit refreshRateChanged(); +} + +void RenderLoop::scheduleRepaint() +{ + if (d->pendingRepaint) { + return; + } + if (!d->pendingFrameCount && !d->inhibitCount) { + d->scheduleRepaint(); + } else { + d->delayScheduleRepaint(); + } +} + +std::chrono::nanoseconds RenderLoop::lastPresentationTimestamp() const +{ + return d->lastPresentationTimestamp; +} + +std::chrono::nanoseconds RenderLoop::nextPresentationTimestamp() const +{ + return d->nextPresentationTimestamp; +} + +} // namespace KWin diff --git a/renderloop.h b/renderloop.h new file mode 100644 index 0000000000..d54982e74d --- /dev/null +++ b/renderloop.h @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwinglobals.h" + +#include + +namespace KWin +{ + +class RenderLoopPrivate; + +/** + * The RenderLoop class represents the compositing scheduler on a particular output. + * + * The RenderLoop class drives the compositing. The frameRequested() signal is emitted + * when the loop wants a new frame to be rendered. The frameCompleted() signal is + * emitted when a previously rendered frame has been presented on the screen. In case + * you want the compositor to repaint the scene, call the scheduleRepaint() function. + */ +class KWIN_EXPORT RenderLoop : public QObject +{ + Q_OBJECT + +public: + explicit RenderLoop(QObject *parent = nullptr); + ~RenderLoop() override; + + /** + * Pauses the render loop. While the render loop is inhibited, scheduleRepaint() + * requests are queued. + * + * Once the render loop is uninhibited, the pending schedule requests are going to + * be re-applied. + */ + void inhibit(); + + /** + * Uninhibits the render loop. + */ + void uninhibit(); + + /** + * This function must be called before the Compositor starts rendering the next + * frame. + */ + void beginFrame(); + + /** + * This function must be called after the Compositor has finished rendering the + * next frame. + */ + void endFrame(); + + /** + * Returns the refresh rate at which the output is being updated, in millihertz. + */ + int refreshRate() const; + + /** + * Sets the refresh rate of this RenderLoop to @a refreshRate, in millihertz. + */ + void setRefreshRate(int refreshRate); + + /** + * Schedules a compositing cycle at the next available moment. + */ + void scheduleRepaint(); + + /** + * Returns the timestamp of the last frame that has been presented on the screen. + * The returned timestamp is sourced from the monotonic clock. + */ + std::chrono::nanoseconds lastPresentationTimestamp() const; + + /** + * If a repaint has been scheduled, this function returns the expected time when + * the next frame will be presented on the screen. The returned timestamp is sourced + * from the monotonic clock. + */ + std::chrono::nanoseconds nextPresentationTimestamp() const; + +Q_SIGNALS: + /** + * This signal is emitted when the refresh rate of this RenderLoop has changed. + */ + void refreshRateChanged(); + /** + * This signal is emitted when a frame has been actually presented on the screen. + * @a timestamp indicates the time when it took place. + */ + void framePresented(RenderLoop *loop, std::chrono::nanoseconds timestamp); + + /** + * This signal is emitted when the render loop wants a new frame to be composited. + * + * The Compositor should make a connection to this signal using Qt::DirectConnection. + */ + void frameRequested(RenderLoop *loop); + +private: + QScopedPointer d; + friend class RenderLoopPrivate; +}; + +} // namespace KWin diff --git a/renderloop_p.h b/renderloop_p.h new file mode 100644 index 0000000000..ffb0a6f4fd --- /dev/null +++ b/renderloop_p.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "renderloop.h" + +#include + +namespace KWin +{ + +class KWIN_EXPORT RenderLoopPrivate +{ +public: + static RenderLoopPrivate *get(RenderLoop *loop); + explicit RenderLoopPrivate(RenderLoop *q); + + void dispatch(); + void invalidate(); + + void delayScheduleRepaint(); + void scheduleRepaint(); + void maybeScheduleRepaint(); + + void notifyFrameCompleted(std::chrono::nanoseconds timestamp); + + RenderLoop *q; + std::chrono::nanoseconds lastPresentationTimestamp = std::chrono::nanoseconds::zero(); + std::chrono::nanoseconds nextPresentationTimestamp = std::chrono::nanoseconds::zero(); + QTimer compositeTimer; + int refreshRate = 60000; + int pendingFrameCount = 0; + int inhibitCount = 0; + bool pendingReschedule = false; + bool pendingRepaint = false; +}; + +} // namespace KWin diff --git a/scene.cpp b/scene.cpp index c59e448b85..8c1f91804c 100644 --- a/scene.cpp +++ b/scene.cpp @@ -210,11 +210,6 @@ void Scene::paintScreen(int* mask, const QRegion &damage, const QRegion &repaint Q_ASSERT(!PaintClipper::clip()); } -// Painting pass is optimized away. -void Scene::idle() -{ -} - // the function that'll be eventually called by paintScreen() above void Scene::finalPaintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) { @@ -663,11 +658,6 @@ void Scene::extendPaintRegion(QRegion ®ion, bool opaqueFullscreen) Q_UNUSED(opaqueFullscreen); } -bool Scene::syncsToVBlank() const -{ - return false; -} - void Scene::screenGeometryChanged(const QSize &size) { if (!overlayWindow()) { diff --git a/scene.h b/scene.h index e28219afa6..6f2b81af13 100644 --- a/scene.h +++ b/scene.h @@ -142,9 +142,6 @@ public: }; // types of filtering available enum ImageFilterType { ImageFilterFast, ImageFilterGood }; - // there's nothing to paint (adjust time_diff later) - void idle(); - virtual bool syncsToVBlank() const; virtual OverlayWindow* overlayWindow() const = 0; virtual bool makeOpenGLContextCurrent();