/* SPDX-FileCopyrightText: 2020 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "renderloop.h" #include "backendoutput.h" #include "options.h" #include "renderloop_p.h" #include "scene/surfaceitem.h" #include "scene/windowitem.h" #include "window.h" #include "workspace.h" using namespace std::chrono_literals; namespace KWin { RenderLoopPrivate *RenderLoopPrivate::get(RenderLoop *loop) { return loop->d.get(); } static const bool s_printDebugInfo = qEnvironmentVariableIntValue("KWIN_LOG_PERFORMANCE_DATA") != 0; RenderLoopPrivate::RenderLoopPrivate(RenderLoop *q, BackendOutput *output) : q(q) , output(output) { QObject::connect(&compositeTimer, &PreciseTimer::timeout, q, [this] { dispatch(); }); } void RenderLoopPrivate::scheduleNextRepaint(std::optional presentNotBefore) { if (kwinApp()->isTerminating() || preparingNewFrame) { return; } const std::chrono::nanoseconds presentNotBeforeTime = presentNotBefore.value_or(std::chrono::steady_clock::now()).time_since_epoch(); if (compositeTimer.isActive() && presentNotBeforeTime >= lastPresentNotBefore) { // this would present later than what we already scheduled for, // so we can ignore it return; } scheduleRepaint(nextPresentationTimestamp, presentNotBeforeTime); } void RenderLoopPrivate::scheduleRepaint(std::chrono::nanoseconds lastTargetTimestamp, std::chrono::nanoseconds presentNotBefore) { pendingReschedule.reset(); const std::chrono::nanoseconds vblankInterval(1'000'000'000'000ull / refreshRate); const std::chrono::nanoseconds currentTime(std::chrono::steady_clock::now().time_since_epoch()); presentNotBefore = std::max(presentNotBefore, currentTime); std::chrono::nanoseconds targetTimestamp; // Estimate when it's a good time to perform the next compositing cycle. // the 1ms on top of the safety margin is required for timer and scheduler inaccuracies std::chrono::nanoseconds expectedCompositingTime = std::min(renderJournal.result() + safetyMargin + 1ms, 2 * vblankInterval); if (presentationMode == PresentationMode::VSync) { // normal presentation: pageflips only happen at vblank const uint64_t pageflipsSince = std::max((presentNotBefore - lastPresentationTimestamp) / vblankInterval, 0); if (pageflipsSince > 100) { // if it's been a while since the last frame, the GPU is likely in a low power state and render time will be increased // -> take that into account and start compositing very early expectedCompositingTime = std::max(vblankInterval - 1us, expectedCompositingTime); } const uint64_t pageflipsSinceLastToTarget = std::max(std::round((lastTargetTimestamp - lastPresentationTimestamp).count() / double(vblankInterval.count())), 0); uint64_t pageflipsInAdvance = std::min(expectedCompositingTime / vblankInterval + 1, maxPendingFrameCount); // switching from double to triple buffering causes a frame drop // -> apply some amount of hysteresis to avoid switching back and forth constantly if (pageflipsInAdvance > 1) { // immediately switch to triple buffering when needed wasTripleBuffering = true; doubleBufferingCounter = 0; } else if (wasTripleBuffering) { // but wait a bit before switching back to double buffering if (doubleBufferingCounter >= 10) { wasTripleBuffering = false; } else if (expectedCompositingTime >= vblankInterval * 0.95) { // also don't switch back if render times are just barely enough for double buffering pageflipsInAdvance = 2; doubleBufferingCounter = 0; expectedCompositingTime = vblankInterval; } else { doubleBufferingCounter++; pageflipsInAdvance = 2; expectedCompositingTime = vblankInterval; } } if (compositeTimer.isActive() && presentNotBefore >= lastPresentNotBefore) { // we already scheduled this frame, but we got a new timestamp // which might require starting to composite earlier than we planned. // It's important here that we do not change the targeted vblank interval, // otherwise with a pessimistic compositing time estimation we might // unnecessarily drop frames const uint32_t intervalsSinceLastTimestamp = std::max(std::round((nextPresentationTimestamp - lastPresentationTimestamp).count() / double(vblankInterval.count())), 0); targetTimestamp = lastPresentationTimestamp + intervalsSinceLastTimestamp * vblankInterval; } else { // Either no frame is scheduled yet, or presentNotBefore is earlier than the last presentNotBefore hint targetTimestamp = lastPresentationTimestamp + std::max(pageflipsSince + pageflipsInAdvance, pageflipsSinceLastToTarget + 1) * vblankInterval; } } else { wasTripleBuffering = false; doubleBufferingCounter = 0; if (presentationMode == PresentationMode::Async || presentationMode == PresentationMode::AdaptiveAsync) { // tearing: pageflips happen ASAP targetTimestamp = presentNotBefore; } else { // adaptive sync: pageflips happen after one vblank interval // FIXME take minimum refresh rate into account as well targetTimestamp = std::max(presentNotBefore, lastPresentationTimestamp + vblankInterval); } } if (compositeTimer.isActive()) { // a frame was previously scheduled, if (presentNotBefore >= lastPresentNotBefore) { // but we have an updated timestamp for the same refresh cycle nextPresentationTimestamp = targetTimestamp; } else if (targetTimestamp < nextPresentationTimestamp) { // but now we want to present in an earlier refresh cycle nextPresentationTimestamp = targetTimestamp; } else { // but this would schedule at a later time lastPresentNotBefore = presentNotBefore; return; } } else { nextPresentationTimestamp = targetTimestamp; } lastPresentNotBefore = presentNotBefore; const std::chrono::nanoseconds nextRenderTimestamp = nextPresentationTimestamp - expectedCompositingTime; compositeTimer.start(nextRenderTimestamp); } void RenderLoopPrivate::notifyFrameDropped() { Q_ASSERT(pendingFrameCount > 0); pendingFrameCount--; if (!inhibitCount && pendingReschedule) { scheduleNextRepaint(pendingReschedule); } } void RenderLoopPrivate::notifyFrameCompleted(std::chrono::nanoseconds timestamp, std::optional renderTime, PresentationMode mode, OutputFrame *frame) { if (output && s_printDebugInfo && !m_debugOutput) { m_debugOutput = std::fstream(qPrintable("kwin perf statistics " + output->name() + ".csv"), std::ios::out); *m_debugOutput << "target pageflip timestamp,pageflip timestamp,render start,render end,safety margin,refresh duration,vrr,tearing,predicted render time\n"; } if (m_debugOutput) { auto times = renderTime.value_or(RenderTimeSpan{}); const bool vrr = mode == PresentationMode::AdaptiveSync || mode == PresentationMode::AdaptiveAsync; const bool tearing = mode == PresentationMode::Async || mode == PresentationMode::AdaptiveAsync; *m_debugOutput << frame->targetPageflipTime().time_since_epoch().count() << "," << timestamp.count() << "," << times.start.time_since_epoch().count() << "," << times.end.time_since_epoch().count() << "," << safetyMargin.count() << "," << frame->refreshDuration().count() << "," << (vrr ? 1 : 0) << "," << (tearing ? 1 : 0) << "," << frame->predictedRenderTime().count() << "\n"; } Q_ASSERT(pendingFrameCount > 0); pendingFrameCount--; notifyVblank(timestamp); if (renderTime) { // timings are pretty unpredictable in the sub-millisecond range; this minimum // ensures that when CPU or GPU power states change, we don't drop any frames const std::chrono::nanoseconds minimumTime = std::chrono::milliseconds(2); renderJournal.add(std::max(minimumTime, renderTime->end - renderTime->start), timestamp); } const auto now = std::chrono::steady_clock::now().time_since_epoch(); if (compositeTimer.isActive() && now > lastPresentNotBefore) { // reschedule to match the new timestamp and render time scheduleRepaint(lastPresentationTimestamp, now); } if (!inhibitCount && pendingReschedule) { scheduleNextRepaint(pendingReschedule); } Q_EMIT q->framePresented(q, timestamp, mode); } void RenderLoopPrivate::notifyVblank(std::chrono::nanoseconds timestamp) { if (lastPresentationTimestamp <= timestamp) { lastPresentationTimestamp = timestamp; } else { qCDebug(KWIN_CORE, "Got invalid presentation timestamp: %lld (current %lld)", static_cast(timestamp.count()), static_cast(lastPresentationTimestamp.count())); lastPresentationTimestamp = std::chrono::steady_clock::now().time_since_epoch(); } } void RenderLoop::timerEvent(QTimerEvent *event) { if (event->timerId() == d->delayedVrrTimer.timerId()) { d->delayedVrrTimer.stop(); scheduleRepaint(nullptr, nullptr); } else { QObject::timerEvent(event); } } void RenderLoopPrivate::dispatch() { Q_EMIT q->frameRequested(q); } RenderLoop::RenderLoop(BackendOutput *output) : d(std::make_unique(this, output)) { } 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->scheduleNextRepaint(std::nullopt); } } void RenderLoop::prepareNewFrame() { d->pendingFrameCount++; d->preparingNewFrame = true; } void RenderLoop::newFramePrepared() { d->preparingNewFrame = false; } int RenderLoop::refreshRate() const { return d->refreshRate; } void RenderLoop::setRefreshRate(int refreshRate) { if (d->refreshRate == refreshRate) { return; } d->refreshRate = refreshRate; Q_EMIT refreshRateChanged(); if (d->compositeTimer.isActive()) { d->scheduleRepaint(d->lastPresentationTimestamp, std::chrono::steady_clock::now().time_since_epoch()); } } void RenderLoop::setPresentationSafetyMargin(std::chrono::nanoseconds safetyMargin) { d->safetyMargin = safetyMargin; } void RenderLoop::scheduleRepaint(Item *item, OutputLayer *outputLayer, std::optional presentNotBefore) { if (d->preparingNewFrame) { return; } const auto surfaceItem = repaintDriver(); if ((item || outputLayer) && d->output && surfaceItem && item != surfaceItem && (!item || !surfaceItem->isAncestorOf(item))) { constexpr std::chrono::milliseconds s_delayVrrTimer = 1'000ms / 30; d->delayedVrrTimer.start(s_delayVrrTimer, Qt::PreciseTimer, this); return; } d->delayedVrrTimer.stop(); const bool vrr = d->presentationMode == PresentationMode::AdaptiveSync || d->presentationMode == PresentationMode::AdaptiveAsync; const bool tearing = d->presentationMode == PresentationMode::Async || d->presentationMode == PresentationMode::AdaptiveAsync; const int effectiveMaxPendingFrameCount = (vrr || tearing) ? 1 : d->maxPendingFrameCount; if (d->pendingFrameCount < effectiveMaxPendingFrameCount && !d->inhibitCount) { d->scheduleNextRepaint(presentNotBefore); } else if (d->pendingReschedule) { d->pendingReschedule = std::min(*d->pendingReschedule, presentNotBefore.value_or(std::chrono::steady_clock::now())); } else { d->pendingReschedule = presentNotBefore.value_or(std::chrono::steady_clock::now()); } } Item *RenderLoop::repaintDriver() const { if (Q_UNLIKELY(!workspace())) { return nullptr; } const bool vrr = d->presentationMode == PresentationMode::AdaptiveSync || d->presentationMode == PresentationMode::AdaptiveAsync; const bool tearing = d->presentationMode == PresentationMode::Async || d->presentationMode == PresentationMode::AdaptiveAsync; if (!vrr && !tearing) { return nullptr; } Window *const activeWindow = workspace()->activeWindow(); LogicalOutput *logical = workspace()->findOutput(d->output); if (!logical) { return nullptr; } const bool control = activeWindow && activeWindow->frameGeometry().intersects(logical->geometryF()) && activeWindow->surfaceItem() && activeWindow->surfaceItem()->recursiveFrameTimeEstimation().transform([](const auto t) { return t <= std::chrono::nanoseconds(1'000'000'000) / 30; }).value_or(false); if (control) { return activeWindow->windowItem(); } else { return nullptr; } } std::chrono::nanoseconds RenderLoop::lastPresentationTimestamp() const { return d->lastPresentationTimestamp; } std::chrono::nanoseconds RenderLoop::nextPresentationTimestamp() const { return d->nextPresentationTimestamp; } void RenderLoop::setPresentationMode(PresentationMode mode) { if (mode != d->presentationMode) { qCDebug(KWIN_CORE) << "Changed presentation mode to" << mode; } d->presentationMode = mode; } void RenderLoop::setMaxPendingFrameCount(uint32_t maxCount) { d->maxPendingFrameCount = maxCount; } std::chrono::nanoseconds RenderLoop::predictedRenderTime() const { return d->renderJournal.result(); } } // namespace KWin #include "moc_renderloop.cpp"