580 changes: 580 additions & 0 deletions mythtv/libs/libmythui/platforms/mythdisplaymutter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,580 @@
// MythTV
#include "mythcorecontext.h"
#include "mythlogging.h"
#include "platforms/mythdisplaymutter.h"

#ifdef USING_DRM
#include "platforms/mythdrmdevice.h"
#endif

#ifdef USING_X11
#include "platforms/mythdisplayx11.h"
#include <X11/extensions/Xrandr.h> // always last
#endif

#define LOC QString("MutterDisp: ")

// ApplyConfiguration CRTC output
struct MythMutterCRTCOut
{
uint32_t id;
int32_t new_mode;
int32_t x;
int32_t y;
uint32_t transform;
QList<uint32_t> outputs;
MythMutterMap properties;
};

// ApplyConfiguration Outputs out
struct MythMutterOutputOut
{
uint32_t id;
MythMutterMap properties;
};

using MythMutterCRTCOutList = QList<MythMutterCRTCOut>;
Q_DECLARE_METATYPE(MythMutterCRTCOut);
Q_DECLARE_METATYPE(MythMutterCRTCOutList);
using MythMutterOutputOutList = QList<MythMutterOutputOut>;
Q_DECLARE_METATYPE(MythMutterOutputOut);
Q_DECLARE_METATYPE(MythMutterOutputOutList);

static QDBusArgument &operator<<(QDBusArgument& Argument, const MythMutterOutputOut& Output)
{
Argument.beginStructure();
Argument << Output.id << Output.properties;
Argument.endStructure();
return Argument;
}

static QDBusArgument &operator<<(QDBusArgument& Argument, const MythMutterOutputOutList& Outputs)
{
Argument.beginArray(qMetaTypeId<MythMutterOutputOut>());
for (auto & output : Outputs)
Argument << output;
Argument.endArray();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterOutputOut& Output)
{
Argument.beginStructure();
Argument >> Output.id >> Output.properties;
Argument.endStructure();
return Argument;
}

static QDBusArgument &operator<<(QDBusArgument& Argument, const MythMutterCRTCOut& CRTC)
{
Argument.beginStructure();
Argument << CRTC.id << CRTC.new_mode << CRTC.x << CRTC.y << CRTC.transform << CRTC.outputs << CRTC.properties;
Argument.endStructure();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterCRTCOut& CRTC)
{
Argument.beginStructure();
Argument >> CRTC.id >> CRTC.new_mode >> CRTC.x >> CRTC.y >> CRTC.transform >> CRTC.outputs >> CRTC.properties;
Argument.endStructure();
return Argument;
}

static QDBusArgument &operator<<(QDBusArgument& Argument, const MythMutterCRTCOutList& CRTCS)
{
Argument.beginArray(qMetaTypeId<MythMutterCRTCOut>());
for (auto & crtc : CRTCS)
Argument << crtc;
Argument.endArray();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterCRTC& CRTC)
{
Argument.beginStructure();
Argument >> CRTC.id >> CRTC.sys_id >> CRTC.x >> CRTC.y;
Argument >> CRTC.width >> CRTC.height >> CRTC.currentmode;
Argument >> CRTC.currenttransform >> CRTC.transforms >> CRTC.properties;
Argument.endStructure();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterCRTCList& CRTCS)
{
Argument.beginArray();
CRTCS.clear();

while (!Argument.atEnd())
{
MythMutterCRTC crtc;
Argument >> crtc;
CRTCS.append(crtc);
}

Argument.endArray();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterOutput& Output)
{
Argument.beginStructure();
Argument >> Output.id >> Output.sys_id >> Output.current_crtc >> Output.possible_crtcs;
Argument >> Output.name >> Output.modes >> Output.clones;
Argument >> Output.properties;
Argument.endStructure();
Output.serialnumber = QString();
Output.edid = QByteArray();
Output.widthmm = 0;
Output.heightmm = 0;
for (auto & property : Output.properties)
{
if (property.first == "serial")
Output.serialnumber = property.second.variant().toString();
if (property.first == "edid")
Output.edid = property.second.variant().toByteArray();
if (property.first == "width-mm")
Output.widthmm = property.second.variant().toInt();
if (property.first == "height-mm")
Output.heightmm = property.second.variant().toInt();
}
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterOutputList& Outputs)
{
Argument.beginArray();
Outputs.clear();

while (!Argument.atEnd())
{
MythMutterOutput output;
Argument >> output;
Outputs.append(output);
}

Argument.endArray();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterMode& Mode)
{
Argument.beginStructure();
Argument >> Mode.id >> Mode.sys_id >> Mode.width >> Mode.height;
Argument >> Mode.frequency >> Mode.flags;
Argument.endStructure();
return Argument;
}

static const QDBusArgument &operator>>(const QDBusArgument& Argument, MythMutterModeList& Modes)
{
Argument.beginArray();
Modes.clear();

while (!Argument.atEnd())
{
MythMutterMode mode;
Argument >> mode;
Modes.append(mode);
}

Argument.endArray();
return Argument;
}

/*! \brief Create a valid instance
*
* If org.gnome.Mutter.DisplayConfig is not available or the ApplyConfiguration
* method is not implemented, a null pointer is returned.
*/
MythDisplayMutter* MythDisplayMutter::Create()
{
static bool s_checked(false);
static bool s_available(false);

if (!s_checked)
{
s_checked = true;
qDBusRegisterMetaType<MythMutterCRTCOut>();
qDBusRegisterMetaType<MythMutterOutputOut>();
auto mutter = QDBusInterface(DISP_CONFIG_SERVICE, DISP_CONFIG_PATH,
DISP_CONFIG_SERVICE, QDBusConnection::sessionBus());

if (mutter.isValid())
{
// Some implementations do not implement ApplyConfiguration and there
// is no point in using this class without it
// N.B. Use a bogus serial here to ensure that if it is implemented,
// it will fail with org.freedesktop.DBus.AccessDenied
QDBusMessage res = mutter.call("GetResources");
QList<QVariant> args = res.arguments();
if ((res.signature() == DISP_CONFIG_SIG) || (args.size() == 6))
{
uint serial = args[0].toUInt() + 100;
MythMutterCRTCOutList crtcs;
MythMutterOutputOutList outputs;
QDBusArgument crtcsarg;
QDBusArgument outarg;
QDBusReply<void> reply = mutter.call(QLatin1String("ApplyConfiguration"),
serial, false,
QVariant::fromValue(crtcsarg << crtcs),
QVariant::fromValue(outarg << outputs));
if (reply.error().type() == QDBusError::UnknownMethod)
{
LOG(VB_GENERAL, LOG_WARNING, LOC +
"org.gnome.Mutter.DisplayConfig.ApplyConfiguration not implemented");
}
else
{
s_available = true;
}

}

if (!s_available)
LOG(VB_GENERAL, LOG_INFO, LOC + "Mutter.DisplayConfig not useable");
}
}

if (s_available)
{
auto result = new MythDisplayMutter();
if (result->IsValid())
return result;
delete result;
}

return nullptr;
}

/*! \class MythDisplayMutter
* \brief A subclass of MythDisplay using the org.gnome.Mutter.DisplayConfig DBUS interface
*
* This class is intended to be used on Linux installations that are running 'pure'
* Wayland desktops (i.e. not Wayland on top of X). When X11 is available,
* MythDisplayX11 is more suitable.
*
* If the interface is available but the 'ApplyConfiguration' method is not implemented,
* then MythDisplay should fallback to an alternative subclass - typically
* MythDisplayDRM (Wayland will be using DRM under the hood but MythTV will not
* have the necessary permissions to switch resolutions/refresh rates).
*
* \note Not all Wayland compositors will implement this interface - so it does
* not provide a universal solution for Wayland.
*/
MythDisplayMutter::MythDisplayMutter()
{
InitialiseInterface();
Initialise();
}

MythDisplayMutter::~MythDisplayMutter()
{
delete m_interface;
}

bool MythDisplayMutter::IsValid()
{
return m_interface != nullptr;
}

void MythDisplayMutter::MonitorsChanged()
{
LOG(VB_GENERAL, LOG_INFO, LOC + "Monitors changed");
UpdateResources();
}

bool MythDisplayMutter::UsingVideoModes()
{
if (gCoreContext)
return gCoreContext->GetBoolSetting("UseVideoModes", false);
return false;
}

const vector<MythDisplayMode>& MythDisplayMutter::GetVideoModes()
{
if (!m_interface || m_outputIdx < 0 || !m_videoModes.empty())
return m_videoModes;

m_videoModes.clear();
m_modeMap.clear();
MythMutterOutput& output = m_outputs[m_outputIdx];
QSize physical(output.widthmm, output.heightmm);
DisplayModeMap screenmap;

for (auto & mode : output.modes)
{
MythMutterMode& mmode = m_modes[static_cast<int32_t>(mode)];

bool interlaced = false;
#ifdef USING_X11
if (MythDisplayX11::IsAvailable())
interlaced = (mmode.flags & RR_Interlace) == RR_Interlace;
#endif
#ifdef USING_DRM
else
interlaced = (mmode.flags & DRM_MODE_FLAG_INTERLACE) == DRM_MODE_FLAG_INTERLACE;
#endif
if (interlaced)
{
LOG(VB_PLAYBACK, LOG_INFO, LOC + QString("Ignoring interlaced mode %1x%2 %3i")
.arg(mmode.width).arg(mmode.width).arg(mmode.frequency, 2, 'f', 2, '0'));
continue;
}

double rate = mmode.frequency;
QSize resolution(static_cast<int32_t>(mmode.width),
static_cast<int32_t>(mmode.height));

uint64_t key = MythDisplayMode::CalcKey(resolution, 0.0);
if (screenmap.find(key) == screenmap.end())
screenmap[key] = MythDisplayMode(resolution, physical, -1.0, rate);
else
screenmap[key].AddRefreshRate(rate);
m_modeMap.insert(MythDisplayMode::CalcKey(resolution, rate), mmode.id);
}

for (auto it = screenmap.begin(); screenmap.end() != it; ++it)
m_videoModes.push_back(it->second);

DebugModes();
return m_videoModes;
}

void MythDisplayMutter::UpdateCurrentMode()
{
if (!m_interface)
{
MythDisplay::UpdateCurrentMode();
return;
}

UpdateResources();
m_modeComplete = true;
}

void MythDisplayMutter::InitialiseInterface()
{
delete m_interface;
m_interface = new QDBusInterface(DISP_CONFIG_SERVICE, DISP_CONFIG_PATH,
DISP_CONFIG_SERVICE, QDBusConnection::sessionBus());
if (m_interface->isValid())
{
QDBusMessage reply = m_interface->call("GetResources");

if (reply.signature() == DISP_CONFIG_SIG)
{
QList<QVariant> args = reply.arguments();
if (args.size() == 6)
{
if (!QDBusConnection::sessionBus().connect(DISP_CONFIG_SERVICE, DISP_CONFIG_PATH,
DISP_CONFIG_SERVICE, "MonitorsChanged", this,
SLOT(MonitorsChanged())))
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to register for MonitorsChanged");
}
return;
}
else
{
LOG(VB_GENERAL, LOG_ERR, LOC + "GetResources unexpected reply");
}
}
else
{
LOG(VB_GENERAL, LOG_ERR, LOC + "GetResources signature not recognised");
}
}

delete m_interface;
m_interface = nullptr;
}

/// \note This is currently untested on a fully functional org.gnome.Mutter.DisplayConfig implementation
bool MythDisplayMutter::SwitchToVideoMode(QSize Size, double DesiredRate)
{
if (!m_interface)
return false;

auto rate = static_cast<double>(NAN);
QSize dummy(0, 0);
MythDisplayMode desired(Size, dummy, -1.0, DesiredRate);
int idx = MythDisplayMode::FindBestMatch(m_videoModes, desired, rate);

if (idx < 0)
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Desired resolution and frame rate not found.");
return false;
}

auto mode = MythDisplayMode::CalcKey(Size, rate);
if (!m_modeMap.contains(mode))
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find mode");
return false;
}

MythMutterCRTCOutList crtcs;
for (auto & crtc : m_crtcs)
{
// leave disabled CRTCs as disabled by ignoring
if ((crtc.currentmode < 0) || (crtc.width < 1) || (crtc.height < 1))
continue;

MythMutterCRTCOut crtcout;
crtcout.id = crtc.id;
crtcout.new_mode = crtc.currentmode;
crtcout.x = crtc.x;
crtcout.y = crtc.y;
crtcout.transform = crtc.currenttransform;
crtcout.outputs = QList<uint32_t>();
crtcout.properties = MythMutterMap();
crtcs.append(crtcout);
}

MythMutterOutputOutList outputs;
QDBusArgument crtcsarg;
QDBusArgument outarg;
QDBusReply<void> reply = m_interface->call(QLatin1String("ApplyConfiguration"),
m_serialVal, false,
QVariant::fromValue(crtcsarg << crtcs),
QVariant::fromValue(outarg << outputs));
if (!reply.isValid())
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Error applying new display configuration ('%1: %2')")
.arg(reply.error().type()).arg(reply.error().message()));
return false;
}

// If ApplyConfiguration is successful, then serial will have been updated
// and we need it for the next change
QDBusMessage resources = m_interface->call("GetResources");
QList<QVariant> args = resources.arguments();
if ((resources.signature() != DISP_CONFIG_SIG) || (args.size() != 6))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Failed to get updated DisplayConfig serial"
" - further display changes may fail");
}
else
{
m_serialVal = args[0].toUInt();
// TODO Validate the new config against the expected result?
}

return true;
}

void MythDisplayMutter::UpdateResources()
{
if (!m_interface)
return;

m_crtcs.clear();
m_outputs.clear();
m_modes.clear();
m_serialVal = 0;
m_outputIdx = -1;

QDBusMessage reply = m_interface->call("GetResources");
QList<QVariant> args = reply.arguments();
if ((reply.signature() != DISP_CONFIG_SIG) || (args.size() != 6))
return;

m_serialVal = args[0].toUInt();

args[1].value<QDBusArgument>() >> m_crtcs;
for (auto & crtc : m_crtcs)
{
LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("CRTC %1/%2: %3x%4+%5+%6 Mode: %7")
.arg(crtc.id).arg(crtc.sys_id).arg(crtc.width)
.arg(crtc.height).arg(crtc.x).arg(crtc.y).arg(crtc.currentmode));
}

args[2].value<QDBusArgument>() >> m_outputs;
for (auto & output : m_outputs)
{
QStringList possiblecrtcs;
for (auto poss : output.possible_crtcs)
possiblecrtcs.append(QString::number(poss));
QStringList modes;
for (auto mode : output.modes)
modes.append(QString::number(mode));
QStringList props;
for (auto prop : output.properties)
props.append(QString("%1:%2").arg(prop.first).arg(prop.second.variant().toString()));
LOG(VB_GENERAL, LOG_DEBUG, LOC +
QString("Output %1/%2: CRTC: %3 Possible CRTCs: %4 Name: '%5'")
.arg(output.id).arg(output.sys_id).arg(output.current_crtc)
.arg(possiblecrtcs.join(",")).arg(output.name));
LOG(VB_GENERAL, LOG_DEBUG, LOC +
QString("Output %1/%2: Modes: %3")
.arg(output.id).arg(output.sys_id).arg(modes.join(",")));
LOG(VB_GENERAL, LOG_DEBUG, LOC +
QString("Output %1/%2: Properties: %3")
.arg(output.id).arg(output.sys_id).arg(props.join(",")));
}

args[3].value<QDBusArgument>() >> m_modes;
for (auto & mode : m_modes)
{
LOG(VB_GENERAL, LOG_DEBUG, LOC +
QString("Mode %1/%2: %3x%4@%5 Flags: 0x%6")
.arg(mode.id).arg(mode.sys_id).arg(mode.width)
.arg(mode.height).arg(mode.frequency).arg(mode.flags, 0, 16));
}

if (m_outputs.empty())
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "No connected outputs");
return;
}

// only one connected device - use it
if (m_outputs.size() == 1)
{
m_outputIdx = 0;
}
else
{
// TODO - we may be able to match based on name - but need to check with Wayland
// Use the serial number from the current QScreen to select a suitable device
auto serial = m_screen->serialNumber();
if (serial.isEmpty())
{
LOG(VB_GENERAL, LOG_INFO, LOC + "No serial number to search for - using first output");
m_outputIdx = 0;
}
// search for the best connected output
else
{
int idx = 0;
for (auto & output : m_outputs)
{
if (output.serialnumber == serial)
{
LOG(VB_GENERAL, LOG_INFO, LOC + QString("Matched serial '%1' to device '%2'")
.arg(serial).arg(output.name));
m_outputIdx = idx;
break;
}
++idx;
}

if (m_outputIdx == -1)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Failed to match display serial - using first device");
m_outputIdx = 0;
}
}
}

// retrieve details
LOG(VB_GENERAL, LOG_INFO, LOC + QString("Using device '%1'").arg(m_outputs[m_outputIdx].name));

int32_t mode = m_crtcs[m_outputs[m_outputIdx].current_crtc].currentmode;
m_refreshRate = m_modes[mode].frequency;
m_resolution = QSize(static_cast<int>(m_modes[mode].width),
static_cast<int>(m_modes[mode].height));
m_physicalSize = QSize(m_outputs[m_outputIdx].widthmm, m_outputs[m_outputIdx].heightmm);
m_edid = MythEDID(m_outputs[m_outputIdx].edid);
}
106 changes: 106 additions & 0 deletions mythtv/libs/libmythui/platforms/mythdisplaymutter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#ifndef MYTHMUTTERDISPLAYCONFIG_H
#define MYTHMUTTERDISPLAYCONFIG_H

// Qt
#include <QtDBus>
#include <QObject>

// MythTV
#include "mythdisplay.h"

#define DISP_CONFIG_SERVICE (QString("org.gnome.Mutter.DisplayConfig"))
#define DISP_CONFIG_PATH (QString("/org/gnome/Mutter/DisplayConfig"))
#define DISP_CONFIG_SIG (QString("ua(uxiiiiiuaua{sv})a(uxiausauaua{sv})a(uxuudu)ii"))

using MythMutterMap = QMap<QString,QDBusVariant>;
using MythMutterProperty = QPair<QString,QDBusVariant>;
using MythMutterProperties = QList<MythMutterProperty>;

// GetResources CRTCs signature a(uxiiiiiuaua{sv})
struct MythMutterCRTC
{
uint32_t id;
qint64 sys_id; // N.B. needs to be a Qt type here
int32_t x;
int32_t y;
int32_t width;
int32_t height;
int32_t currentmode;
uint32_t currenttransform;
QList<uint32_t> transforms;
MythMutterProperties properties;
};

// GetResources Outputs signature a(uxiausauaua{sv})
struct MythMutterOutput
{
uint32_t id;
qint64 sys_id;
int32_t current_crtc;
QList<uint32_t> possible_crtcs;
QString name;
QList<uint32_t> modes;
QList<uint32_t> clones;
MythMutterProperties properties;

// MythTV properties
QString serialnumber;
QByteArray edid;
int widthmm;
int heightmm;
};

// GetResources Modes signature a(uxuudu)
struct MythMutterMode
{
uint32_t id;
qint64 sys_id;
uint32_t width;
uint32_t height;
double frequency;
uint32_t flags;
};

using MythMutterCRTCList = QList<MythMutterCRTC>;
Q_DECLARE_METATYPE(MythMutterCRTC);
Q_DECLARE_METATYPE(MythMutterCRTCList);
using MythMutterOutputList = QList<MythMutterOutput>;
Q_DECLARE_METATYPE(MythMutterOutput);
Q_DECLARE_METATYPE(MythMutterOutputList);
using MythMutterModeList = QList<MythMutterMode>;
Q_DECLARE_METATYPE(MythMutterMode);
Q_DECLARE_METATYPE(MythMutterModeList);

class MythDisplayMutter : public MythDisplay
{
Q_OBJECT

public:
static MythDisplayMutter* Create();
~MythDisplayMutter() override;

void UpdateCurrentMode () override;
bool VideoModesAvailable () override { return true; }
bool UsingVideoModes () override;
const vector<MythDisplayMode>& GetVideoModes(void) override;
bool SwitchToVideoMode (QSize Size, double DesiredRate) override;

public slots:
void MonitorsChanged();

private:
MythDisplayMutter();
bool IsValid();
void InitialiseInterface();
void UpdateResources();

QDBusInterface* m_interface { nullptr };
uint32_t m_serialVal { 0 };
MythMutterCRTCList m_crtcs;
MythMutterOutputList m_outputs;
MythMutterModeList m_modes;
int m_outputIdx { -1 };
QMap<uint64_t, uint32_t> m_modeMap;
};

#endif