eglfs_kms: Add headless mode for DRM render nodes

Attempting to switch /dev/dri/cardX to /dev/dri/renderDY is futile
on its own now since many output-related drm operations fail and we
eventually crash.

Add a new headless mode that skips the real screen stuff and registers
a fairly dummy, headless screen, does not bother with the mouse cursor,
and disallows rendering to the screen via a QWindow (while keeping the
actual rendering still fully functional).

Such applications will not need any special privileges and will run even
if there is a DRM master (X11, Wayland compositor) active.

For example the configuration can look like this:

{
    "device": "/dev/dri/renderD128",
    "headless": "1024x768"
}

After this applications have two choices to perform offscreen
rendering:

1. Use an ordinary window (and its default framebuffer, meaning the
gbm_surface), e.g. a QOpenGLWindow subclass

  MyOpenGLWindow w;
  w.show(); // will not actually show on screen
  w.grabFramebuffer().save("output.png");

Note that there is no vsync-based throttling. Also note that windows are
still sized to match the screen size, hence the need for specifying a size
in the headless property.

2. Or the typical offscreen approach with an extra FBO

  QOffscreenSurface s;
  s.setFormat(ctx.format());
  s.create();
  ctx.makeCurrent(&s0;
  QOpenGLFramebufferObject fbo(1024, 768);
  fbo.bind();
  ctx.functions()->glClearColor(1, 0, 0, 1);
  ctx.functions()->glClear(GL_COLOR_BUFFER_BIT);
  fbo.toImage().save("output.png");
  ctx.doneCurrent();

Task-number: QTBUG-62262
Change-Id: Ic1dbfa2b27b223bd5ef8ba36b665f0f61abf4f06
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
This commit is contained in:
Laszlo Agocs 2017-08-04 13:31:20 +02:00
parent f2289bbcbb
commit cf24d20385
8 changed files with 83 additions and 27 deletions

View File

@ -1,6 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2016 The Qt Company Ltd. ** Copyright (C) 2017 The Qt Company Ltd.
** Copyright (C) 2016 Pelagicore AG ** Copyright (C) 2016 Pelagicore AG
** Copyright (C) 2015 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com> ** Copyright (C) 2015 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
** Contact: https://www.qt.io/licensing/ ** Contact: https://www.qt.io/licensing/
@ -179,7 +179,7 @@ QPlatformScreen *QKmsDevice::createScreenForConnector(drmModeResPtr resources,
drmModeModeInfo configurationModeline; drmModeModeInfo configurationModeline;
auto userConfig = m_screenConfig->outputSettings(); auto userConfig = m_screenConfig->outputSettings();
auto userConnectorConfig = userConfig.value(QString::fromUtf8(connectorName)); QVariantMap userConnectorConfig = userConfig.value(QString::fromUtf8(connectorName));
// default to the preferred mode unless overridden in the config // default to the preferred mode unless overridden in the config
const QByteArray mode = userConnectorConfig.value(QStringLiteral("mode"), QStringLiteral("preferred")) const QByteArray mode = userConnectorConfig.value(QStringLiteral("mode"), QStringLiteral("preferred"))
.toByteArray().toLower(); .toByteArray().toLower();
@ -489,9 +489,22 @@ static bool orderedScreenLessThan(const OrderedScreen &a, const OrderedScreen &b
void QKmsDevice::createScreens() void QKmsDevice::createScreens()
{ {
// Headless mode using a render node: cannot do any output related DRM
// stuff. Skip it all and register a dummy screen.
if (m_screenConfig->headless()) {
QPlatformScreen *screen = createHeadlessScreen();
if (screen) {
qCDebug(qLcKmsDebug, "Headless mode enabled");
registerScreen(screen, true, QPoint(0, 0), QList<QPlatformScreen *>());
return;
} else {
qWarning("QKmsDevice: Requested headless mode without support in the backend. Request is ignored.");
}
}
drmModeResPtr resources = drmModeGetResources(m_dri_fd); drmModeResPtr resources = drmModeGetResources(m_dri_fd);
if (!resources) { if (!resources) {
qWarning("drmModeGetResources failed"); qErrnoWarning(errno, "drmModeGetResources failed");
return; return;
} }
@ -597,6 +610,12 @@ void QKmsDevice::createScreens()
} }
} }
QPlatformScreen *QKmsDevice::createHeadlessScreen()
{
// headless mode not supported by default
return nullptr;
}
// not all subclasses support screen cloning // not all subclasses support screen cloning
void QKmsDevice::registerScreenCloning(QPlatformScreen *screen, void QKmsDevice::registerScreenCloning(QPlatformScreen *screen,
QPlatformScreen *screenThisScreenClones, QPlatformScreen *screenThisScreenClones,
@ -628,7 +647,8 @@ QKmsScreenConfig *QKmsDevice::screenConfig() const
} }
QKmsScreenConfig::QKmsScreenConfig() QKmsScreenConfig::QKmsScreenConfig()
: m_hwCursor(true) : m_headless(false)
, m_hwCursor(true)
, m_separateScreens(false) , m_separateScreens(false)
, m_pbuffers(false) , m_pbuffers(false)
, m_virtualDesktopLayout(VirtualDesktopLayoutHorizontal) , m_virtualDesktopLayout(VirtualDesktopLayoutHorizontal)
@ -663,6 +683,16 @@ void QKmsScreenConfig::loadConfig()
const QJsonObject object = doc.object(); const QJsonObject object = doc.object();
const QString headlessStr = object.value(QLatin1String("headless")).toString();
const QByteArray headless = headlessStr.toUtf8();
QSize headlessSize;
if (sscanf(headless.constData(), "%dx%d", &headlessSize.rwidth(), &headlessSize.rheight()) == 2) {
m_headless = true;
m_headlessSize = headlessSize;
} else {
m_headless = false;
}
m_hwCursor = object.value(QLatin1String("hwcursor")).toBool(m_hwCursor); m_hwCursor = object.value(QLatin1String("hwcursor")).toBool(m_hwCursor);
m_pbuffers = object.value(QLatin1String("pbuffers")).toBool(m_pbuffers); m_pbuffers = object.value(QLatin1String("pbuffers")).toBool(m_pbuffers);
m_devicePath = object.value(QLatin1String("device")).toString(); m_devicePath = object.value(QLatin1String("device")).toString();
@ -694,6 +724,7 @@ void QKmsScreenConfig::loadConfig()
} }
qCDebug(qLcKmsDebug) << "Requested configuration (some settings may be ignored):\n" qCDebug(qLcKmsDebug) << "Requested configuration (some settings may be ignored):\n"
<< "\theadless:" << m_headless << "\n"
<< "\thwcursor:" << m_hwCursor << "\n" << "\thwcursor:" << m_hwCursor << "\n"
<< "\tpbuffers:" << m_pbuffers << "\n" << "\tpbuffers:" << m_pbuffers << "\n"
<< "\tseparateScreens:" << m_separateScreens << "\n" << "\tseparateScreens:" << m_separateScreens << "\n"

View File

@ -77,6 +77,8 @@ public:
QString devicePath() const { return m_devicePath; } QString devicePath() const { return m_devicePath; }
bool headless() const { return m_headless; }
QSize headlessSize() const { return m_headlessSize; }
bool hwCursor() const { return m_hwCursor; } bool hwCursor() const { return m_hwCursor; }
bool separateScreens() const { return m_separateScreens; } bool separateScreens() const { return m_separateScreens; }
bool supportsPBuffers() const { return m_pbuffers; } bool supportsPBuffers() const { return m_pbuffers; }
@ -88,6 +90,8 @@ private:
void loadConfig(); void loadConfig();
QString m_devicePath; QString m_devicePath;
bool m_headless;
QSize m_headlessSize;
bool m_hwCursor; bool m_hwCursor;
bool m_separateScreens; bool m_separateScreens;
bool m_pbuffers; bool m_pbuffers;
@ -98,21 +102,21 @@ private:
struct QKmsOutput struct QKmsOutput
{ {
QString name; QString name;
uint32_t connector_id; uint32_t connector_id = 0;
uint32_t crtc_id; uint32_t crtc_id = 0;
QSizeF physical_size; QSizeF physical_size;
int preferred_mode; // index of preferred mode in list below int preferred_mode = -1; // index of preferred mode in list below
int mode; // index of selected mode in list below int mode = -1; // index of selected mode in list below
bool mode_set; bool mode_set = false;
drmModeCrtcPtr saved_crtc; drmModeCrtcPtr saved_crtc = nullptr;
QList<drmModeModeInfo> modes; QList<drmModeModeInfo> modes;
int subpixel; int subpixel = DRM_MODE_SUBPIXEL_UNKNOWN;
drmModePropertyPtr dpms_prop; drmModePropertyPtr dpms_prop = nullptr;
drmModePropertyBlobPtr edid_blob; drmModePropertyBlobPtr edid_blob = nullptr;
bool wants_plane; bool wants_plane = false;
uint32_t plane_id; uint32_t plane_id = 0;
bool plane_set; bool plane_set = false;
uint32_t drm_format; uint32_t drm_format = DRM_FORMAT_XRGB8888;
QString clone_source; QString clone_source;
void restoreMode(QKmsDevice *device); void restoreMode(QKmsDevice *device);
@ -147,6 +151,7 @@ public:
protected: protected:
virtual QPlatformScreen *createScreen(const QKmsOutput &output) = 0; virtual QPlatformScreen *createScreen(const QKmsOutput &output) = 0;
virtual QPlatformScreen *createHeadlessScreen();
virtual void registerScreenCloning(QPlatformScreen *screen, virtual void registerScreenCloning(QPlatformScreen *screen,
QPlatformScreen *screenThisScreenClones, QPlatformScreen *screenThisScreenClones,
const QVector<QPlatformScreen *> &screensCloningThisScreen); const QVector<QPlatformScreen *> &screensCloningThisScreen);

View File

@ -151,7 +151,7 @@ void QEglFSKmsGbmDevice::handleDrmEvent()
QPlatformScreen *QEglFSKmsGbmDevice::createScreen(const QKmsOutput &output) QPlatformScreen *QEglFSKmsGbmDevice::createScreen(const QKmsOutput &output)
{ {
QEglFSKmsGbmScreen *screen = new QEglFSKmsGbmScreen(this, output); QEglFSKmsGbmScreen *screen = new QEglFSKmsGbmScreen(this, output, false);
if (!m_globalCursor && screenConfig()->hwCursor()) { if (!m_globalCursor && screenConfig()->hwCursor()) {
qCDebug(qLcEglfsKmsDebug, "Creating new global GBM mouse cursor"); qCDebug(qLcEglfsKmsDebug, "Creating new global GBM mouse cursor");
@ -161,6 +161,11 @@ QPlatformScreen *QEglFSKmsGbmDevice::createScreen(const QKmsOutput &output)
return screen; return screen;
} }
QPlatformScreen *QEglFSKmsGbmDevice::createHeadlessScreen()
{
return new QEglFSKmsGbmScreen(this, QKmsOutput(), true);
}
void QEglFSKmsGbmDevice::registerScreenCloning(QPlatformScreen *screen, void QEglFSKmsGbmDevice::registerScreenCloning(QPlatformScreen *screen,
QPlatformScreen *screenThisScreenClones, QPlatformScreen *screenThisScreenClones,
const QVector<QPlatformScreen *> &screensCloningThisScreen) const QVector<QPlatformScreen *> &screensCloningThisScreen)

View File

@ -68,6 +68,7 @@ public:
void handleDrmEvent(); void handleDrmEvent();
QPlatformScreen *createScreen(const QKmsOutput &output) override; QPlatformScreen *createScreen(const QKmsOutput &output) override;
QPlatformScreen *createHeadlessScreen() override;
void registerScreenCloning(QPlatformScreen *screen, void registerScreenCloning(QPlatformScreen *screen,
QPlatformScreen *screenThisScreenClones, QPlatformScreen *screenThisScreenClones,
const QVector<QPlatformScreen *> &screensCloningThisScreen) override; const QVector<QPlatformScreen *> &screensCloningThisScreen) override;

View File

@ -109,8 +109,8 @@ QEglFSKmsGbmScreen::FrameBuffer *QEglFSKmsGbmScreen::framebufferForBufferObject(
return fb.take(); return fb.take();
} }
QEglFSKmsGbmScreen::QEglFSKmsGbmScreen(QKmsDevice *device, const QKmsOutput &output) QEglFSKmsGbmScreen::QEglFSKmsGbmScreen(QKmsDevice *device, const QKmsOutput &output, bool headless)
: QEglFSKmsScreen(device, output) : QEglFSKmsScreen(device, output, headless)
, m_gbm_surface(Q_NULLPTR) , m_gbm_surface(Q_NULLPTR)
, m_gbm_bo_current(Q_NULLPTR) , m_gbm_bo_current(Q_NULLPTR)
, m_gbm_bo_next(Q_NULLPTR) , m_gbm_bo_next(Q_NULLPTR)
@ -131,6 +131,8 @@ QEglFSKmsGbmScreen::~QEglFSKmsGbmScreen()
QPlatformCursor *QEglFSKmsGbmScreen::cursor() const QPlatformCursor *QEglFSKmsGbmScreen::cursor() const
{ {
QKmsScreenConfig *config = device()->screenConfig(); QKmsScreenConfig *config = device()->screenConfig();
if (config->headless())
return nullptr;
if (config->hwCursor()) { if (config->hwCursor()) {
if (!config->separateScreens()) if (!config->separateScreens())
return static_cast<QEglFSKmsGbmDevice *>(device())->globalCursor(); return static_cast<QEglFSKmsGbmDevice *>(device())->globalCursor();
@ -240,10 +242,8 @@ void QEglFSKmsGbmScreen::ensureModeSet(uint32_t fb)
void QEglFSKmsGbmScreen::waitForFlip() void QEglFSKmsGbmScreen::waitForFlip()
{ {
if (m_cloneSource) { if (m_headless || m_cloneSource)
qWarning("Screen %s clones another screen. swapBuffers() not allowed.", qPrintable(name()));
return; return;
}
// Don't lock the mutex unless we actually need to // Don't lock the mutex unless we actually need to
if (!m_gbm_bo_next) if (!m_gbm_bo_next)
@ -256,6 +256,11 @@ void QEglFSKmsGbmScreen::waitForFlip()
void QEglFSKmsGbmScreen::flip() void QEglFSKmsGbmScreen::flip()
{ {
// For headless screen just return silently. It is not necessarily an error
// to end up here, so show no warnings.
if (m_headless)
return;
if (m_cloneSource) { if (m_cloneSource) {
qWarning("Screen %s clones another screen. swapBuffers() not allowed.", qPrintable(name())); qWarning("Screen %s clones another screen. swapBuffers() not allowed.", qPrintable(name()));
return; return;

View File

@ -54,7 +54,7 @@ class QEglFSKmsGbmCursor;
class QEglFSKmsGbmScreen : public QEglFSKmsScreen class QEglFSKmsGbmScreen : public QEglFSKmsScreen
{ {
public: public:
QEglFSKmsGbmScreen(QKmsDevice *device, const QKmsOutput &output); QEglFSKmsGbmScreen(QKmsDevice *device, const QKmsOutput &output, bool headless);
~QEglFSKmsGbmScreen(); ~QEglFSKmsGbmScreen();
QPlatformCursor *cursor() const override; QPlatformCursor *cursor() const override;

View File

@ -68,12 +68,13 @@ private:
QEglFSKmsScreen *m_screen; QEglFSKmsScreen *m_screen;
}; };
QEglFSKmsScreen::QEglFSKmsScreen(QKmsDevice *device, const QKmsOutput &output) QEglFSKmsScreen::QEglFSKmsScreen(QKmsDevice *device, const QKmsOutput &output, bool headless)
: QEglFSScreen(static_cast<QEglFSIntegration *>(QGuiApplicationPrivate::platformIntegration())->display()) : QEglFSScreen(static_cast<QEglFSIntegration *>(QGuiApplicationPrivate::platformIntegration())->display())
, m_device(device) , m_device(device)
, m_output(output) , m_output(output)
, m_powerState(PowerStateOn) , m_powerState(PowerStateOn)
, m_interruptHandler(new QEglFSKmsInterruptHandler(this)) , m_interruptHandler(new QEglFSKmsInterruptHandler(this))
, m_headless(headless)
{ {
m_siblings << this; // gets overridden later m_siblings << this; // gets overridden later
@ -109,6 +110,9 @@ void QEglFSKmsScreen::setVirtualPosition(const QPoint &pos)
// geometry() calls rawGeometry() and may apply additional transforms. // geometry() calls rawGeometry() and may apply additional transforms.
QRect QEglFSKmsScreen::rawGeometry() const QRect QEglFSKmsScreen::rawGeometry() const
{ {
if (m_headless)
return QRect(QPoint(0, 0), m_device->screenConfig()->headlessSize());
const int mode = m_output.mode; const int mode = m_output.mode;
return QRect(m_pos.x(), m_pos.y(), return QRect(m_pos.x(), m_pos.y(),
m_output.modes[mode].hdisplay, m_output.modes[mode].hdisplay,
@ -177,7 +181,7 @@ Qt::ScreenOrientation QEglFSKmsScreen::orientation() const
QString QEglFSKmsScreen::name() const QString QEglFSKmsScreen::name() const
{ {
return m_output.name; return !m_headless ? m_output.name : QStringLiteral("qt_Headless");
} }
QString QEglFSKmsScreen::manufacturer() const QString QEglFSKmsScreen::manufacturer() const
@ -214,6 +218,9 @@ void QEglFSKmsScreen::restoreMode()
qreal QEglFSKmsScreen::refreshRate() const qreal QEglFSKmsScreen::refreshRate() const
{ {
if (m_headless)
return 60;
quint32 refresh = m_output.modes[m_output.mode].vrefresh; quint32 refresh = m_output.modes[m_output.mode].vrefresh;
return refresh > 0 ? refresh : 60; return refresh > 0 ? refresh : 60;
} }

View File

@ -56,7 +56,7 @@ class QEglFSKmsInterruptHandler;
class Q_EGLFS_EXPORT QEglFSKmsScreen : public QEglFSScreen class Q_EGLFS_EXPORT QEglFSKmsScreen : public QEglFSScreen
{ {
public: public:
QEglFSKmsScreen(QKmsDevice *device, const QKmsOutput &output); QEglFSKmsScreen(QKmsDevice *device, const QKmsOutput &output, bool headless = false);
~QEglFSKmsScreen(); ~QEglFSKmsScreen();
void setVirtualPosition(const QPoint &pos); void setVirtualPosition(const QPoint &pos);
@ -113,6 +113,8 @@ protected:
PowerState m_powerState; PowerState m_powerState;
QEglFSKmsInterruptHandler *m_interruptHandler; QEglFSKmsInterruptHandler *m_interruptHandler;
bool m_headless;
}; };
QT_END_NAMESPACE QT_END_NAMESPACE