summaryrefslogblamecommitdiffstats
path: root/src/xprivate.cpp
blob: 2f4cd5514afd58f22b021b86541cd1d313c0e3a1 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13


                     
                             








                                                                               











                                                                                        







































































                                                                                           
                          


                                                                       
                                                           








































































































                                                                                                                                                                            
















                                                                                                                                                                                      
















































































































































































































































                                                                                                                                                     
#include "xprivate.h"

#include <QDebug>
#include <QRegularExpression>

/**
 * 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<RROutput, QString> tempMap;
    QMap<Projector, int> 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<OutputInfo*> 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<const char*>(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<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];
                }
                _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<unsigned int>(resolution.width()), static_cast<unsigned int>(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<QSize> XPrivate::getTotalSize(const QList<OutputInfo*> &projectors, const QList<OutputInfo*> &screens) const
{
        const int max = qMax(screens.size(), projectors.size());
        QList<QSize> 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;
}