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();