#include "xprivate.h" #include #include /** * Check list of known model names that falsely report a screen size or similar */ static QStringList initProjectorList() { QStringList list; list << "AT-HDVS-RX"; // Switchbox // TODO: Load from file QFile file("/opt/openslx/beamergui/beamer.conf"); // TODO: Config option if (file.open(QIODevice::ReadOnly)) { QRegularExpression re("^([^=]+)=beamer$"); while (!file.atEnd()) { QString line = file.readLine(); auto match = re.match(line); if (match.hasMatch()) { list << match.captured(1); } } } qDebug() << "Forced beamer list:" << list; return list; } static bool isProjectorName(const QString &name) { static QStringList projectors = initProjectorList(); return projectors.contains(name); } static bool xRectLessThan(const OutputInfo* const &a, const OutputInfo* const &b) { return a->crtc->x < b->crtc->x; } /* * Class members */ XPrivate::XPrivate() : _screenResources(nullptr) { // Get initial data (to be freed) _display = XOpenDisplay(nullptr); if (_display == nullptr) { qFatal("Cannot open display"); ::exit(1); } _EDID_ATOM = XInternAtom(_display, RR_PROPERTY_RANDR_EDID, False); } XPrivate::~XPrivate() { XCloseDisplay(_display); } void XPrivate::freeResources() { freeCrtcBackup(); // Clear the modemap (nothing to be freed, stored in screenResources) _modeMap.clear(); _resolutions.clear(); // Clear output info for (OutputInfo *output : _outputMap.values()) { delete output; } _outputMap.clear(); // Clear crtc info for (XRRCrtcInfo *info : _crtcMap.values()) { XRRFreeCrtcInfo(info); } _crtcMap.clear(); // Release resources if (_screenResources != nullptr) { XRRFreeScreenResources(_screenResources); _screenResources = nullptr; } XRRFreeScreenResources(_screenResources); } void XPrivate::updateScreenResources() { freeResources(); _screenResources = XRRGetScreenResourcesCurrent(_display, DefaultRootWindow(_display)); // Create the modemap qDebug() << "Modes"; for (int i = 0; i < _screenResources->nmode; ++i) { _modeMap.insert( _screenResources->modes[i].id, &_screenResources->modes[i]); /* qDebug() << _screenResources->modes[i].id << "\t" << _screenResources->modes[i].width << "x" << _screenResources->modes[i].height; */ } /* int num = 0; XRRMonitorInfo *mi = XRRGetMonitors(_display, DefaultRootWindow(_display), True, &num); for (int i = 0; i < num; ++i) { char *name = XGetAtomName(_display, mi [i].name); if (name == nullptr) continue; qDebug() << "some monitor with name" << name << mi[i].primary; XFree(name); } */ // Create crtcMap qDebug() << "CRTCs"; for (int i = 0; i < _screenResources->ncrtc; ++i) { XRRCrtcInfo * info = XRRGetCrtcInfo( _display, _screenResources, _screenResources->crtcs[i]); if (info == nullptr) { qDebug() << "Error getting CRTC info for crtc" << i; continue; } qDebug() << _screenResources->crtcs[i] << "-- Outputs:" << info->noutput << "Possible:" << info->npossible; _crtcMap.insert(_screenResources->crtcs[i], info); } // Create outputmap and connectedOutputMap qDebug() << "Outputs"; QHash tempMap; QMap typeCount; for (int i = 0; i < _screenResources->noutput; ++i) { XRROutputInfo* info = XRRGetOutputInfo( _display, _screenResources, _screenResources->outputs[i]); if (info == nullptr) { qDebug() << "Error getting info for output" << i; continue; } const QString outputName = QString::fromLocal8Bit(info->name, info->nameLen); tempMap.insert(_screenResources->outputs[i], outputName); if (info->connection == RR_Disconnected) { qDebug() << "Ignoring disconnected output" << outputName; XRRFreeOutputInfo(info); continue; } bool disconnected = false; if (info->crtc == None) { disconnected = true; qDebug() << "Connected output" << outputName << "has no CRTC -- trying to find free one"; info->crtc = getFreeCrtc(info); } if (!_crtcMap.contains(info->crtc)) { qDebug() << "Have output" << outputName << "with no known crtc"; XRRFreeOutputInfo(info); continue; } XRRCrtcInfo *crtc = _crtcMap.value(info->crtc); XRRModeInfo *mode = nullptr; if (!disconnected) { if (!_modeMap.contains(crtc->mode)) { qDebug() << "Have output" << outputName << " with crtc with no known mode -- offline?"; } else { mode = _modeMap.value(crtc->mode); } } OutputInfo *oi = new OutputInfo(_screenResources->outputs[i], info, crtc, mode); oi->outputName = outputName; if (!this->readEdid(oi)) { // We have no EDID - take it as an indicator that there's a dumb output split/switch box oi->outputType = Projector::Maybe; } else if ((info->mm_height == 0 && info->mm_width == 0) || isProjectorName(oi->modelName)) { oi->outputType = Projector::Yes; // Screens with size 0x0 are projectors by convention } else if (info->mm_width > 500 && info->mm_height > 500) { // Big screen - probably should be handled like a projector oi->outputType = Projector::Yes; } else if (info->mm_width > 350 && info->mm_height > 350) { // Somewhere in-between oi->outputType = Projector::Maybe; } typeCount[oi->outputType]++; _outputMap.insert(_screenResources->outputs[i], oi); qDebug() << "Connected" << outputName << "-- Clones:" << info->nclone << "Modes:" << info->nmode << "Crtcs:" << info->ncrtc << "Preferred:" << info->npreferred; } // Final checks for projector if (typeCount[Projector::Yes] == 0) { // No definitive projector if (typeCount[Projector::Maybe] > 0 && typeCount[Projector::No] > 0) { // Potential projector(s) and normal screen, promote for (OutputInfo *info : _outputMap) { if (info->outputType == Projector::Maybe) { info->outputType = Projector::Yes; break; } } } } // Determine each screen's position QList screens = _outputMap.values(); // Nothing connected? if (screens.isEmpty()) return; qSort(screens.begin(), screens.end(), xRectLessThan); int endX = -0xffff; qDebug() << "From left to right"; for (OutputInfo* output : screens) { if (output->mode == nullptr) { qDebug() << "(Ignoring" << output->outputName << "since it's disconnected)"; continue; } if (output->crtc->x >= endX) { QSize res(0, 0); res = QSize(int(output->mode->width), int(output->mode->height)); _resolutions.append(res); endX = -0xffff; // Reset } output->position = _resolutions.size() - 1; qDebug() << "Screen (" << output->crtc->x << "," << output->crtc->y << ") @" << output->mode->width << "x" << output->mode->height << "as screen" << output->position; if (output->crtc->x + int(output->crtc->width) > endX) { endX = output->crtc->x + int(output->mode->width); } } qDebug() << "Loaded."; } bool XPrivate::readEdid(OutputInfo* output) { int numProps = 0; bool found = false; Atom* properties = XRRListOutputProperties(_display, output->id, &numProps); for (int i = 0; i < numProps; ++i) { if (properties[i] == _EDID_ATOM) { found = true; break; } } XFree(properties); if (!found) return false; unsigned long nitems, bytes_after; unsigned char *prop; int actual_format; Atom actual_type; bool valid; XRRGetOutputProperty(_display, output->id, _EDID_ATOM, 0, 128, False, False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop); valid = (actual_format == 8 && nitems >= 128); if (valid) { int idx; for (unsigned char *byte = prop + 54; byte < prop + 126; byte += 18) { if (byte[0] != 0 || byte[1] != 0 || byte[2] != 0) continue; // Not a text block if (byte[3] == 0xfc) { // Serial number output->modelName = QString::fromLatin1(reinterpret_cast(byte) + 5, 13); // It's actually CP-437 but meh if ((idx = output->modelName.indexOf('\r')) != -1) { output->modelName.truncate(idx); } output->modelName = output->modelName.trimmed(); qDebug() << "Display name:" << output->modelName; } } } XFree(prop); return valid; } void XPrivate::setScreenSize(const QSize &size) { XRRSetScreenSize(_display, DefaultRootWindow(_display), size.width(), size.height(), int(25.4 * size.width() / 96.0), // standard dpi that X uses int(25.4 * size.height() / 96.0)); // standard dpi that X uses } void XPrivate::disconnectAllCrtcs() { // Disconnect everything for (auto crtc : _crtcMap.keys()) { XRRSetCrtcConfig(_display, _screenResources, crtc, CurrentTime, 0, 0, None, RR_Rotate_0, nullptr, 0); } } void XPrivate::freeCrtcBackup() { for (auto entry : _crtcBackup) { free(entry->outputs); free(entry); } _crtcBackup.clear(); qDebug() << "CRTC freed"; } void XPrivate::createCrtcBackup() { freeCrtcBackup(); for (CrtcMap::iterator it = _crtcMap.begin(); it != _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]; } _crtcBackup[it.key()] = copy; } qDebug() << "Created CRTC backup with entries:" << _crtcBackup.size(); } /** * Copy first "num" modes from output to all other outputs if they * dont have a suitable mode yet. */ void XPrivate::copyModesToAll(RROutput sourceId, int num) { const XRROutputInfo *outputInfo = _outputMap[sourceId]->output; for (int i = 0; i < num; ++i) { if (!_modeMap.contains(outputInfo->modes[i])) { qDebug() << "BUG: Mode" << outputInfo->modes[i] << "not found in copyModesToAll"; continue; } const XRRModeInfo *mode = _modeMap[outputInfo->modes[i]]; for (auto other : _outputMap.keys()) { if (other == sourceId) continue; const XRROutputInfo *otherInfo = _outputMap[other]->output; if (getOutputModeForResolution(otherInfo, mode->width, mode->height) != None) continue; XRRAddOutputMode(_display, other, outputInfo->modes[i]); } } } /** * Get the RRMode for the specified output matching the given resolution. * If multiple modes match the given resolution, the function will * return the first matching preferred mode. If no preferred * mode matches, the non-preferred mode with the highest refresh rate * will be returned. * Otherwise, None will be returned. */ RRMode XPrivate::getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const { XRRModeInfo *retval = nullptr; for (int i = 0; i < output->nmode; ++i) { if (!_modeMap.contains(output->modes[i])) continue; XRRModeInfo *info = _modeMap[output->modes[i]]; if (info->width == width && info->height == height) { if (i < output->npreferred) return output->modes[i]; if (retval == nullptr || retval->dotClock < info->dotClock) { retval = info; } } } return retval == nullptr ? None : retval->id; } RRMode XPrivate::getOutputModeForResolution(const XRROutputInfo *output, const QSize &resolution) const { return getOutputModeForResolution(output, static_cast(resolution.width()), static_cast(resolution.height())); } RRCrtc XPrivate::getFreeCrtc(const XRROutputInfo* output) const { for (int i = 0; i < output->ncrtc; ++i) { RRCrtc c = output->crtcs[i]; if (!_crtcMap.contains(c)) { // Unknown CRTC!? Scan already known outputs qDebug() << "BUG: Output has unknown possible CRTC"; for (auto oi : _outputMap) { if (oi->output->crtc == c) goto next; } } else { // Known CRTC -- see if free if (_crtcMap[c]->noutput > 0) goto next; } return c; next:; } return None; } XRRModeInfo* XPrivate::getPreferredMode(OutputInfo *oi) const { if (oi->output->nmode == 0) { qDebug() << "getPreferredMode: Output" << oi->outputName << "has no modes!?"; return nullptr; // WTF!? } RRMode mode; if (oi->output->npreferred > 0) { mode = oi->output->modes[0]; } else { mode = getOutputModeForResolution(oi->output, 1920, 1080); if (mode == None) { mode = getOutputModeForResolution(oi->output, 1280, 720); } if (mode == None) { mode = getOutputModeForResolution(oi->output, 1280, 800); } if (mode == None) { mode = oi->output->modes[0]; } } if (!_modeMap.contains(mode)) return nullptr; return _modeMap[mode]; } QList XPrivate::getTotalSize(const QList &projectors, const QList &screens) const { const int max = qMax(screens.size(), projectors.size()); QList modes; for (int i = 0; i < max; ++i) { XRRModeInfo *mode = nullptr; if (i < projectors.size()) { mode = getPreferredMode(projectors.at(i)); } if (mode == nullptr && i < screens.size()) { mode = getPreferredMode(screens.at(i)); } if (mode != nullptr) { modes.append(QSize(int(mode->width), int(mode->height))); } } return modes; } bool XPrivate::setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun) { RRMode mode = getOutputModeForResolution(oi->output, size); if (mode == None) { qDebug() << "Cannot set" << oi->outputName << "to" << size << " since it's not supported"; if (oi->output->nmode == 0) return false; qDebug() << "falling back to its default"; mode = oi->output->modes[0]; } if (!dryRun) { XRRSetCrtcConfig(_display, _screenResources, oi->output->crtc, CurrentTime, x, y, mode, RR_Rotate_0, &oi->id, 1); } qDebug() << "Set" << oi->outputName << "to" << _modeMap[mode]->width << "x" << _modeMap[mode]->height << "-- offset" << x << "/" << y; return true; }