#include "xx.h"
#include "xprivate.h"
#include "cvt.h"
#include "main.h"
#include <QDebug>
#include <QSocketNotifier>
#include <QThread>
#include <QProcess>
#include <QRegularExpression>
/*
* This clusterfuck exists because there are name clashes between X11/Xrandr headers
* and Qt classes. Hence the split into xx.* and xprivate.* as well as those idiotic
* matrjoschka classes. Or I'm just stupid.
*/
class BackupInternalInternal
{
private:
XPrivate *x;
public:
CrtcMap map;
BackupInternalInternal() : x(nullptr) {
qDebug() << "new BackupInternalInternal";
}
~BackupInternalInternal() {
qDebug() << "delete BackupInternalInternal";
freeBackup();
}
void createBackup(XPrivate *x) {
this->x = x;
freeBackup();
for (CrtcMap::iterator it = x->_crtcMap.begin(); it != x->_crtcMap.end(); ++it) {
const auto src = it.value();
XRRCrtcInfo *copy = static_cast<XRRCrtcInfo*>(calloc(1, sizeof(XRRCrtcInfo)));
copy->outputs = static_cast<RROutput*>(calloc(size_t(src->noutput), sizeof(RROutput)));
copy->x = src->x;
copy->y = src->y;
copy->mode = src->mode;
copy->rotation = src->rotation;
copy->noutput = src->noutput;
for (int i = 0; i < src->noutput; ++i) {
copy->outputs[i] = src->outputs[i];
}
map[it.key()] = copy;
}
qDebug() << "Created CRTC backup with entries:" << map.size();
}
void freeBackup() {
for (auto i : map) {
free(i->outputs);
free(i);
}
map.clear();
}
void revertChanges()
{
if (map.isEmpty())
return;
qDebug() << "Starting revert";
QStringList cmd;
QSize screenSize;
for (auto e : map) {
if (e->mode == None || !x->_modeMap.contains(e->mode))
continue;
XRRModeInfo *mode = x->_modeMap[e->mode];
QString rate = QString::number(toVertRefresh(mode), 'f', 2);
for (int i = 0; i < e->noutput; ++i) {
auto *oi = x->_outputMap[e->outputs[i]];
cmd << "--output" << oi->outputName << "--mode" << mode->name << "--rate" << rate;
cmd << "--pos" << QString::asprintf("%dx%d", oi->crtc->x, oi->crtc->y);
if (oi->crtc->x == 0 && oi->crtc->y == 0 && !cmd.contains("--primary")) {
cmd << "--primary";
}
}
screenSize = screenSize.expandedTo(QSize(e->x + int(x->_modeMap[e->mode]->width), e->y + int(x->_modeMap[e->mode]->height)));
}
ScreenSetup::inst()->runXrandr(cmd);
freeBackup();
}
};
class BackupInternal
{
public:
BackupInternal() {
qDebug() << "new BackupInternal";
}
~BackupInternal() {
qDebug() << "delete BackupInternal";
}
QSharedPointer<BackupInternalInternal> backup;
};
ConfigBackup::ConfigBackup()
{
_ok = false;
a = new BackupInternal;
}
ConfigBackup::ConfigBackup(XPrivate *x)
{
_ok = false;
a = new BackupInternal;
a->backup = QSharedPointer<BackupInternalInternal>(new BackupInternalInternal);
a->backup->createBackup(x);
}
ConfigBackup::~ConfigBackup()
{
delete a;
}
ConfigBackup& ConfigBackup::operator=(const ConfigBackup &other)
{
if (this == &other)
return *this;
delete a;
_ok = other._ok;
a = new BackupInternal;
a->backup = other.a->backup;
return *this;
}
ConfigBackup::ConfigBackup(const ConfigBackup &other)
{
_ok = other._ok;
a = new BackupInternal;
a->backup = other.a->backup;
}
void ConfigBackup::revert()
{
a->backup->revertChanges();
}
static QWeakPointer<BackupInternalInternal> currentBackup;
/*
* Slightly more normal code starts here
*/
ScreenInfo ScreenSetup::initScreenInfo(const OutputInfo *oi) const
{
ScreenInfo si;
si.position = oi->position;
si.name = oi->modelName;
si.output = oi->outputName;
si.isProjector = (oi->outputType == Projector::Yes);
if (oi->mode != nullptr) {
si.currentResolution = QSize(QSize(int(oi->mode->width), int(oi->mode->height)));
}
if (oi->crtc != nullptr) {
si.location = QPoint(oi->crtc->x, oi->crtc->y);
}
for (int i = 0; i < oi->output->nmode; ++i) {
if (a->_modeMap.contains(oi->output->modes[i])) {
auto m = a->_modeMap.value(oi->output->modes[i]);
const QSize size(int(m->width), int(m->height));
if (!si.modes.contains(size)) {
si.modes.append(size);
}
}
}
auto pref = a->getPreferredMode(oi);
if (pref != nullptr) {
si.preferredResolution = QSize(int(pref->width), int(pref->height));
}
return si;
}
ScreenSetup * ScreenSetup::_instance = nullptr;
static int errorHandlerX(Display*)
{
exit(1);
}
ScreenSetup::ScreenSetup() : a(new XPrivate())
{
int event_base_return, error_base_return;
if (!XRRQueryExtension(a->_display, &event_base_return, &error_base_return)) {
qDebug() << "No XRANDR extension found";
exit(1);
}
updateScreenResources();
XRRSelectInput(a->_display, DefaultRootWindow(a->_display), RROutputChangeNotifyMask);
//XSync(a->_display, False);
XSetIOErrorHandler((XIOErrorHandler) errorHandlerX);
_socketNotifier = new QSocketNotifier(qintptr(ConnectionNumber(a->_display)), QSocketNotifier::Read);
connect(_socketNotifier, &QSocketNotifier::activated, [=](int) {
XEvent ev;
qDebug() << "Socket Event";
while (XPending(a->_display) > 0) {
XNextEvent(a->_display, &ev);
if (ev.type - event_base_return != RRNotify) {
qDebug() << "Received unknown X event";
continue;
}
qDebug() << "Got Change Event";
XRROutputChangeNotifyEvent *oce = reinterpret_cast<XRROutputChangeNotifyEvent*>(&ev);
XRRScreenResources *sr = XRRGetScreenResources(oce->display, oce->window);
if (sr == nullptr) {
emit outputConfigChanged(ConnectionEvent::Unknown);
continue;
}
XRROutputInfo *oi = XRRGetOutputInfo(a->_display, sr, oce->output);
if (oi == nullptr) {
XRRFreeScreenResources(sr);
emit outputConfigChanged(ConnectionEvent::Unknown);
continue;
}
if (oi->connection == RR_Connected) {
emit outputConfigChanged(ConnectionEvent::Connected);
} else if (oi->connection == RR_Disconnected) {
emit outputConfigChanged(ConnectionEvent::Disconnected);
} else {
emit outputConfigChanged(ConnectionEvent::Unknown);
}
XRRFreeOutputInfo(oi);
XRRFreeScreenResources(sr);
}
});
}
void ScreenSetup::addMissingEdidResolutions()
{
a->addMissingEdidResolutions();
}
//___________________________________________________________________________
void ScreenSetup::updateScreenResources()
{
a->updateScreenResources();
}
QMap<QString, ScreenInfo> ScreenSetup::getScreenPositions() const
{
QMap<QString, ScreenInfo> ret;
for (auto oi : a->_outputMap) {
if (oi->mode == nullptr)
continue;
if (oi->mode->width == 0 || oi->mode->height == 0)
continue;
ret.insert(oi->outputName, initScreenInfo(oi));
}
return ret;
}
/**
* Create common modes and add them to all outputs.
* Make sure every output has some variant of the most commonly
* used modes.
*/
void ScreenSetup::initModes()
{
// First copy typical resolutions to all outputs
#define RES(x,y) (((y) << 16) | (x))
QSet<quint32> wanted;
wanted << RES(1280, 720) << RES(1280, 800) << RES(1920, 1080);
for (XRRModeInfo *mode : a->_modeMap) {
if (toVertRefresh(mode) < 55 || toVertRefresh(mode) > 65)
continue; // Play it safe and consider only those for copying that are 60Hz
if (!wanted.remove(RES(mode->width, mode->height)))
continue; // Is not a wanted resolution
// Make sure all outputs got it
for (OutputInfo *info : a->_outputMap) {
if (!a->getOutputModeForResolution(info->output, mode->width, mode->height).isEmpty())
continue;
XRRAddOutputMode(a->_display, info->id, mode->id);
qDebug() << "Adding mode" << mode->width << 'x' << mode->height << "to" << info->outputName;
}
}
#undef RES
// Create those that no output supported
for (auto res : wanted) {
unsigned int x = res & 0xffff;
unsigned int y = res >> 16;
qDebug() << "Creating missing wanted resolution of" << x << "x" << y;
createMode(x, y, 60, QString::asprintf("%ux%u", x, y));
}
if (!wanted.isEmpty()) { // Modes were added, update for final loop below
updateScreenResources();
}
// Finally copy all those the projector supports to other outputs
for (auto key : a->_outputMap.keys()) {
OutputInfo *oi = a->_outputMap[key];
if (oi->outputType == Projector::Yes && oi->output->npreferred > 0) { // Only if has edid
qDebug() << "Copying" << oi->output->nmode << "modes to all from" << oi->outputName;
a->copyModesToAll(key, oi->output->nmode);
}
}
updateScreenResources();
}
static QSize getTotalSizeHorz(const QList<QSize> &list)
{
QSize ret(0, 0);
for (auto e : list) {
ret.rwidth() += e.width();
ret.rheight() = qMax(ret.height(), e.height());
}
return ret;
}
ScreenMode ScreenSetup::getCurrentMode()
{
bool notAtOrigin = false;
for (auto oi : a->_outputMap) {
if (oi->mode != nullptr) {
if (oi->crtc->x != 0 || oi->crtc->y != 0) {
notAtOrigin = true;
}
}
}
if (a->_outputMap.size() == 1)
return ScreenMode::Single;
if (a->_outputMap.size() > 2)
return ScreenMode::Advanced;
if (notAtOrigin)
return ScreenMode::Dual;
return ScreenMode::Clone;
}
bool ScreenSetup::hasScreenWithoutEdid()
{
for (auto oi : a->_outputMap) {
// no preferred modes pretty much means no EDID, although technically I think you could have EDID
// that doesn't provide a preferred mode...
if (oi->output != nullptr && oi->output->connection != RR_Disconnected && oi->output->npreferred == 0)
return true;
}
return false;
}
ConfigBackup ScreenSetup::setResolutionsFromString(const QString &resolutions, const QString &mapping)
{
auto resListStr = resolutions.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts);
QList<QPair<QSize, QList<QString>>> config;
QRegularExpression re(QLatin1String("^(\\d+)x(\\d+)$"));
for (auto s : resListStr) {
auto m = re.match(s);
if (m.hasMatch()) {
config.append(qMakePair(QSize(m.captured(1).toInt(), m.captured(2).toInt()), QList<QString>()));
qDebug() << "Adding resolution" << s;
}
}
auto outputListStr = mapping.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts);
qDebug() << mapping << ">" << outputListStr;
if (outputListStr.isEmpty()) {
QList<QString> sorted;
for (auto *o : a->_outputMap) {
sorted.append(o->outputName);
}
qSort(sorted);
int i = 0;
for (auto o : sorted) {
int index = i % config.size();
auto x = config.at(index).second;
x.append(o);
config.replace(index, qMakePair(config.at(index).first, x));
qDebug() << "Resolution" << config.at(index).first << "is now" << config.at(index).second;
++i;
}
} else {
QRegularExpression re(QLatin1String("^(.+)=(\\d+)$"));
for (auto s : outputListStr) {
auto m = re.match(s);
if (m.hasMatch()) {
int index = m.captured(2).toInt();
if (index >= 0 && index < config.size()) {
auto x = config.at(index).second;
x.append(m.captured(1));
config.replace(index, qMakePair(config.at(index).first, x));
}
}
}
}
qDebug() << config;
return setCustom(config);
}
ConfigBackup ScreenSetup::setDefaultMode(ScreenMode &mode)
{
ConfigBackup retval;
if (a->_outputMap.size() == 1) { // Only one output exists, do nothing
retval._ok = (a->_outputMap.begin().value()->mode != nullptr);
mode = ScreenMode::Single;
return retval;
}
// QMap sorts by key, so the outputs should be sorted alphabetically
QMap<QString, OutputInfo*> screenMap;
QMap<QString, OutputInfo*> projectorMap;
for (auto o : a->_outputMap) {
qDebug() << o->outputName << quint32(o->outputType);
if (o->outputType == Projector::Yes) {
projectorMap.insert(o->outputName, o);
} else {
screenMap.insert(o->outputName, o);
}
}
auto projectors = projectorMap.values();
auto screens = screenMap.values();
qDebug() << projectors.size() << "projectors," << screens.size() << "screens.";
qDebug() << "Projectors:" << projectorMap.keys();
qDebug() << "Screens:" << screenMap.keys();
QList<QSize> outputSizes = a->getTotalSize(projectors, screens);
if (outputSizes.isEmpty()) {
mode = ScreenMode::Advanced; // Dunno lol
return retval;
}
for (;;) {
QSize screenSize = getTotalSizeHorz(outputSizes);
QStringList cmd;
retval = createCrtcBackup();
qDebug() << "Virtual screen size:" << screenSize << "with" << outputSizes.size() << "different screens.";
int offset = 0;
for (int i = 0; i < outputSizes.size(); ++i) {
const QSize &size = outputSizes.at(i);
unsigned int w = 0;
if (i < projectors.size()) {
auto *mode = a->setOutputResolution(cmd, projectors.at(i), offset, 0, size);
if (mode != nullptr && mode->width > w) {
w = mode->width;
}
}
if (i < screens.size()) {
auto *mode = a->setOutputResolution(cmd, screens.at(i), offset, 0, size);
if (mode != nullptr && mode->width > w) {
w = mode->width;
}
}
offset += w;
}
retval._ok = runXrandr(cmd);
if (retval._ok)
break;
// If xrandr failed, try again while capping the resolution of any screen to FullHD
bool keepGoing = false;
for (QSize &geo : outputSizes) {
if (geo.width() > 1920) {
keepGoing = true;
geo.setWidth(1920);
}
if (geo.height() > 1200) {
keepGoing = true;
geo.setHeight(1080);
}
}
// If we didn't clamp resolutions, but have more than one output group, remove last and retry
if (!keepGoing && outputSizes.size() > 1) {
keepGoing = true;
outputSizes.removeLast();
}
if (!keepGoing)
break; // No more options
}
updateScreenResources(); // Re-Read
if (outputSizes.size() == 1) {
// One output size, at least 2 outputs in total -- clone mode
mode = ScreenMode::Clone;
} else if (outputSizes.size() == 2 && a->_outputMap.size() == 2) {
// Two outputs, two sizes -- extended
mode = ScreenMode::Dual;
} else {
mode = ScreenMode::Advanced; // Must be more than 2 outputs -> something more involved
}
return retval;
}
//___________________________________________________________________________
bool ScreenSetup::createMode(unsigned int resX, unsigned int resY, float refresh, QString name)
{
QByteArray ba = name.toLocal8Bit();
mode *mode = vert_refresh(int(resX), int(resY), refresh, 0, 0, 0);
if (mode == nullptr)
return false;
XRRModeInfo m;
memset(&m, 0, sizeof(m));
m.width = static_cast<unsigned int>(mode->hr);
m.height = static_cast<unsigned int>(mode->vr);
m.dotClock = static_cast<unsigned long>(mode->pclk) * 1000ul * 1000ul;
m.hSyncStart= static_cast<unsigned int>(mode->hss);
m.hSyncEnd = static_cast<unsigned int>(mode->hse);
m.hTotal = static_cast<unsigned int>(mode->hfl);
m.hSkew = 0;
m.vSyncStart= static_cast<unsigned int>(mode->vss);
m.vSyncEnd = static_cast<unsigned int>(mode->vse);
m.vTotal = static_cast<unsigned int>(mode->vfl);
m.id = 0;
m.name = ba.data();
m.nameLength= static_cast<unsigned int>(ba.length());
m.modeFlags = RR_VSyncPositive | RR_HSyncNegative;
free(mode);
for (XRRModeInfo *mode : a->_modeMap) {
if (mode->width == m.width && mode->height == m.height && mode->dotClock == m.dotClock)
return true; // Already exists, return true?
}
RRMode xid = XRRCreateMode(a->_display, DefaultRootWindow(a->_display), &m);
if (xid <= 0) {
qDebug() << "Return value of create mode was" << xid << "(" << resX << 'x' << resY << ")";
return false;
}
// Immediately add to all screens
for (OutputInfo *info : a->_outputMap) {
XRRAddOutputMode(a->_display, info->id, xid);
}
return true;
}
//___________________________________________________________________________
ScreenSetup::~ScreenSetup()
{
delete a;
}
ConfigBackup ScreenSetup::setCenteredClone()
{
ConfigBackup retval = createCrtcBackup();
XRRModeInfo *fallback = nullptr;
for (auto m : a->_modeMap) {
if (m->width == 1024 && m->height == 768) {
fallback = m;
break;
}
}
// See if we even need to do anything
bool ok = true;
unsigned int lX = UINT_MAX, lY = 0;
for (auto oi : a->_outputMap) {
if (oi->crtc == nullptr)
continue;
if (oi->crtc->x != 0 || oi->crtc->y != 0) {
ok = false; // A screen that's not at origin
break;
}
if (lX != UINT_MAX && (oi->crtc->width != lX || oi->crtc->height != lY)) {
ok = false; // Different size than previous screen
break;
}
lX = oi->crtc->width;
lY = oi->crtc->height;
}
if (ok)
return retval;
QStringList cmd;
QSize screenSize;
for (auto oi : a->_outputMap) {
auto mode = a->getPreferredMode(oi, fallback);
if (mode == nullptr)
continue;
if (int(mode->width) > screenSize.width()) {
screenSize.setWidth(int(mode->width));
}
if (int(mode->height) > screenSize.height()) {
screenSize.setHeight(int(mode->height));
}
}
for (auto oi : a->_outputMap) {
auto mode = a->getPreferredMode(oi, fallback);
if (mode == nullptr)
continue;
const int x = (screenSize.width() - int(mode->width)) / 2;
const int y = (screenSize.height() - int(mode->height)) / 2;
a->setOutputResolution(cmd, oi, x, y, QSize(int(mode->width), int(mode->height)));
}
retval._ok = runXrandr(cmd);
return retval;
}
ConfigBackup ScreenSetup::setClone(const QSize &resolution)
{
QStringList cmd;
ConfigBackup retval = createCrtcBackup();
for (auto oi : a->_outputMap) {
a->setOutputResolution(cmd, oi, 0, 0, resolution);
}
retval._ok = runXrandr(cmd);
return retval;
}
ConfigBackup ScreenSetup::setCustom(const QList<QPair<QSize, QList<QString>>> &list)
{
ConfigBackup retval;
QStringList cmd;
QList<QSize> sizes;
for (auto e : list) {
if (e.second.isEmpty())
continue;
sizes.append(e.first);
}
if (sizes.isEmpty())
return retval;
retval = createCrtcBackup();
auto screenSize = getTotalSizeHorz(sizes);
if (screenSize.isEmpty())
return retval;
// Make sure desired resolutions exist on outputs
bool reload = false;
for (auto e : list) {
if (e.second.isEmpty())
continue;
const QSize &res = e.first;
for (auto outputName : e.second) {
for (auto oi : a->_outputMap) {
if (oi->outputName != outputName)
continue;
// Now add resolution if not found
if (a->addResolutionToOutput(oi, res)) {
reload = true;
}
break;
}
}
}
if (reload) {
updateScreenResources();
}
int x = 0;
for (auto e : list) {
if (e.second.isEmpty())
continue;
const QSize &res = e.first;
unsigned int w = 0;
// Find according output, apply resolution
for (auto outputName : e.second) {
for (auto oi : a->_outputMap) {
if (oi->outputName != outputName)
continue;
auto *mode = a->setOutputResolution(cmd, oi, x, 0, res);
if (mode != nullptr && mode->width > w) {
w = mode->width;
}
break;
}
}
x += w;
}
retval._ok = runXrandr(cmd);
return retval;
}
static bool modeBiggerThan(const QSize &a, const QSize &b)
{
if (a.width() > b.width())
return true;
return a.width() == b.width() && a.height() > b.height();
}
ResolutionVector ScreenSetup::getCommonModes() const
{
QHash<QPair<quint32, quint32>, QSet<RROutput>> matches;
for (auto oi : a->_outputMap) {
for (int i = 0; i < oi->output->nmode; ++i) {
if (!a->_modeMap.contains(oi->output->modes[i]))
continue;
const auto mode = a->_modeMap[oi->output->modes[i]];
const QPair<quint32, quint32> pair = qMakePair(mode->width, mode->height);
matches[pair].insert(oi->id);
}
}
ResolutionVector ret;
for (auto it = matches.begin(); it != matches.end(); ++it) {
if (it.value().size() == a->_outputMap.size()) {
ret.append(QSize(int(it.key().first), int(it.key().second)));
}
}
qSort(ret.begin(), ret.end(), modeBiggerThan);
return ret;
}
ConfigBackup ScreenSetup::createCrtcBackup()
{
auto bd = currentBackup.toStrongRef();
if (bd.data() != nullptr && !bd->map.isEmpty()) {
ConfigBackup backup;
backup.a->backup = bd;
return backup;
}
ConfigBackup backup(a);
currentBackup = backup.a->backup.toWeakRef();
return backup;
}
/**
* Return number of connected (no active) outputs according to last query
*/
int ScreenSetup::getOutputCount() const
{
return a->_outputMap.size();
}
/**
* Query currently connected number of outputs
*/
int ScreenSetup::queryCurrentOutputCount() const
{
auto sr = XRRGetScreenResourcesCurrent(a->_display, DefaultRootWindow(a->_display));
if (sr == nullptr)
return 0;
int count = 0;
for (int i = 0; i < sr->noutput; ++i) {
XRROutputInfo* info = XRRGetOutputInfo(a->_display, sr, sr->outputs[i]);
if (info == nullptr)
continue;
if (info->connection != RR_Disconnected) {
count++;
}
XRRFreeOutputInfo(info);
}
XRRFreeScreenResources(sr);
return count;
}
const ResolutionVector &ScreenSetup::getVirtualResolutions() const
{
return a->_resolutions;
}
bool ScreenSetup::runXrandr(QStringList &cmd)
{
QProcess proc;
// Sloppy: Turn off all outputs not found in argument list. Doesn't actually parse the
// command line, so if you have a mode that's called like an output, funny things might happen.
for (const auto &name : a->_allOutputs) {
if (!cmd.contains(name)) {
cmd << "--output" << name << "--off";
}
}
qDebug() << "XRANDR:" << cmd;
if (CommandLine::testMode()) {
cmd << "--dryrun";
}
proc.setProcessChannelMode(QProcess::ProcessChannelMode::ForwardedChannels);
proc.start("xrandr", cmd);
proc.waitForFinished(5000);
if (proc.state() == QProcess::Running) {
proc.kill();
}
return proc.exitCode() == 0;
}