platforms/drm: delay presentation for modesets

Currently KWin is combining modesets with presentation, which causes problems
when multiple monitors are used and crtcs need to be switched around, because
taking away a CRTC from another output causes the driver to disable the
other output. In order to avoid such problems, delay presentation until
all pipelines are ready to present and then do a modeset with a single atomic
commit. To process the resulting page flip events properly this commit also
ports KWin to page_flip_handler2 and changes how the pageFlipped and
notifyFrameFailed signals are processed.
This commit is contained in:
Xaver Hugl 2021-10-08 10:52:01 +02:00
parent e2a0863843
commit a07aae8282
14 changed files with 209 additions and 52 deletions

View file

@ -763,6 +763,9 @@ bool DrmBackend::applyOutputChanges(const WaylandOutputConfig &config)
}
};
updateCursor();
if (auto compositor = Compositor::self()) {
compositor->addRepaintFull();
}
return true;
}

View file

@ -83,6 +83,11 @@ bool GbmBuffer::map(uint32_t flags)
return m_data;
}
KWaylandServer::ClientBuffer *GbmBuffer::clientBuffer() const
{
return m_clientBuffer;
}
DrmGbmBuffer::DrmGbmBuffer(DrmGpu *gpu, GbmSurface *surface, gbm_bo *bo)
: DrmBuffer(gpu, gbm_bo_get_format(bo), gbm_bo_get_modifier(bo)), GbmBuffer(surface, bo)

View file

@ -47,6 +47,7 @@ public:
uint32_t stride() const {
return m_stride;
}
KWaylandServer::ClientBuffer *clientBuffer() const;
protected:
GbmSurface *m_surface = nullptr;

View file

@ -347,7 +347,30 @@ bool DrmGpu::checkCrtcAssignment(QVector<DrmConnector*> connectors, QVector<DrmC
qCWarning(KWIN_DRM) << "disabling connector" << conn->modelName() << "without a crtc";
conn->pipeline()->pending.crtc = nullptr;
}
return DrmPipeline::commitPipelines(m_pipelines, DrmPipeline::CommitMode::Test);
// non-desktop outputs need to be tested when they would be enabled so that they can be used if leased
QVector<DrmPipeline*> leasePipelines;
for (const auto &output : qAsConst(m_leaseOutputs)) {
if (!output->lease()) {
output->pipeline()->pending.active = true;
leasePipelines << output->pipeline();
}
}
bool test = DrmPipeline::commitPipelines(m_pipelines, DrmPipeline::CommitMode::Test);
if (!leasePipelines.isEmpty() && test) {
// non-desktop outputs should be disabled for normal usage
for (const auto &pipeline : qAsConst(leasePipelines)) {
pipeline->pending.active = false;
}
bool ret = DrmPipeline::commitPipelines(m_pipelines, DrmPipeline::CommitMode::Test);
if (ret) {
for (const auto &pipeline : qAsConst(leasePipelines)) {
pipeline->applyPendingChanges();
}
}
return ret;
} else {
return test;
}
}
auto connector = connectors.takeFirst();
auto pipeline = connector->pipeline();
@ -424,7 +447,7 @@ void DrmGpu::waitIdle()
m_socketNotifier->setEnabled(false);
while (true) {
const bool idle = std::all_of(m_drmOutputs.constBegin(), m_drmOutputs.constEnd(), [](DrmOutput *output){
return !output->m_pageFlipPending;
return !output->pipeline()->pageflipPending();
});
if (idle) {
break;
@ -471,10 +494,10 @@ static std::chrono::nanoseconds convertTimestamp(clockid_t sourceClock, clockid_
return convertTimestamp(targetCurrentTime) - delta;
}
static void pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data)
void DrmGpu::pageFlipHandler(int fd, unsigned int sequence, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *user_data)
{
Q_UNUSED(fd)
Q_UNUSED(frame)
Q_UNUSED(sequence)
Q_UNUSED(user_data)
auto backend = dynamic_cast<DrmBackend*>(kwinApp()->platform());
if (!backend) {
return;
@ -483,29 +506,28 @@ static void pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsign
if (!gpu) {
return;
}
auto output = static_cast<DrmOutput *>(data);
if (!gpu->outputs().contains(output)) {
// output already got deleted
return;
}
// The static_cast<> here are for a 32-bit environment where
// sizeof(time_t) == sizeof(unsigned int) == 4 . Putting @p sec
// into a time_t cuts off the most-significant bit (after the
// year 2038), similarly long can't hold all the bits of an
// unsigned multiplication.
std::chrono::nanoseconds timestamp = convertTimestamp(output->gpu()->presentationClock(),
CLOCK_MONOTONIC,
std::chrono::nanoseconds timestamp = convertTimestamp(gpu->presentationClock(), CLOCK_MONOTONIC,
{ static_cast<time_t>(sec), static_cast<long>(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()));
qCDebug(KWIN_DRM, "Got invalid timestamp (sec: %u, usec: %u) on gpu %s",
sec, usec, qPrintable(gpu->devNode()));
timestamp = std::chrono::steady_clock::now().time_since_epoch();
}
output->pageFlipped();
RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(output->renderLoop());
renderLoopPrivate->notifyFrameCompleted(timestamp);
const auto pipelines = gpu->pipelines();
auto it = std::find_if(pipelines.begin(), pipelines.end(), [crtc_id](const auto &pipeline) {
return pipeline->currentCrtc() && pipeline->currentCrtc()->id() == crtc_id;
});
if (it == pipelines.end()) {
qCWarning(KWIN_DRM, "received invalid page flip event for crtc %u", crtc_id);
} else {
(*it)->pageFlipped(timestamp);
}
}
void DrmGpu::dispatchEvents()
@ -514,8 +536,8 @@ void DrmGpu::dispatchEvents()
return;
}
drmEventContext context = {};
context.version = 2;
context.page_flip_handler = pageFlipHandler;
context.version = 3;
context.page_flip_handler2 = pageFlipHandler;
drmHandleEvent(m_fd, &context);
}
@ -707,4 +729,30 @@ bool DrmGpu::isNVidia() const
return m_isNVidia;
}
bool DrmGpu::needsModeset() const
{
return std::any_of(m_pipelines.constBegin(), m_pipelines.constEnd(), [](const auto &pipeline) {
return pipeline->needsModeset();
});
}
bool DrmGpu::maybeModeset()
{
auto pipelines = m_pipelines;
for (const auto &output : qAsConst(m_leaseOutputs)) {
if (output->lease()) {
pipelines.removeOne(output->pipeline());
}
}
bool presentPendingForAll = std::all_of(pipelines.constBegin(), pipelines.constEnd(), [](const auto &pipeline) {
return pipeline->modesetPresentPending() || !pipeline->pending.active;
});
if (presentPendingForAll) {
return DrmPipeline::commitPipelines(pipelines, DrmPipeline::CommitMode::CommitModeset);
} else {
// commit only once all pipelines are ready for presentation
return true;
}
}
}

View file

@ -83,6 +83,9 @@ public:
DrmVirtualOutput *createVirtualOutput(const QString &name, const QSize &size, double scale, VirtualOutputMode mode);
void removeVirtualOutput(DrmVirtualOutput *output);
bool needsModeset() const;
bool maybeModeset();
Q_SIGNALS:
void outputAdded(DrmAbstractOutput *output);
void outputRemoved(DrmAbstractOutput *output);
@ -102,6 +105,8 @@ private:
void handleLeaseRequest(KWaylandServer::DrmLeaseV1Interface *leaseRequest);
void handleLeaseRevoked(KWaylandServer::DrmLeaseV1Interface *lease);
static void pageFlipHandler(int fd, unsigned int sequence, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *user_data);
const int m_fd;
const dev_t m_deviceId;
const QString m_devNode;

View file

@ -306,6 +306,9 @@ bool DrmConnector::vrrCapable() const
bool DrmConnector::needsModeset() const
{
if (!gpu()->atomicModeSetting()) {
return false;
}
if (getProp(PropertyIndex::CrtcId)->needsCommit()) {
return true;
}

View file

@ -54,6 +54,9 @@ drmModeModeInfo DrmCrtc::queryCurrentMode()
bool DrmCrtc::needsModeset() const
{
if (!gpu()->atomicModeSetting()) {
return false;
}
return getProp(PropertyIndex::Active)->needsCommit()
|| getProp(PropertyIndex::ModeId)->needsCommit();
}

View file

@ -168,6 +168,9 @@ void DrmPlane::setBuffer(DrmBuffer *buffer)
bool DrmPlane::needsModeset() const
{
if (!gpu()->atomicModeSetting()) {
return false;
}
auto rotation = getProp(PropertyIndex::Rotation);
if (rotation && rotation->needsCommit()) {
return true;

View file

@ -69,9 +69,6 @@ DrmOutput::DrmOutput(DrmPipeline *pipeline)
DrmOutput::~DrmOutput()
{
if (m_pageFlipPending) {
pageFlipped();
}
m_pipeline->setOutput(nullptr);
}
@ -264,7 +261,7 @@ bool DrmOutput::setDrmDpmsMode(DpmsMode mode)
return true;
}
m_pipeline->pending.active = active;
if (DrmPipeline::commitPipelines({m_pipeline}, active ? DrmPipeline::CommitMode::Test : DrmPipeline::CommitMode::Commit)) {
if (DrmPipeline::commitPipelines({m_pipeline}, active ? DrmPipeline::CommitMode::Test : DrmPipeline::CommitMode::CommitModeset)) {
m_pipeline->applyPendingChanges();
setDpmsModeInternal(mode);
if (active) {
@ -370,13 +367,6 @@ bool DrmOutput::needsSoftwareTransformation() const
return m_pipeline->pending.transformation != outputToPlaneTransform(transform());
}
void DrmOutput::pageFlipped()
{
Q_ASSERT(m_pageFlipPending || !m_gpu->atomicModeSetting());
m_pageFlipPending = false;
m_pipeline->pageFlipped();
}
bool DrmOutput::present(const QSharedPointer<DrmBuffer> &buffer, QRegion damagedRegion)
{
if (!buffer || buffer->bufferId() == 0) {
@ -393,7 +383,6 @@ bool DrmOutput::present(const QSharedPointer<DrmBuffer> &buffer, QRegion damaged
}
}
if (m_pipeline->present(buffer)) {
m_pageFlipPending = true;
Q_EMIT outputChange(damagedRegion);
return true;
} else {
@ -494,4 +483,14 @@ void DrmOutput::revertQueuedChanges()
m_pipeline->revertPendingChanges();
}
void DrmOutput::pageFlipped(std::chrono::nanoseconds timestamp)
{
RenderLoopPrivate::get(m_renderLoop)->notifyFrameCompleted(timestamp);
}
void DrmOutput::presentFailed()
{
RenderLoopPrivate::get(m_renderLoop)->notifyFrameFailed();
}
}

View file

@ -20,6 +20,7 @@
#include <QVector>
#include <QSharedPointer>
#include <xf86drmMode.h>
#include <chrono>
namespace KWin
{
@ -47,7 +48,6 @@ public:
bool moveCursor() override;
bool present(const QSharedPointer<DrmBuffer> &buffer, QRegion damagedRegion) override;
void pageFlipped();
DrmConnector *connector() const;
DrmPipeline *pipeline() const;
@ -61,6 +61,9 @@ public:
void applyQueuedChanges(const WaylandOutputConfig &config);
void revertQueuedChanges();
void pageFlipped(std::chrono::nanoseconds timestamp);
void presentFailed();
private:
friend class DrmGpu;
friend class DrmBackend;
@ -84,7 +87,6 @@ private:
DrmConnector *m_connector;
QSharedPointer<DrmDumbBuffer> m_cursor;
bool m_pageFlipPending = false;
QTimer m_turnOffTimer;
};

View file

@ -46,11 +46,21 @@ DrmPipeline::DrmPipeline(DrmConnector *conn)
DrmPipeline::~DrmPipeline()
{
m_output = nullptr;
if (m_pageflipPending && m_current.crtc) {
pageFlipped({});
}
}
bool DrmPipeline::present(const QSharedPointer<DrmBuffer> &buffer)
{
Q_ASSERT(pending.crtc);
if (!buffer) {
if (m_output) {
m_output->presentFailed();
}
return false;
}
m_primaryBuffer = buffer;
if (gpu()->useEglStreams() && gpu()->eglBackend() != nullptr && gpu() == gpu()->platform()->primaryGpu()) {
// EglStreamBackend queues normal page flips through EGL,
@ -59,19 +69,42 @@ bool DrmPipeline::present(const QSharedPointer<DrmBuffer> &buffer)
return true;
}
}
bool directScanout = false;
#if HAVE_GBM
// with direct scanout disallow modesets, calling presentFailed() and logging warnings
if (auto buf = dynamic_cast<DrmGbmBuffer*>(buffer.data()); buf && buf->clientBuffer()) {
directScanout = true;
}
#endif
if (gpu()->needsModeset()) {
if (directScanout) {
return false;
}
m_modesetPresentPending = true;
return gpu()->maybeModeset();
}
if (gpu()->atomicModeSetting()) {
if (!commitPipelines({this}, CommitMode::Commit)) {
// update properties and try again
updateProperties();
if (!commitPipelines({this}, CommitMode::Commit)) {
if (directScanout) {
return false;
}
qCWarning(KWIN_DRM) << "Atomic present failed!" << strerror(errno);
printDebugInfo();
if (m_output) {
m_output->presentFailed();
}
return false;
}
}
} else {
if (!presentLegacy()) {
qCWarning(KWIN_DRM) << "Present failed!" << strerror(errno);
if (m_output) {
m_output->presentFailed();
}
return false;
}
}
@ -87,8 +120,8 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
qCDebug(KWIN_DRM) << "Failed to allocate drmModeAtomicReq!" << strerror(errno);
return false;
}
uint32_t flags = DRM_MODE_ATOMIC_NONBLOCK;
const auto &failed = [pipelines, req](){
uint32_t flags = 0;
const auto &failed = [pipelines, req, mode](){
drmModeAtomicFree(req);
for (const auto &pipeline : pipelines) {
pipeline->printDebugInfo();
@ -101,6 +134,10 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
pipeline->pending.crtc->rollbackPending();
pipeline->pending.crtc->primaryPlane()->rollbackPending();
}
if (mode != CommitMode::Test && pipeline->activePending() && pipeline->output()) {
pipeline->m_modesetPresentPending = false;
pipeline->output()->presentFailed();
}
}
return false;
};
@ -114,11 +151,22 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
return failed();
}
}
if (drmModeAtomicCommit(pipelines[0]->gpu()->fd(), req, (flags & (~DRM_MODE_PAGE_FLIP_EVENT)) | DRM_MODE_ATOMIC_TEST_ONLY, pipelines[0]->output()) != 0) {
bool modeset = flags & DRM_MODE_ATOMIC_ALLOW_MODESET;
Q_ASSERT(!modeset || mode != CommitMode::Commit);
if (modeset) {
// The kernel fails commits with DRM_MODE_PAGE_FLIP_EVENT when a crtc is disabled in the commit
// and already was disabled before, to work around some quirks in old userspace.
// Instead of using DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK, do the modeset in a blocking
// fashion without page flip events and directly call the pageFlipped method afterwards
flags = flags & (~DRM_MODE_PAGE_FLIP_EVENT);
} else {
flags |= DRM_MODE_ATOMIC_NONBLOCK;
}
if (drmModeAtomicCommit(pipelines[0]->gpu()->fd(), req, (flags & (~DRM_MODE_PAGE_FLIP_EVENT)) | DRM_MODE_ATOMIC_TEST_ONLY, nullptr) != 0) {
qCWarning(KWIN_DRM) << "Atomic test for" << mode << "failed!" << strerror(errno);
return failed();
}
if (mode != CommitMode::Test && drmModeAtomicCommit(pipelines[0]->gpu()->fd(), req, flags, pipelines[0]->output()) != 0) {
if (mode != CommitMode::Test && drmModeAtomicCommit(pipelines[0]->gpu()->fd(), req, flags, nullptr) != 0) {
qCWarning(KWIN_DRM) << "Atomic commit failed! This should never happen!" << strerror(errno);
return failed();
}
@ -130,6 +178,8 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
pipeline->pending.crtc->primaryPlane()->commitPending();
}
if (mode != CommitMode::Test) {
pipeline->m_modesetPresentPending = false;
pipeline->m_pageflipPending = true;
pipeline->m_connector->commit();
if (pipeline->pending.crtc) {
pipeline->pending.crtc->primaryPlane()->setNext(pipeline->m_primaryBuffer);
@ -137,6 +187,9 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
pipeline->pending.crtc->primaryPlane()->commit();
}
pipeline->m_current = pipeline->pending;
if (modeset && pipeline->activePending()) {
pipeline->pageFlipped(std::chrono::steady_clock::now().time_since_epoch());
}
}
}
drmModeAtomicFree(req);
@ -152,11 +205,21 @@ bool DrmPipeline::commitPipelines(const QVector<DrmPipeline*> &pipelines, Commit
if (failure) {
// at least try to revert the config
for (const auto &pipeline : pipelines) {
pipeline->pending = pipeline->m_next;
pipeline->revertPendingChanges();
pipeline->applyPendingChangesLegacy();
if (mode == CommitMode::CommitModeset && pipeline->output() && pipeline->activePending()) {
pipeline->output()->presentFailed();
}
}
return false;
} else {
for (const auto &pipeline : pipelines) {
pipeline->applyPendingChanges();
pipeline->m_current = pipeline->pending;
if (mode == CommitMode::CommitModeset && mode != CommitMode::Test && pipeline->activePending()) {
pipeline->pageFlipped(std::chrono::steady_clock::now().time_since_epoch());
}
}
return true;
}
}
@ -201,6 +264,7 @@ bool DrmPipeline::presentLegacy()
qCWarning(KWIN_DRM) << "Page flip failed:" << strerror(errno) << m_primaryBuffer;
return false;
}
m_pageflipPending = true;
pending.crtc->setNext(m_primaryBuffer);
return true;
}
@ -378,12 +442,16 @@ DrmGpu *DrmPipeline::gpu() const
return m_connector->gpu();
}
void DrmPipeline::pageFlipped()
void DrmPipeline::pageFlipped(std::chrono::nanoseconds timestamp)
{
m_current.crtc->flipBuffer();
if (m_current.crtc->primaryPlane()) {
m_current.crtc->primaryPlane()->flipBuffer();
}
m_pageflipPending = false;
if (m_output) {
m_output->pageFlipped(timestamp);
}
}
void DrmPipeline::setOutput(DrmOutput *output)
@ -426,7 +494,8 @@ bool DrmPipeline::needsModeset() const
|| pending.active != m_current.active
|| pending.modeIndex != m_current.modeIndex
|| pending.rgbRange != m_current.rgbRange
|| pending.transformation != m_current.transformation;
|| pending.transformation != m_current.transformation
|| m_modesetPresentPending;
}
bool DrmPipeline::activePending() const
@ -439,6 +508,21 @@ void DrmPipeline::revertPendingChanges()
pending = m_next;
}
bool DrmPipeline::pageflipPending() const
{
return m_pageflipPending;
}
bool DrmPipeline::modesetPresentPending() const
{
return m_modesetPresentPending;
}
DrmCrtc *DrmPipeline::currentCrtc() const
{
return m_current.crtc;
}
DrmGammaRamp::DrmGammaRamp(DrmGpu *gpu, const GammaRamp &lut)
: lut(lut)
, size(lut.size())

View file

@ -15,6 +15,7 @@
#include <QSharedPointer>
#include <xf86drmMode.h>
#include <chrono>
#include "drm_object_plane.h"
#include "renderloop_p.h"
@ -54,6 +55,7 @@ public:
bool present(const QSharedPointer<DrmBuffer> &buffer);
bool needsModeset() const;
bool needsCommit() const;
void prepareModeset();
void applyPendingChanges();
void revertPendingChanges();
@ -65,9 +67,12 @@ public:
QPoint cursorPos() const;
DrmConnector *connector() const;
DrmCrtc *currentCrtc() const;
DrmGpu *gpu() const;
void pageFlipped();
void pageFlipped(std::chrono::nanoseconds timestamp);
bool pageflipPending() const;
bool modesetPresentPending() const;
void printDebugInfo() const;
QSize sourceSize() const;
void updateProperties();
@ -92,7 +97,8 @@ public:
enum class CommitMode {
Test,
Commit
Commit,
CommitModeset
};
Q_ENUM(CommitMode);
static bool commitPipelines(const QVector<DrmPipeline*> &pipelines, CommitMode mode);
@ -111,6 +117,8 @@ private:
QSharedPointer<DrmBuffer> m_primaryBuffer;
QSharedPointer<DrmBuffer> m_oldTestBuffer;
bool m_pageflipPending = false;
bool m_modesetPresentPending = false;
QMap<uint32_t, QVector<uint64_t>> m_formats;
int m_lastFlags = 0;

View file

@ -581,11 +581,7 @@ void EglGbmBackend::endFrame(AbstractOutput *drmOutput, const QRegion &renderedR
const QRegion dirty = damagedRegion.intersected(output.output->geometry());
QSharedPointer<DrmBuffer> buffer = endFrameWithBuffer(drmOutput, dirty);
if (!buffer || !output.output->present(buffer, dirty)) {
RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(output.output->renderLoop());
renderLoopPrivate->notifyFrameFailed();
return;
}
output.output->present(buffer, dirty);
}
void EglGbmBackend::updateBufferAge(Output &output, const QRegion &dirty)

View file

@ -78,10 +78,7 @@ void DrmQPainterBackend::endFrame(AbstractOutput *output, const QRegion &damage)
QSharedPointer<DrmDumbBuffer> back = rendererOutput.swapchain->currentBuffer();
rendererOutput.swapchain->releaseBuffer(back);
if (!drmOutput->present(back, drmOutput->geometry())) {
RenderLoopPrivate *renderLoopPrivate = RenderLoopPrivate::get(drmOutput->renderLoop());
renderLoopPrivate->notifyFrameFailed();
}
drmOutput->present(back, drmOutput->geometry());
rendererOutput.damageJournal.add(damage);
}