#include "xprivate.h" #include #include // Put this here as we need it in the static error handler static Display* __display; static XErrorHandler old_handler; static double toVertRefresh(const XRRModeInfo *mode) { if (mode->hTotal > 0 && mode->vTotal > 0) return (double(mode->dotClock) / (double(mode->hTotal) * double(mode->vTotal))); return 0; } /** * 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; } static int modeEqual(const XRRModeInfo *a, const XRRModeInfo *b) { return (a->width == b->width && a->height == b->height && a->dotClock / 10 == b->dotClock / 10 && a->hSyncStart == b->hSyncStart && a->hSyncEnd == b->hSyncEnd && a->hTotal == b->hTotal && a->hSkew == b->hSkew && a->vSyncStart == b->vSyncStart && a->vSyncEnd == b->vSyncEnd && a->vTotal == b->vTotal && a->modeFlags == b->modeFlags); } static int errorHandler(Display *dpy, XErrorEvent *err) { if (dpy != __display) { if (old_handler != nullptr) { return old_handler(dpy, err); } } else { char buf[1000]; XGetErrorText(dpy, err->error_code, buf, 1000); qDebug() << "**X11 error**" << buf; } return 0; } /* * Class members */ XPrivate::XPrivate() : _screenResources(nullptr) { // Get initial data (to be freed) __display = _display = XOpenDisplay(nullptr); if (_display == nullptr) { qFatal("Cannot open display"); ::exit(1); } old_handler = XSetErrorHandler(errorHandler); _EDID_ATOM = XInternAtom(_display, RR_PROPERTY_RANDR_EDID, False); } XPrivate::~XPrivate() { XCloseDisplay(_display); } void XPrivate::freeResources() { // 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() << "updateScreenResources: 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() << "updateScreenResources: 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() << "updateScreenResources: 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 && info->connection == RR_Connected) { 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() << "updateScreenResources: 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() << "updateScreenResources: Loaded."; } void XPrivate::addMissingEdidResolutions() { qDebug() << "addMissingEdidResolutions"; XRRScreenResources *resptr = XRRGetScreenResourcesCurrent(_display, DefaultRootWindow(_display)); if (resptr == nullptr) return; for (int i = 0; i < resptr->noutput; ++i) { unsigned char prop[512]; unsigned long edidLen = 512; if (!getEdid(resptr->outputs[i], prop, &edidLen)) continue; if (edidLen < 128) continue; for (unsigned int j = 54; j < 125; j+= 18) { addMissingModesFromDtd(resptr, resptr->outputs[i], prop + j); } for (unsigned int j = 128; j < edidLen - 127; j += 128) { addMissingModesFromExtBlock(resptr, resptr->outputs[i], prop + j); } } XRRFreeScreenResources(resptr); } bool XPrivate::getEdid(RROutput outputId, unsigned char *buffer, unsigned long *size) { int numProps = 0; bool found = false; qDebug() << "getEdid:" << outputId; Atom* properties = XRRListOutputProperties(_display, outputId, &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 = nullptr; int actual_format; Atom actual_type; bool valid; XRRGetOutputProperty(_display, outputId, _EDID_ATOM, 0, long(size), False, False, AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &prop); valid = (actual_format == 8 && nitems >= 128 && prop != nullptr); if (valid) { memcpy(buffer, prop, qMin(*size, nitems)); *size = qMin(nitems, *size); } else { *size = 0; } if (prop != nullptr) { XFree(prop); } return valid; } bool XPrivate::readEdid(OutputInfo* output) { unsigned char prop[512]; unsigned long edidLen = 512; if (!getEdid(output->id, prop, &edidLen)) return false; 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; } } return true; } void XPrivate::addMissingModesFromExtBlock(XRRScreenResources *res, RROutput outputId, unsigned char *data) { if (data[0] != 2) // Not type 2, skip return; if (data[2] == 0) // Empty block return; int dtdOffset = data[2]; if (dtdOffset < 4) // No DTD blocks return; if (dtdOffset > 128) { dtdOffset = 128; } qDebug() << "addMissingModesFromExtBlock:" << outputId; for (int i = dtdOffset; i < 128 - 17; i += 18) { addMissingModesFromDtd(res, outputId, data + i); } } void XPrivate::addMissingModesFromDtd(XRRScreenResources *res, RROutput outputId, unsigned char *data) { if (data[0] == 0 && data[1] == 0) return; int hactive, vactive, pixclk, hsyncoff, hsyncwidth, hblank, vsyncoff, vsyncwidth, vblank; hactive = data[2] + ((data[4] & 0xf0) << 4); hblank = data[3] + ((data[4] & 0x0f) << 8); vactive = data[5] + ((data[7] & 0xf0) << 4); vblank = data[6] + ((data[7] & 0x0f) << 8); pixclk = (data[1] << 8) | (data[0]); // 10kHz hsyncoff = data[8] | ((data[11] & 0xC0) << 2); hsyncwidth = data[9] | ((data[11] & 0x30) << 4); vsyncoff = ((data[10] & 0xf0) >> 4) | ((data[11] & 0x0C) << 2); vsyncwidth = (data[10] & 0x0f) | ((data[11] & 0x03) << 4); char buf[100]; XRRModeInfo m; memset(&m, 0, sizeof(m)); m.width = static_cast(hactive); m.height = static_cast(vactive); m.dotClock = static_cast(pixclk) * 10ul * 1000ul; m.hSyncStart= static_cast(hactive + hsyncoff); m.hSyncEnd = static_cast(hactive + hsyncoff + hsyncwidth); m.hTotal = static_cast(hactive + hblank); m.hSkew = 0; m.vSyncStart= static_cast(vactive + vsyncoff); m.vSyncEnd = static_cast(vactive + vsyncoff + vsyncwidth); m.vTotal = static_cast(vactive + vblank); m.id = 0; m.name = buf; m.nameLength = static_cast(snprintf(buf, sizeof(buf), "%dx%d_%d", hactive, vactive, static_cast(m.dotClock / (m.hTotal * m.vTotal)))); if (m.nameLength > sizeof(buf)) { m.nameLength = sizeof(buf); } if ((data[17] & 0x18) == 0x18) { // Digital Separate if (data[17] & 0x04) { m.modeFlags |= RR_VSyncPositive; } else { m.modeFlags |= RR_VSyncNegative; } if (data[17] & 0x02) { m.modeFlags |= RR_HSyncPositive; } else { m.modeFlags |= RR_HSyncNegative; } } else if ((data[17] & 0x18) == 0x10) { // Digital Composite if (data[17] & 0x02) { m.modeFlags |= RR_HSyncPositive; } else { m.modeFlags |= RR_HSyncNegative; } m.modeFlags |= RR_VSyncNegative; // Default? Clarify... } else { m.modeFlags |= RR_VSyncNegative | RR_HSyncNegative; // ... } if (data[17] & 0x80) { m.modeFlags |= RR_Interlace; } // See if we should add this mode RRMode modeId = 0; for (int mode = 0; mode < res->nmode; ++mode) { if (modeEqual(&res->modes[mode], &m)) { modeId = res->modes[mode].id; break; } } if (modeId == 0) { modeId = XRRCreateMode(_display, DefaultRootWindow(_display), &m); } if (modeId != 0) { XRRAddOutputMode(_display, outputId, modeId); } } 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()) { Status s = XRRSetCrtcConfig(_display, _screenResources, crtc, CurrentTime, 0, 0, None, RR_Rotate_0, nullptr, 0); if (s != RRSetConfigSuccess) { qDebug() << "Disconnecting CRTC" << crtc << "failed"; } } } /** * 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).isEmpty()) continue; XRRAddOutputMode(_display, other, outputInfo->modes[i]); } } } /** * Get all known modes matching the given resolution. * If multiple modes match the given resolution, the function will * put the preferred resolution first, then all others sorted by * dotClock, descending. */ QList XPrivate::getOutputModeForResolution(const XRROutputInfo *output, unsigned int width, unsigned int height) const { XRRModeInfo *pref = nullptr; QList others; 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 && pref == nullptr) { pref = info; } else { others.append(info); } } } // Sort others descending by dotClock qSort(others.begin(), others.end(), [](const XRRModeInfo *a, const XRRModeInfo *b) { return toVertRefresh(a) > toVertRefresh(b); }); QList ret; if (pref != nullptr) { ret.append(pref->id); } for (auto e : others) { ret.append(e->id); } return ret; } QList 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(const OutputInfo *oi, XRRModeInfo *fallback) const { if (oi->output->nmode == 0) { qDebug() << "getPreferredMode: Output" << oi->outputName << "has no modes!?"; return nullptr; // WTF!? } RRMode mode; // H4xx0r - Ultrawide screens seem to incorrectly report a 16:9 resultion as their preferred one // I can only guess this is a safety measure for old/bad OS or gfx cards // So, determine the aspect ratio by looking at the physical size and if it's ultrawide, look for a // more fitting resolution in the list of supported ones // UPDATE: Further digging using edid-decode suggests that Xorg parses EDID data improperly, most notably // not preferring the resolution given in the first DTD block in the base section (first 128b) but rather // going for the first DTD in the CTA extension block. (This is just a guess since I didn't check the // Xorg source code.) if (oi->output->mm_height > 0) { float ar = float(oi->output->mm_width) / float(oi->output->mm_height); if (ar >= 1.999f) { // Consider 18:9+ as ultrawide XRRModeInfo *best = nullptr; for (int i = 0; i < oi->output->nmode; ++i) { if (!_modeMap.contains(oi->output->modes[i])) continue; auto mode = _modeMap[oi->output->modes[i]]; if (mode->height == 0) continue; float resAr = float(mode->width) / float(mode->height); if (qAbs(resAr - ar) > 0.02f) // Check if aspect ratios are same continue; if (best == nullptr || mode->width > best->width) { best = mode; // If multiple resolutions match, use the largest one } } if (best != nullptr) { qDebug() << oi->outputName << "Overriding UltraWide"; return best; // Ultrawide it is! } } } // "Normal" way of determining optimal resolution if (oi->output->npreferred > 0) { mode = oi->output->modes[0]; qDebug() << "Picking preferred mode for" << oi->outputName; } else { // No preferred one, try some more or less clever fallback qDebug() << "No preferred mode for" << oi->outputName; mode = None; int maxX = 1920; int maxY = 1080; if (fallback != nullptr) { maxX = int(fallback->width); maxY = int(fallback->height); auto list = getOutputModeForResolution(oi->output, fallback->width, fallback->height); if (!list.isEmpty()) { mode = list.first(); } } static const std::vector wanted = {QSize(1920, 1080), QSize(1280, 800), QSize(1280, 720), QSize(1152, 864), QSize(1024, 768)}; for (const QSize s : wanted) { if (mode != None) break; if (s.width() <= maxX && s.height() <= maxY) { auto list = getOutputModeForResolution(oi->output, s.width(), s.height()); if (!list.isEmpty()) { mode = list.first(); } } } if (mode == None) { mode = oi->output->modes[0]; } } if (!_modeMap.contains(mode)) { qDebug() << "Could not pick a preferred mode"; return nullptr; } qDebug() << "Final Preferred mode of" << oi->outputName << "is" << _modeMap[mode]->width << "x" << _modeMap[mode]->height; 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 < screens.size()) { mode = getPreferredMode(screens.at(i)); if (mode != nullptr) { qDebug() << "Screen wants" << mode->width << "x" << mode->height; } } if (i < projectors.size()) { mode = getPreferredMode(projectors.at(i), mode); if (mode != nullptr) { qDebug() << "Projector wants" << mode->width << "x" << mode->height; } } if (mode != nullptr) { QSize size(int(mode->width), int(mode->height)); modes.append(size); qDebug() << "Next screen:" << size; } else { qDebug() << "Empty screen :("; } } return modes; } bool XPrivate::setOutputResolution(OutputInfo *oi, int x, int y, const QSize &size, bool dryRun) { QList modes = getOutputModeForResolution(oi->output, size); Status s = RRSetConfigSuccess; if (modes.isEmpty()) { 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"; modes.append(oi->output->modes[0]); } if (!dryRun) { for (RRMode mode : modes) { qDebug() << "Trying" << toVertRefresh(_modeMap[mode]) << "Hz"; s = XRRSetCrtcConfig(_display, _screenResources, oi->output->crtc, CurrentTime, x, y, mode, RR_Rotate_0, &oi->id, 1); if (s == RRSetConfigSuccess) { if (x == 0 && y == 0) { XRRSetOutputPrimary(_display, DefaultRootWindow(_display), oi->id); } break; } } } qDebug() << (s != RRSetConfigSuccess ? "Failed to" : "") << "Set" << oi->outputName << "to" << size << "-- offset" << x << "/" << y; if (s == RRSetConfigSuccess) { return true; } return false; }