Commit b50f7478 authored by Xaver Hugl's avatar Xaver Hugl Committed by David Edmundson
Browse files

Multi-GPU output support on Wayland

This commit sets up udev and the DrmBackend to list and use all GPUs.

BUG: 425586
BUG: 417323
parent 1f6e615e
......@@ -11,6 +11,7 @@ set(DRM_SOURCES
logging.cpp
scene_qpainter_drm_backend.cpp
screens_drm.cpp
drm_gpu.cpp
)
if (HAVE_GBM)
......
......@@ -47,6 +47,8 @@
#include <xf86drmMode.h>
#include <libdrm/drm_mode.h>
#include "drm_gpu.h"
#ifndef DRM_CAP_CURSOR_WIDTH
#define DRM_CAP_CURSOR_WIDTH 0x8
#endif
......@@ -77,21 +79,12 @@ DrmBackend::DrmBackend(QObject *parent)
DrmBackend::~DrmBackend()
{
#if HAVE_GBM
if (m_gbmDevice) {
gbm_device_destroy(m_gbmDevice);
}
#endif
if (m_fd >= 0) {
if (m_gpus.size() > 0) {
// wait for pageflips
while (m_pageFlipsPending != 0) {
QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents);
}
qDeleteAll(m_planes);
qDeleteAll(m_crtcs);
qDeleteAll(m_connectors);
close(m_fd);
qDeleteAll(m_gpus);
}
}
......@@ -243,71 +236,45 @@ void DrmBackend::pageFlipHandler(int fd, unsigned int frame, unsigned int sec, u
void DrmBackend::openDrm()
{
connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, &DrmBackend::activate);
UdevDevice::Ptr device = m_udev->primaryGpu();
if (!device) {
std::vector<UdevDevice::Ptr> devices = m_udev->listGPUs();
if (devices.size() == 0) {
qCWarning(KWIN_DRM) << "Did not find a GPU";
return;
}
m_devNode = qEnvironmentVariableIsSet("KWIN_DRM_DEVICE_NODE") ? qgetenv("KWIN_DRM_DEVICE_NODE") : QByteArray(device->devNode());
int fd = LogindIntegration::self()->takeDevice(m_devNode.constData());
if (fd < 0) {
qCWarning(KWIN_DRM) << "failed to open drm device at" << m_devNode;
return;
}
m_fd = fd;
m_active = true;
QSocketNotifier *notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
connect(notifier, &QSocketNotifier::activated, this,
[this] {
if (!LogindIntegration::self()->isActiveSession()) {
return;
}
drmEventContext e;
memset(&e, 0, sizeof e);
e.version = KWIN_DRM_EVENT_CONTEXT_VERSION;
e.page_flip_handler = pageFlipHandler;
drmHandleEvent(m_fd, &e);
for (unsigned int gpu_index = 0; gpu_index < devices.size(); gpu_index++) {
auto device = std::move(devices.at(gpu_index));
auto devNode = QByteArray(device->devNode());
int fd = LogindIntegration::self()->takeDevice(devNode.constData());
if (fd < 0) {
qCWarning(KWIN_DRM) << "failed to open drm device at" << devNode;
return;
}
);
m_drmId = device->sysNum();
// trying to activate Atomic Mode Setting (this means also Universal Planes)
if (!qEnvironmentVariableIsSet("KWIN_DRM_NO_AMS")) {
if (drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0) {
qCDebug(KWIN_DRM) << "Using Atomic Mode Setting.";
m_atomicModeSetting = true;
DrmScopedPointer<drmModePlaneRes> planeResources(drmModeGetPlaneResources(m_fd));
if (!planeResources) {
qCWarning(KWIN_DRM) << "Failed to get plane resources. Falling back to legacy mode";
m_atomicModeSetting = false;
}
if (m_atomicModeSetting) {
qCDebug(KWIN_DRM) << "Number of planes:" << planeResources->count_planes;
// create the plane objects
for (unsigned int i = 0; i < planeResources->count_planes; ++i) {
DrmScopedPointer<drmModePlane> kplane(drmModeGetPlane(m_fd, planeResources->planes[i]));
DrmPlane *p = new DrmPlane(kplane->plane_id, m_fd);
if (p->atomicInit()) {
m_planes << p;
if (p->type() == DrmPlane::TypeIndex::Overlay) {
m_overlayPlanes << p;
}
} else {
delete p;
}
}
if (m_planes.isEmpty()) {
qCWarning(KWIN_DRM) << "Failed to create any plane. Falling back to legacy mode";
m_atomicModeSetting = false;
m_active = true;
QSocketNotifier *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
connect(notifier, &QSocketNotifier::activated, this,
[fd] {
if (!LogindIntegration::self()->isActiveSession()) {
return;
}
drmEventContext e;
memset(&e, 0, sizeof e);
e.version = KWIN_DRM_EVENT_CONTEXT_VERSION;
e.page_flip_handler = pageFlipHandler;
drmHandleEvent(fd, &e);
}
} else {
qCWarning(KWIN_DRM) << "drmSetClientCap for Atomic Mode Setting failed. Using legacy mode.";
}
);
DrmGpu *gpu = new DrmGpu(this, devNode, fd, device->sysNum());
connect(gpu, &DrmGpu::outputAdded, this, &DrmBackend::addOutput);
connect(gpu, &DrmGpu::outputRemoved, this, &DrmBackend::removeOutput);
m_gpus.append(gpu);
break;
}
// trying to activate Atomic Mode Setting (this means also Universal Planes)
if (!qEnvironmentVariableIsSet("KWIN_DRM_NO_AMS")) {
for (auto gpu : m_gpus)
gpu->tryAMS();
}
initCursor();
......@@ -317,7 +284,7 @@ void DrmBackend::openDrm()
if (m_outputs.isEmpty()) {
qCDebug(KWIN_DRM) << "No connected outputs found on startup.";
}
// setup udevMonitor
if (m_udevMonitor) {
m_udevMonitor->filterSubsystemDevType("drm");
......@@ -330,7 +297,14 @@ void DrmBackend::openDrm()
if (!device) {
return;
}
if (device->sysNum() != m_drmId) {
bool drm = false;
for (auto gpu : m_gpus) {
if (gpu->drmId() == device->sysNum()) {
drm = true;
break;
}
}
if (!drm) {
return;
}
if (device->hasProperty("HOTPLUG", "1")) {
......@@ -346,176 +320,34 @@ void DrmBackend::openDrm()
setReady(true);
}
bool DrmBackend::updateOutputs()
void DrmBackend::addOutput(DrmOutput *o)
{
if (m_fd < 0) {
return false;
}
DrmScopedPointer<drmModeRes> resources(drmModeGetResources(m_fd));
if (!resources) {
qCWarning(KWIN_DRM) << "drmModeGetResources failed";
return false;
}
auto oldConnectors = m_connectors;
for (int i = 0; i < resources->count_connectors; ++i) {
const uint32_t currentConnector = resources->connectors[i];
auto it = std::find_if(m_connectors.constBegin(), m_connectors.constEnd(), [currentConnector] (DrmConnector *c) { return c->id() == currentConnector; });
if (it == m_connectors.constEnd()) {
auto c = new DrmConnector(currentConnector, m_fd);
if (m_atomicModeSetting && !c->atomicInit()) {
delete c;
continue;
}
m_connectors << c;
} else {
oldConnectors.removeOne(*it);
}
}
auto oldCrtcs = m_crtcs;
for (int i = 0; i < resources->count_crtcs; ++i) {
const uint32_t currentCrtc = resources->crtcs[i];
auto it = std::find_if(m_crtcs.constBegin(), m_crtcs.constEnd(), [currentCrtc] (DrmCrtc *c) { return c->id() == currentCrtc; });
if (it == m_crtcs.constEnd()) {
auto c = new DrmCrtc(currentCrtc, this, i);
if (m_atomicModeSetting && !c->atomicInit()) {
delete c;
continue;
}
m_crtcs << c;
} else {
oldCrtcs.removeOne(*it);
}
}
for (auto c : qAsConst(oldConnectors)) {
m_connectors.removeOne(c);
}
for (auto c : qAsConst(oldCrtcs)) {
m_crtcs.removeOne(c);
}
QVector<DrmOutput*> connectedOutputs;
QVector<DrmConnector*> pendingConnectors;
// split up connected connectors in already or not yet assigned ones
for (DrmConnector *con : qAsConst(m_connectors)) {
if (!con->isConnected()) {
continue;
}
if (DrmOutput *o = findOutput(con->id())) {
connectedOutputs << o;
} else {
pendingConnectors << con;
}
}
// check for outputs which got removed
QVector<DrmOutput*> removedOutputs;
auto it = m_outputs.begin();
while (it != m_outputs.end()) {
if (connectedOutputs.contains(*it)) {
it++;
continue;
}
DrmOutput *removed = *it;
it = m_outputs.erase(it);
m_enabledOutputs.removeOne(removed);
emit outputRemoved(removed);
removedOutputs.append(removed);
}
// now check new connections
for (DrmConnector *con : qAsConst(pendingConnectors)) {
DrmScopedPointer<drmModeConnector> connector(drmModeGetConnector(m_fd, con->id()));
if (!connector) {
continue;
}
if (connector->count_modes == 0) {
continue;
}
bool outputDone = false;
QVector<uint32_t> encoders = con->encoders();
for (auto encId : qAsConst(encoders)) {
DrmScopedPointer<drmModeEncoder> encoder(drmModeGetEncoder(m_fd, encId));
if (!encoder) {
continue;
}
for (DrmCrtc *crtc : qAsConst(m_crtcs)) {
if (!(encoder->possible_crtcs & (1 << crtc->resIndex()))) {
continue;
}
// check if crtc isn't used yet -- currently we don't allow multiple outputs on one crtc (cloned mode)
auto it = std::find_if(connectedOutputs.constBegin(), connectedOutputs.constEnd(),
[crtc] (DrmOutput *o) {
return o->m_crtc == crtc;
}
);
if (it != connectedOutputs.constEnd()) {
continue;
}
// we found a suitable encoder+crtc
// TODO: we could avoid these lib drm calls if we store all struct data in DrmCrtc and DrmConnector in the beginning
DrmScopedPointer<drmModeCrtc> modeCrtc(drmModeGetCrtc(m_fd, crtc->id()));
if (!modeCrtc) {
continue;
}
DrmOutput *output = new DrmOutput(this);
con->setOutput(output);
output->m_conn = con;
crtc->setOutput(output);
output->m_crtc = crtc;
if (modeCrtc->mode_valid) {
output->m_mode = modeCrtc->mode;
} else {
output->m_mode = connector->modes[0];
}
qCDebug(KWIN_DRM) << "For new output use mode " << output->m_mode.name;
m_outputs.append(o);
m_enabledOutputs.append(o);
emit o->gpu()->outputEnabled(o);
}
if (!output->init(connector.data())) {
qCWarning(KWIN_DRM) << "Failed to create output for connector " << con->id();
delete output;
continue;
}
if (!output->initCursor(m_cursorSize)) {
setSoftWareCursor(true);
}
qCDebug(KWIN_DRM) << "Found new output with uuid" << output->uuid();
void DrmBackend::removeOutput(DrmOutput *o)
{
emit o->gpu()->outputDisabled(o);
m_outputs.removeOne(o);
m_enabledOutputs.removeOne(o);
}
connectedOutputs << output;
emit outputAdded(output);
outputDone = true;
break;
}
if (outputDone) {
break;
}
}
bool DrmBackend::updateOutputs()
{
if (m_gpus.size() == 0) {
return false;
}
std::sort(connectedOutputs.begin(), connectedOutputs.end(), [] (DrmOutput *a, DrmOutput *b) { return a->m_conn->id() < b->m_conn->id(); });
m_outputs = connectedOutputs;
m_enabledOutputs = connectedOutputs;
for (auto gpu : m_gpus)
gpu->updateOutputs();
std::sort(m_outputs.begin(), m_outputs.end(), [] (DrmOutput *a, DrmOutput *b) { return a->m_conn->id() < b->m_conn->id(); });
readOutputsConfiguration();
updateOutputsEnabled();
if (!m_outputs.isEmpty()) {
emit screensQueried();
}
for(DrmOutput* removedOutput : removedOutputs) {
removedOutput->teardown();
removedOutput->m_crtc = nullptr;
removedOutput->m_conn = nullptr;
}
qDeleteAll(oldConnectors);
qDeleteAll(oldCrtcs);
return true;
}
......@@ -574,12 +406,12 @@ void DrmBackend::enableOutput(DrmOutput *output, bool enable)
if (enable) {
Q_ASSERT(!m_enabledOutputs.contains(output));
m_enabledOutputs << output;
emit outputAdded(output);
emit output->gpu()->outputEnabled(output);
} else {
Q_ASSERT(m_enabledOutputs.contains(output));
m_enabledOutputs.removeOne(output);
Q_ASSERT(!m_enabledOutputs.contains(output));
emit outputRemoved(output);
emit output->gpu()->outputDisabled(output);
}
updateOutputsEnabled();
checkOutputsAreOn();
......@@ -640,6 +472,7 @@ void DrmBackend::initCursor()
if (m_cursorEnabled) {
if (!(*it)->showCursor()) {
setSoftWareCursor(true);
break;
}
} else {
(*it)->hideCursor();
......@@ -649,15 +482,17 @@ void DrmBackend::initCursor()
);
uint64_t capability = 0;
QSize cursorSize;
if (drmGetCap(m_fd, DRM_CAP_CURSOR_WIDTH, &capability) == 0) {
cursorSize.setWidth(capability);
} else {
cursorSize.setWidth(64);
cursorSize.setWidth(64);
for (auto gpu : m_gpus) {
if (drmGetCap(gpu->fd(), DRM_CAP_CURSOR_WIDTH, &capability) == 0) {
cursorSize.setWidth(capability);
}
}
if (drmGetCap(m_fd, DRM_CAP_CURSOR_HEIGHT, &capability) == 0) {
cursorSize.setHeight(capability);
} else {
cursorSize.setHeight(64);
cursorSize.setHeight(64);
for (auto gpu : m_gpus) {
if (drmGetCap(gpu->fd(), DRM_CAP_CURSOR_HEIGHT, &capability) == 0) {
cursorSize.setHeight(capability);
}
}
m_cursorSize = cursorSize;
// now we have screens and can set cursors, so start tracking
......@@ -735,7 +570,7 @@ Screens *DrmBackend::createScreens(QObject *parent)
QPainterBackend *DrmBackend::createQPainterBackend()
{
m_deleteBufferAfterPageFlip = false;
return new DrmQPainterBackend(this);
return new DrmQPainterBackend(this, m_gpus.at(0));
}
OpenGLBackend *DrmBackend::createOpenGLBackend()
......@@ -743,32 +578,18 @@ OpenGLBackend *DrmBackend::createOpenGLBackend()
#if HAVE_EGL_STREAMS
if (m_useEglStreams) {
m_deleteBufferAfterPageFlip = false;
return new EglStreamBackend(this);
return new EglStreamBackend(this, m_gpus.at(0));
}
#endif
#if HAVE_GBM
m_deleteBufferAfterPageFlip = true;
return new EglGbmBackend(this);
return new EglGbmBackend(this, m_gpus.at(0));
#else
return Platform::createOpenGLBackend();
#endif
}
DrmDumbBuffer *DrmBackend::createBuffer(const QSize &size)
{
DrmDumbBuffer *b = new DrmDumbBuffer(m_fd, size);
return b;
}
#if HAVE_GBM
DrmSurfaceBuffer *DrmBackend::createBuffer(const std::shared_ptr<GbmSurface> &surface)
{
DrmSurfaceBuffer *b = new DrmSurfaceBuffer(m_fd, surface);
return b;
}
#endif
void DrmBackend::updateOutputsEnabled()
{
bool enabled = false;
......@@ -801,7 +622,9 @@ QString DrmBackend::supportInformation() const
s.nospace();
s << "Name: " << "DRM" << Qt::endl;
s << "Active: " << m_active << Qt::endl;
s << "Atomic Mode Setting: " << m_atomicModeSetting << Qt::endl;
for (int g = 0; g < m_gpus.size(); g++) {
s << "Atomic Mode Setting on GPU " << g << ": " << m_gpus.at(g)->atomicModeSetting() << Qt::endl;
}
#if HAVE_EGL_STREAMS
s << "Using EGL Streams: " << m_useEglStreams << Qt::endl;
#endif
......@@ -811,7 +634,10 @@ QString DrmBackend::supportInformation() const
DmaBufTexture *DrmBackend::createDmaBufTexture(const QSize &size)
{
#if HAVE_GBM
return GbmDmaBuf::createBuffer(size, m_gbmDevice);
// gpu_index is a fixed 0 here
// as the first GPU is assumed to always be the one used for scene rendering
// and this function is only used for Pipewire
return GbmDmaBuf::createBuffer(size, m_gpus.at(0)->gbmDevice());
#else
return nullptr;
#endif
......
......@@ -43,6 +43,7 @@ class DrmCrtc;
class DrmConnector;
class GbmSurface;
class Cursor;
class DrmGpu;
class KWIN_EXPORT DrmBackend : public Platform
{
......@@ -60,16 +61,9 @@ public:
void init() override;
void prepareShutdown() override;
DrmDumbBuffer *createBuffer(const QSize &size);
#if HAVE_GBM
DrmSurfaceBuffer *createBuffer(const std::shared_ptr<GbmSurface> &surface);
#endif
bool present(DrmBuffer *buffer, DrmOutput *output);
int fd() const {
return m_fd;
}
Outputs outputs() const override;
Outputs enabledOutputs() const override;
QVector<DrmOutput*> drmOutputs() const {
......@@ -81,13 +75,6 @@ public:
void enableOutput(DrmOutput *output, bool enable);
QVector<DrmPlane*> planes() const {
return m_planes;
}
QVector<DrmPlane*> overlayPlanes() const {
return m_overlayPlanes;
}
void createDpmsFilter();
void checkOutputsAreOn();
......@@ -95,21 +82,6 @@ public:
bool deleteBufferAfterPageFlip() const {
return m_deleteBufferAfterPageFlip;
}
// returns use of AMS, default is not/legacy
bool atomicModeSetting() const {
return m_atomicModeSetting;
}
void setGbmDevice(gbm_device *device) {
m_gbmDevice = device;
}
gbm_device *gbmDevice() const {
return m_gbmDevice;
}
QByteArray devNode() const {
return m_devNode;
}
#if HAVE_EGL_STREAMS
bool useEglStreams() const {
......@@ -128,22 +100,13 @@ public:
public Q_SLOTS:
void turnOutputsOn();
Q_SIGNALS:
/**
* Emitted whenever an output is removed/disabled
*/
void outputRemoved(KWin::DrmOutput *output);
/**
* Emitted whenever an output is added/enabled
*/
void outputAdded(KWin::DrmOutput *output);
protected:
void doHideCursor() override;
void doShowCursor() override;
private:
friend class DrmGpu;
void addOutput(DrmOutput* output);
void removeOutput(DrmOutput* output);
static void pageFlipHandler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *data);
void openDrm();
void activate(bool active);
......@@ -161,32 +124,22 @@ private:
void updateOutputsEnabled();
QScopedPointer<Udev> m_udev;
QScopedPointer<UdevMonitor> m_udevMonitor;
int m_fd = -1;
int m_drmId = 0;
// all crtcs
QVector<DrmCrtc*> m_crtcs;
// all connectors
QVector<DrmConnector*> m_connectors;
// active output pipelines (planes + crtc + encoder + connector)
QVector<DrmOutput*> m_outputs;
// active and enabled pipelines (above + wl_output)
QVector<DrmOutput*> m_enabledOutputs;
bool m_deleteBufferAfterPageFlip;
bool m_atomicModeSetting = false;
bool m_cursorEnabled = false;
QSize m_cursorSize;
int m_pageFlipsPending = 0;
bool m_active = false;
QByteArray m_devNode;
#if HAVE_EGL_STREAMS
bool m_useEglStreams = false;
#endif
// all available planes: primarys, cursors and overlays
QVector<DrmPlane*> m_planes;
QVector<DrmPlane*> m_overlayPlanes;
QVector<DrmGpu*> m_gpus;
QScopedPointer<DpmsInputEventFilter> m_dpmsFilter;
gbm_device *m_gbmDevice = nullptr;
};
......
/*
KWin - the KDE window manager