You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
352 lines
12 KiB
352 lines
12 KiB
/* |
|
KWin - the KDE window manager |
|
This file is part of the KDE project. |
|
|
|
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
#include "drm_commit_thread.h" |
|
#include "drm_commit.h" |
|
#include "drm_gpu.h" |
|
#include "drm_logging.h" |
|
#include "utils/realtime.h" |
|
|
|
#include <span> |
|
|
|
using namespace std::chrono_literals; |
|
|
|
namespace KWin |
|
{ |
|
|
|
DrmCommitThread::DrmCommitThread(DrmGpu *gpu, const QString &name) |
|
{ |
|
if (!gpu->atomicModeSetting()) { |
|
return; |
|
} |
|
|
|
m_thread.reset(QThread::create([this]() { |
|
const auto thread = QThread::currentThread(); |
|
gainRealTime(); |
|
while (true) { |
|
if (thread->isInterruptionRequested()) { |
|
return; |
|
} |
|
std::unique_lock lock(m_mutex); |
|
bool timeout = false; |
|
if (m_committed) { |
|
timeout = m_commitPending.wait_for(lock, DrmGpu::s_pageflipTimeout) == std::cv_status::timeout; |
|
} else if (m_commits.empty()) { |
|
m_commitPending.wait(lock); |
|
} |
|
if (m_committed) { |
|
// the commit would fail with EBUSY, wait until the pageflip is done |
|
if (timeout) { |
|
qCCritical(KWIN_DRM, "Pageflip timed out! This is a kernel bug"); |
|
} |
|
continue; |
|
} |
|
if (m_commits.empty()) { |
|
continue; |
|
} |
|
const auto now = std::chrono::steady_clock::now(); |
|
if (m_targetPageflipTime > now + m_safetyMargin) { |
|
lock.unlock(); |
|
std::this_thread::sleep_until(m_targetPageflipTime - m_safetyMargin); |
|
lock.lock(); |
|
// the main thread might've modified the list |
|
if (m_commits.empty()) { |
|
continue; |
|
} |
|
} |
|
optimizeCommits(m_targetPageflipTime); |
|
if (!m_commits.front()->isReadyFor(m_targetPageflipTime)) { |
|
// no commit is ready yet, reschedule |
|
if (m_vrr || m_tearing) { |
|
m_targetPageflipTime += 50us; |
|
} else { |
|
m_targetPageflipTime += m_minVblankInterval; |
|
} |
|
continue; |
|
} |
|
if (m_commits.front()->isCursorOnly() && m_vrr) { |
|
// wait for a primary plane commit to be in, while still enforcing |
|
// a minimum cursor refresh rate of 30Hz |
|
const auto cursorTarget = m_lastPageflip + std::chrono::duration_cast<std::chrono::nanoseconds>(1s) / 30; |
|
const bool cursorOnly = std::ranges::all_of(m_commits, [](const auto &commit) { |
|
return commit->isCursorOnly(); |
|
}); |
|
if (cursorOnly) { |
|
// no primary plane commit, just wait until a new one gets added or the cursorTarget time is reached |
|
if (m_commitPending.wait_until(lock, cursorTarget) == std::cv_status::no_timeout) { |
|
continue; |
|
} |
|
} else { |
|
bool timeout = true; |
|
while (std::chrono::steady_clock::now() < cursorTarget && timeout && m_commits.front()->isCursorOnly()) { |
|
timeout = m_commitPending.wait_for(lock, 50us) == std::cv_status::timeout; |
|
if (m_commits.empty()) { |
|
break; |
|
} |
|
optimizeCommits(cursorTarget); |
|
} |
|
if (!timeout) { |
|
// some new commit was added, process that |
|
continue; |
|
} |
|
} |
|
if (m_commits.empty()) { |
|
continue; |
|
} |
|
} |
|
submit(); |
|
} |
|
})); |
|
m_thread->setObjectName(name); |
|
m_thread->start(); |
|
} |
|
|
|
void DrmCommitThread::submit() |
|
{ |
|
DrmAtomicCommit *commit = m_commits.front().get(); |
|
const auto vrr = commit->isVrr(); |
|
const bool success = commit->commit(); |
|
if (success) { |
|
m_vrr = vrr.value_or(m_vrr); |
|
m_tearing = commit->isTearing(); |
|
m_committed = std::move(m_commits.front()); |
|
m_commits.erase(m_commits.begin()); |
|
} else { |
|
if (m_commits.size() > 1) { |
|
// the failure may have been because of the reordering of commits |
|
// -> collapse all commits into one and try again with an already tested state |
|
while (m_commits.size() > 1) { |
|
auto toMerge = std::move(m_commits[1]); |
|
m_commits.erase(m_commits.begin() + 1); |
|
commit->merge(toMerge.get()); |
|
m_commitsToDelete.push_back(std::move(toMerge)); |
|
} |
|
if (commit->test()) { |
|
// presentation didn't fail after all, try again |
|
submit(); |
|
return; |
|
} |
|
} |
|
for (auto &commit : m_commits) { |
|
m_commitsToDelete.push_back(std::move(commit)); |
|
} |
|
m_commits.clear(); |
|
qCWarning(KWIN_DRM) << "atomic commit failed:" << strerror(errno); |
|
} |
|
QMetaObject::invokeMethod(this, &DrmCommitThread::clearDroppedCommits, Qt::ConnectionType::QueuedConnection); |
|
} |
|
|
|
static std::unique_ptr<DrmAtomicCommit> mergeCommits(std::span<const std::unique_ptr<DrmAtomicCommit>> commits) |
|
{ |
|
auto ret = std::make_unique<DrmAtomicCommit>(*commits.front()); |
|
for (const auto &onTop : commits.subspan(1)) { |
|
ret->merge(onTop.get()); |
|
} |
|
return ret; |
|
} |
|
|
|
void DrmCommitThread::optimizeCommits(TimePoint pageflipTarget) |
|
{ |
|
if (m_commits.size() <= 1) { |
|
return; |
|
} |
|
// merge commits in the front that are already ready (regardless of which planes they modify) |
|
if (m_commits.front()->areBuffersReadable()) { |
|
const auto firstNotReady = std::find_if(m_commits.begin() + 1, m_commits.end(), [pageflipTarget](const auto &commit) { |
|
return !commit->isReadyFor(pageflipTarget); |
|
}); |
|
if (firstNotReady != m_commits.begin() + 1) { |
|
auto merged = mergeCommits(std::span(m_commits.begin(), firstNotReady)); |
|
std::move(m_commits.begin(), firstNotReady, std::back_inserter(m_commitsToDelete)); |
|
m_commits.erase(m_commits.begin() + 1, firstNotReady); |
|
m_commits.front() = std::move(merged); |
|
} |
|
} |
|
// merge commits that are ready and modify the same drm planes |
|
for (auto it = m_commits.begin(); it != m_commits.end();) { |
|
const auto startIt = it; |
|
auto &startCommit = *startIt; |
|
const auto firstNotSamePlaneNotReady = std::find_if(startIt + 1, m_commits.end(), [&startCommit, pageflipTarget](const auto &commit) { |
|
return startCommit->modifiedPlanes() != commit->modifiedPlanes() || !commit->isReadyFor(pageflipTarget); |
|
}); |
|
if (firstNotSamePlaneNotReady == startIt + 1) { |
|
it++; |
|
continue; |
|
} |
|
auto merged = mergeCommits(std::span(startIt, firstNotSamePlaneNotReady)); |
|
std::move(startIt, firstNotSamePlaneNotReady, std::back_inserter(m_commitsToDelete)); |
|
startCommit = std::move(merged); |
|
it = m_commits.erase(startIt + 1, firstNotSamePlaneNotReady); |
|
} |
|
if (m_commits.size() == 1) { |
|
// already done |
|
return; |
|
} |
|
std::unique_ptr<DrmAtomicCommit> front; |
|
if (m_commits.front()->isReadyFor(pageflipTarget)) { |
|
// can't just move the commit, or merging might drop the last reference |
|
// to an OutputFrame, which should only happen in the main thread |
|
front = std::make_unique<DrmAtomicCommit>(*m_commits.front()); |
|
m_commitsToDelete.push_back(std::move(m_commits.front())); |
|
m_commits.erase(m_commits.begin()); |
|
} |
|
// try to move commits that are ready to the front |
|
for (auto it = m_commits.begin() + 1; it != m_commits.end();) { |
|
auto &commit = *it; |
|
if (!commit->isReadyFor(pageflipTarget)) { |
|
it++; |
|
continue; |
|
} |
|
// commits that target the same plane(s) need to stay in the same order |
|
const auto &planes = commit->modifiedPlanes(); |
|
const bool skipping = std::any_of(m_commits.begin(), it, [&planes](const auto &other) { |
|
return std::ranges::any_of(planes, [&other](DrmPlane *plane) { |
|
return other->modifiedPlanes().contains(plane); |
|
}); |
|
}); |
|
if (skipping) { |
|
it++; |
|
continue; |
|
} |
|
// find out if the modified commit order will actually work |
|
std::unique_ptr<DrmAtomicCommit> duplicate; |
|
if (front) { |
|
duplicate = std::make_unique<DrmAtomicCommit>(*front); |
|
duplicate->merge(commit.get()); |
|
if (!duplicate->test()) { |
|
m_commitsToDelete.push_back(std::move(duplicate)); |
|
it++; |
|
continue; |
|
} |
|
} else { |
|
if (!commit->test()) { |
|
it++; |
|
continue; |
|
} |
|
duplicate = std::make_unique<DrmAtomicCommit>(*commit); |
|
} |
|
bool success = true; |
|
for (const auto &otherCommit : m_commits) { |
|
if (otherCommit != commit) { |
|
duplicate->merge(otherCommit.get()); |
|
if (!duplicate->test()) { |
|
success = false; |
|
break; |
|
} |
|
} |
|
} |
|
m_commitsToDelete.push_back(std::move(duplicate)); |
|
if (success) { |
|
if (front) { |
|
front->merge(commit.get()); |
|
m_commitsToDelete.push_back(std::move(commit)); |
|
} else { |
|
front = std::make_unique<DrmAtomicCommit>(*commit); |
|
m_commitsToDelete.push_back(std::move(commit)); |
|
} |
|
it = m_commits.erase(it); |
|
} else { |
|
it++; |
|
} |
|
} |
|
if (front) { |
|
m_commits.insert(m_commits.begin(), std::move(front)); |
|
} |
|
} |
|
|
|
DrmCommitThread::~DrmCommitThread() |
|
{ |
|
if (m_thread) { |
|
m_thread->requestInterruption(); |
|
m_commitPending.notify_all(); |
|
m_thread->wait(); |
|
} |
|
} |
|
|
|
void DrmCommitThread::addCommit(std::unique_ptr<DrmAtomicCommit> &&commit) |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
m_commits.push_back(std::move(commit)); |
|
const auto now = std::chrono::steady_clock::now(); |
|
if (m_tearing) { |
|
m_targetPageflipTime = now; |
|
} else if (m_vrr && now >= m_lastPageflip + m_minVblankInterval) { |
|
m_targetPageflipTime = now; |
|
} else { |
|
m_targetPageflipTime = estimateNextVblank(now); |
|
} |
|
m_commits.back()->setDeadline(m_targetPageflipTime - m_safetyMargin); |
|
m_commitPending.notify_all(); |
|
} |
|
|
|
void DrmCommitThread::setPendingCommit(std::unique_ptr<DrmLegacyCommit> &&commit) |
|
{ |
|
m_committed = std::move(commit); |
|
} |
|
|
|
void DrmCommitThread::clearDroppedCommits() |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
m_commitsToDelete.clear(); |
|
} |
|
|
|
void DrmCommitThread::setModeInfo(uint32_t maximum, std::chrono::nanoseconds vblankTime) |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
m_minVblankInterval = std::chrono::nanoseconds(1'000'000'000'000ull / maximum); |
|
// the kernel rejects commits that happen during vblank |
|
// the 1.5ms on top of that was chosen experimentally, for the time it takes to commit + scheduling inaccuracies |
|
m_safetyMargin = vblankTime + 1500us; |
|
} |
|
|
|
void DrmCommitThread::pageFlipped(std::chrono::nanoseconds timestamp) |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
m_lastPageflip = TimePoint(timestamp); |
|
m_committed.reset(); |
|
if (!m_commits.empty()) { |
|
m_targetPageflipTime = estimateNextVblank(std::chrono::steady_clock::now()); |
|
m_commitPending.notify_all(); |
|
} |
|
} |
|
|
|
bool DrmCommitThread::pageflipsPending() |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
return !m_commits.empty() || m_committed; |
|
} |
|
|
|
TimePoint DrmCommitThread::estimateNextVblank(TimePoint now) const |
|
{ |
|
// the pageflip timestamp may be in the future |
|
const uint64_t pageflipsSince = now >= m_lastPageflip ? (now - m_lastPageflip) / m_minVblankInterval : 0; |
|
return m_lastPageflip + m_minVblankInterval * (pageflipsSince + 1); |
|
} |
|
|
|
std::chrono::nanoseconds DrmCommitThread::safetyMargin() const |
|
{ |
|
return m_safetyMargin; |
|
} |
|
|
|
bool DrmCommitThread::drain() |
|
{ |
|
std::unique_lock lock(m_mutex); |
|
if (m_committed) { |
|
return true; |
|
} |
|
if (m_commits.empty()) { |
|
return false; |
|
} |
|
if (m_commits.size() > 1) { |
|
m_commits.front() = mergeCommits(m_commits); |
|
m_commits.erase(m_commits.begin() + 1, m_commits.end()); |
|
} |
|
submit(); |
|
return m_committed != nullptr; |
|
} |
|
}
|
|
|