#include "xx.h" #include "xprivate.h" #include "cvt.h" #include "main.h" #include #include #include #include #include /* * 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(calloc(1, sizeof(XRRCrtcInfo))); copy->outputs = static_cast(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 backup; }; ConfigBackup::ConfigBackup() { _ok = false; a = new BackupInternal; } ConfigBackup::ConfigBackup(XPrivate *x) { _ok = false; a = new BackupInternal; a->backup = QSharedPointer(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 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))); } 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(&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 ScreenSetup::getScreenPositions() const { QMap ret; for (auto oi : a->_outputMap) { 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 wanted; wanted << RES(1280, 720) << RES(1280, 800) << RES(1920, 1080); for (XRRModeInfo *mode : a->_modeMap) { if (toVertRefresh(mode) < 58 || toVertRefresh(mode) > 61) 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); } } #undef RES // Create those that no output supported for (auto res : wanted) { unsigned int x = res & 0xffff; unsigned int y = res >> 16; 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) { a->copyModesToAll(key, oi->output->nmode); } } updateScreenResources(); } static QSize getTotalSizeHorz(const QList &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>> 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())); qDebug() << "Adding resolution" << s; } } auto outputListStr = mapping.split(QRegularExpression(QLatin1String("\\s+")), QString::SkipEmptyParts); qDebug() << mapping << ">" << outputListStr; if (outputListStr.isEmpty()) { QList 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 screenMap; QMap 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."; QList 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(mode->hr); m.height = static_cast(mode->vr); m.dotClock = static_cast(mode->pclk) * 1000ul * 1000ul; m.hSyncStart= static_cast(mode->hss); m.hSyncEnd = static_cast(mode->hse); m.hTotal = static_cast(mode->hfl); m.hSkew = 0; m.vSyncStart= static_cast(mode->vss); m.vSyncEnd = static_cast(mode->vse); m.vTotal = static_cast(mode->vfl); m.id = 0; m.name = ba.data(); m.nameLength= static_cast(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); qDebug() << "Return value of create was" << xid; // 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 for (int i = 0; i < 2; ++i) { int withPref = 0; QSet known; for (auto oi : a->_outputMap) { if (oi->output->npreferred == 0) { if (oi->mode != nullptr) { known.insert(oi->mode->id); } } else { withPref++; known.insert(oi->output->modes[0]); } } if (withPref == 0 && i == 0) { QThread::msleep(100); updateScreenResources(); retval = createCrtcBackup(); continue; } if (known.count() < 2) return retval; break; } 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>> &list) { ConfigBackup retval; QStringList cmd; QList 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; int x = 0; for (auto e : list) { if (e.second.isEmpty()) continue; const QSize &res = e.first; unsigned int w = 0; 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; } } } 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, QSet> 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 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; }