From 4dd7aeee10fc4d3883fd26fd70662405b533b6b4 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Thu, 5 Feb 2015 15:45:53 +0100 Subject: [PATCH] Add support for GPU bug list reading. - Add support for reading out feature lists from GPU bug lists in Chromium format (supporting OS + version, exceptions lists, device ids and vendor ids). Add a overloads allowing for passing file name, data or JSON documents. - Test reading in tst_qopenglconfig - Prototypically have the Windows plugin read the file from the environment variable "QT_OPENGL_BUGLIST" and turn off renderers depending on the standard keyword "disable_d3d11" and newly introduced keywords "disable_d3d9", "disable_desktopgl". - QT_OPENGL_BUGLIST can point to an absolute or relative path, in which case it is resolved via QLibraryInfo::SettingsPath and QStandardPaths::ConfigLocation. [ChangeLog][QtGui][Windows] Introduce experimental feature allowing the user to specify a GPU driver buglist with some additional keywords to chooose the GL renderer backend depending on GPU. Task-number: QTBUG-43263 Change-Id: I8c20b0e8ee3cf575b91d0672bf405c42d3e9fb64 Reviewed-by: Laszlo Agocs --- src/gui/opengl/qopengl.cpp | 369 ++++++++++++++++++ src/gui/opengl/qopengl_p.h | 26 ++ .../windows/qwindowsopengltester.cpp | 60 +++ tests/auto/gui/qopenglconfig/buglist.json | 106 +++++ .../auto/gui/qopenglconfig/qopenglconfig.pro | 1 + .../gui/qopenglconfig/tst_qopenglconfig.cpp | 37 ++ 6 files changed, 599 insertions(+) create mode 100644 tests/auto/gui/qopenglconfig/buglist.json diff --git a/src/gui/opengl/qopengl.cpp b/src/gui/opengl/qopengl.cpp index f700d65b17..61d0614724 100644 --- a/src/gui/opengl/qopengl.cpp +++ b/src/gui/opengl/qopengl.cpp @@ -37,6 +37,15 @@ #include "qopenglcontext.h" #include "qopenglfunctions.h" +#include +#include +#include +#include +#include +#include +#include +#include + QT_BEGIN_NAMESPACE #if defined(QT_OPENGL_3) @@ -79,4 +88,364 @@ QOpenGLExtensionMatcher::QOpenGLExtensionMatcher() } } +/* Helpers to read out the list of features matching a device from + * a Chromium driver bug list of the format using a subset of keys + * (namely, matching by gl_vendor RegExp is not implemented): + { + "entries": [ + { + "id": 20, + "description": "Disable EXT_draw_buffers on GeForce GT 650M on Linux due to driver bugs", + "os": { + "type": "linux" + }, + // Optional: "exceptions" list + "vendor_id": "0x10de", + "device_id": ["0x0fd5"], + "multi_gpu_category": "any", + "features": [ + "disable_ext_draw_buffers" + ] + }, + .... + } +*/ + +QDebug operator<<(QDebug d, const QOpenGLConfig::Gpu &g) +{ + QDebugStateSaver s(d); + d.nospace(); + d << "Gpu("; + if (g.isValid()) { + d << "vendor=" << hex << showbase <", ">="}; + +static inline QString valueKey() { return QStringLiteral("value"); } +static inline QString opKey() { return QStringLiteral("op"); } +static inline QString versionKey() { return QStringLiteral("version"); } +static inline QString typeKey() { return QStringLiteral("type"); } +static inline QString osKey() { return QStringLiteral("os"); } +static inline QString vendorIdKey() { return QStringLiteral("vendor_id"); } +static inline QString glVendorKey() { return QStringLiteral("gl_vendor"); } +static inline QString deviceIdKey() { return QStringLiteral("device_id"); } +static inline QString driverVersionKey() { return QStringLiteral("driver_version"); } +static inline QString featuresKey() { return QStringLiteral("features"); } +static inline QString idKey() { return QStringLiteral("id"); } +static inline QString descriptionKey() { return QStringLiteral("description"); } +static inline QString exceptionsKey() { return QStringLiteral("exceptions"); } + +namespace { +// VersionTerm describing a version term consisting of number and operator +// found in "os", "driver_version", "gl_version". +struct VersionTerm { + VersionTerm() : op(NotEqual) {} + static VersionTerm fromJson(const QJsonValue &v); + bool isNull() const { return number.isNull(); } + bool matches(const QVersionNumber &other) const; + + QVersionNumber number; + Operator op; +}; + +bool VersionTerm::matches(const QVersionNumber &other) const +{ + if (isNull() || other.isNull()) { + qWarning() << Q_FUNC_INFO << "called with invalid parameters"; + return false; + } + switch (op) { + case NotEqual: + return other != number; + case LessThan: + return other < number; + case LessEqualThan: + return other <= number; + case Equals: + return other == number; + case GreaterThan: + return other > number; + case GreaterEqualThan: + return other >= number; + } + return false; +} + +VersionTerm VersionTerm::fromJson(const QJsonValue &v) +{ + VersionTerm result; + if (!v.isObject()) + return result; + const QJsonObject o = v.toObject(); + result.number = QVersionNumber::fromString(o.value(valueKey()).toString()); + const QString opS = o.value(opKey()).toString(); + for (size_t i = 0; i < sizeof(operators) / sizeof(operators[0]); ++i) { + if (opS == QLatin1String(operators[i])) { + result.op = static_cast(i); + break; + } + } + return result; +} + +// OS term consisting of name and optional version found in +// under "os" in main array and in "exceptions" lists. +struct OsTypeTerm +{ + static OsTypeTerm fromJson(const QJsonValue &v); + static QString hostOs(); + static QVersionNumber hostKernelVersion() { return QVersionNumber::fromString(QSysInfo::kernelVersion()); } + + bool isNull() const { return type.isEmpty(); } + bool matches(const QString &osName, const QVersionNumber &kernelVersion) const + { + if (isNull() || osName.isEmpty() || kernelVersion.isNull()) { + qWarning() << Q_FUNC_INFO << "called with invalid parameters"; + return false; + } + if (type != osName) + return false; + return versionTerm.isNull() || versionTerm.matches(kernelVersion); + } + + QString type; + VersionTerm versionTerm; +}; + +OsTypeTerm OsTypeTerm::fromJson(const QJsonValue &v) +{ + OsTypeTerm result; + if (!v.isObject()) + return result; + const QJsonObject o = v.toObject(); + result.type = o.value(typeKey()).toString(); + result.versionTerm = VersionTerm::fromJson(o.value(versionKey())); + return result; +} + +QString OsTypeTerm::hostOs() +{ + // Determine Host OS. +#if defined(Q_OS_WIN) + return QStringLiteral("win"); +#elif defined(Q_OS_LINUX) + return QStringLiteral("linux"); +#elif defined(Q_OS_OSX) + return QStringLiteral("macosx"); +#elif defined(Q_OS_ANDROID) + return QStringLiteral("android"); +#else + return QString(); +#endif +} +} // anonymous namespace + +typedef QJsonArray::ConstIterator JsonArrayConstIt; + +static inline bool contains(const QJsonArray &a, unsigned needle) +{ + for (JsonArrayConstIt it = a.constBegin(), cend = a.constEnd(); it != cend; ++it) { + if (needle == it->toString().toUInt(Q_NULLPTR, /* base */ 0)) + return true; + } + return false; +} + +static QString msgSyntaxWarning(const QJsonObject &object, const QString &what) +{ + QString result; + QTextStream(&result) << "Id " << object.value(idKey()).toInt() + << " (\"" << object.value(descriptionKey()).toString() + << "\"): " << what; + return result; +} + +// Check whether an entry matches. Called recursively for +// "exceptions" list. + +static bool matches(const QJsonObject &object, + const QString &osName, + const QVersionNumber &kernelVersion, + const QOpenGLConfig::Gpu &gpu) +{ + const OsTypeTerm os = OsTypeTerm::fromJson(object.value(osKey())); + if (!os.isNull() && !os.matches(osName, kernelVersion)) + return false; + + const QJsonValue exceptionsV = object.value(exceptionsKey()); + if (exceptionsV.isArray()) { + const QJsonArray exceptionsA = exceptionsV.toArray(); + for (JsonArrayConstIt it = exceptionsA.constBegin(), cend = exceptionsA.constEnd(); it != cend; ++it) { + if (matches(it->toObject(), osName, kernelVersion, gpu)) + return false; + } + } + + const QJsonValue vendorV = object.value(vendorIdKey()); + if (vendorV.isString()) { + if (gpu.vendorId != vendorV.toString().toUInt(Q_NULLPTR, /* base */ 0)) + return false; + } else { + if (object.contains(glVendorKey())) { + qWarning().nospace() << "Id " << object.value(idKey()).toInt() + << ": Matching by " << glVendorKey() << " is not implemented."; + return false; + } + } + + if (gpu.deviceId) { + const QJsonValue deviceIdV = object.value(deviceIdKey()); + switch (deviceIdV.type()) { + case QJsonValue::Array: + if (!contains(deviceIdV.toArray(), gpu.deviceId)) + return false; + break; + case QJsonValue::Undefined: + case QJsonValue::Null: + break; + default: + qWarning().noquote() + << msgSyntaxWarning(object, + QLatin1String("Device ID must be of type array.")); + } + } + if (!gpu.driverVersion.isNull()) { + const QJsonValue driverVersionV = object.value(driverVersionKey()); + switch (driverVersionV.type()) { + case QJsonValue::Object: + if (!VersionTerm::fromJson(driverVersionV).matches(gpu.driverVersion)) + return false; + break; + case QJsonValue::Undefined: + case QJsonValue::Null: + break; + default: + qWarning().noquote() + << msgSyntaxWarning(object, + QLatin1String("Driver version must be of type object.")); + } + } + return true; +} + +static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, + const QString &osName, + const QVersionNumber &kernelVersion, + const QJsonDocument &doc, + QSet *result, + QString *errorMessage) +{ + result->clear(); + errorMessage->clear(); + const QJsonValue entriesV = doc.object().value(QStringLiteral("entries")); + if (!entriesV.isArray()) { + *errorMessage = QLatin1String("No entries read."); + return false; + } + + const QJsonArray entriesA = entriesV.toArray(); + for (JsonArrayConstIt eit = entriesA.constBegin(), ecend = entriesA.constEnd(); eit != ecend; ++eit) { + if (eit->isObject()) { + const QJsonObject object = eit->toObject(); + if (matches(object, osName, kernelVersion, gpu)) { + const QJsonValue featuresListV = object.value(featuresKey()); + if (featuresListV.isArray()) { + const QJsonArray featuresListA = featuresListV.toArray(); + for (JsonArrayConstIt fit = featuresListA.constBegin(), fcend = featuresListA.constEnd(); fit != fcend; ++fit) + result->insert(fit->toString()); + } + } + } + } + return true; +} + +static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, + const QString &osName, + const QVersionNumber &kernelVersion, + const QByteArray &jsonAsciiData, + QSet *result, QString *errorMessage) +{ + result->clear(); + errorMessage->clear(); + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(jsonAsciiData, &error); + if (document.isNull()) { + const int lineNumber = 1 + jsonAsciiData.left(error.offset).count('\n'); + QTextStream str(errorMessage); + str << "Failed to parse data: \"" << error.errorString() + << "\" at line " << lineNumber << " (offset: " + << error.offset << ")."; + return false; + } + return readGpuFeatures(gpu, osName, kernelVersion, document, result, errorMessage); +} + +static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, + const QString &osName, + const QVersionNumber &kernelVersion, + const QString &fileName, + QSet *result, QString *errorMessage) +{ + result->clear(); + errorMessage->clear(); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QTextStream str(errorMessage); + str << "Cannot open \"" << QDir::toNativeSeparators(fileName) << "\": " + << file.errorString(); + return false; + } + const bool success = readGpuFeatures(gpu, osName, kernelVersion, file.readAll(), result, errorMessage); + if (!success) { + errorMessage->prepend(QLatin1String("Error reading \"") + + QDir::toNativeSeparators(fileName) + + QLatin1String("\": ")); + } + return success; +} + +QSet QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu, + const QString &osName, + const QVersionNumber &kernelVersion, + const QJsonDocument &doc) +{ + QSet result; + QString errorMessage; + if (!readGpuFeatures(gpu, osName, kernelVersion, doc, &result, &errorMessage)) + qWarning().noquote() << errorMessage; + return result; +} + +QSet QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu, + const QString &osName, + const QVersionNumber &kernelVersion, + const QString &fileName) +{ + QSet result; + QString errorMessage; + if (!readGpuFeatures(gpu, osName, kernelVersion, fileName, &result, &errorMessage)) + qWarning().noquote() << errorMessage; + return result; +} + +QSet QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QJsonDocument &doc) +{ + return gpuFeatures(gpu, OsTypeTerm::hostOs(), OsTypeTerm::hostKernelVersion(), doc); +} + +QSet QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QString &fileName) +{ + return gpuFeatures(gpu, OsTypeTerm::hostOs(), OsTypeTerm::hostKernelVersion(), fileName); +} + + QT_END_NAMESPACE diff --git a/src/gui/opengl/qopengl_p.h b/src/gui/opengl/qopengl_p.h index a82195757d..377440455a 100644 --- a/src/gui/opengl/qopengl_p.h +++ b/src/gui/opengl/qopengl_p.h @@ -48,9 +48,13 @@ #include #include #include +#include +#include QT_BEGIN_NAMESPACE +class QJsonDocument; + class Q_GUI_EXPORT QOpenGLExtensionMatcher { public: @@ -67,6 +71,28 @@ private: QSet m_extensions; }; +class Q_GUI_EXPORT QOpenGLConfig +{ +public: + struct Gpu { + Gpu() : vendorId(0), deviceId(0) {} + bool isValid() const { return deviceId; } + + uint vendorId; + uint deviceId; + QVersionNumber driverVersion; + }; + + static QSet gpuFeatures(const Gpu &gpu, + const QString &osName, const QVersionNumber &kernelVersion, + const QJsonDocument &doc); + static QSet gpuFeatures(const Gpu &gpu, + const QString &osName, const QVersionNumber &kernelVersion, + const QString &fileName); + static QSet gpuFeatures(const Gpu &gpu, const QJsonDocument &doc); + static QSet gpuFeatures(const Gpu &gpu, const QString &fileName); +}; + QT_END_NAMESPACE #endif // QOPENGL_H diff --git a/src/plugins/platforms/windows/qwindowsopengltester.cpp b/src/plugins/platforms/windows/qwindowsopengltester.cpp index 12d45f432b..ba4c95544e 100644 --- a/src/plugins/platforms/windows/qwindowsopengltester.cpp +++ b/src/plugins/platforms/windows/qwindowsopengltester.cpp @@ -38,6 +38,12 @@ #include #include #include +#include +#include +#include +#include + +#include #ifndef Q_OS_WINCE # include @@ -183,6 +189,57 @@ QWindowsOpenGLTester::Renderer QWindowsOpenGLTester::requestedRenderer() return QWindowsOpenGLTester::InvalidRenderer; } +#ifndef Q_OS_WINCE + +static inline QString resolveBugListFile(const QString &fileName) +{ + if (QFileInfo(fileName).isAbsolute()) + return fileName; + // Try QLibraryInfo::SettingsPath which is typically empty unless specified in qt.conf, + // then resolve via QStandardPaths::ConfigLocation. + const QString settingsPath = QLibraryInfo::location(QLibraryInfo::SettingsPath); + if (!settingsPath.isEmpty()) { // SettingsPath is empty unless specified in qt.conf. + const QFileInfo fi(settingsPath + QLatin1Char('/') + fileName); + if (fi.isFile()) + return fi.absoluteFilePath(); + } + return QStandardPaths::locate(QStandardPaths::ConfigLocation, fileName); +} + +static void readDriverBugList(const GpuDescription &gpu, + QWindowsOpenGLTester::Renderers *result) +{ + const char bugListFileVar[] = "QT_OPENGL_BUGLIST"; + if (!qEnvironmentVariableIsSet(bugListFileVar)) + return; + const QString fileName = resolveBugListFile(QFile::decodeName(qgetenv(bugListFileVar))); + if (fileName.isEmpty()) + return; + QOpenGLConfig::Gpu qgpu; + qgpu.deviceId = gpu.deviceId; + qgpu.vendorId = gpu.vendorId; + qgpu.driverVersion = gpu.driverVersion; + const QSet features = QOpenGLConfig::gpuFeatures(qgpu, fileName); + if (features.contains(QStringLiteral("disable_desktopgl"))) { // Qt-specific + qCWarning(lcQpaGl) << "Disabling Desktop GL: " << gpu; + *result &= ~QWindowsOpenGLTester::DesktopGl; + } + if (features.contains(QStringLiteral("disable_angle"))) { // Qt-specific keyword + qCWarning(lcQpaGl) << "Disabling ANGLE: " << gpu; + *result &= ~QWindowsOpenGLTester::GlesMask; + } else { + if (features.contains(QStringLiteral("disable_d3d11"))) { // standard keyword + qCWarning(lcQpaGl) << "Disabling D3D11: " << gpu; + *result &= ~QWindowsOpenGLTester::AngleRendererD3d11; + } + if (features.contains(QStringLiteral("disable_d3d9"))) { // Qt-specific + qCWarning(lcQpaGl) << "Disabling D3D9: " << gpu; + *result &= ~QWindowsOpenGLTester::AngleRendererD3d9; + } + } +} +#endif // !Q_OS_WINCE + static inline QWindowsOpenGLTester::Renderers detectSupportedRenderers(const GpuDescription &gpu, bool glesOnly) { @@ -196,6 +253,9 @@ static inline QWindowsOpenGLTester::Renderers if (!glesOnly && QWindowsOpenGLTester::testDesktopGL()) result |= QWindowsOpenGLTester::DesktopGl; + + readDriverBugList(gpu, &result); + return result; #else // !Q_OS_WINCE return QWindowsOpenGLTester::Gles; diff --git a/tests/auto/gui/qopenglconfig/buglist.json b/tests/auto/gui/qopenglconfig/buglist.json new file mode 100644 index 0000000000..d2d06645aa --- /dev/null +++ b/tests/auto/gui/qopenglconfig/buglist.json @@ -0,0 +1,106 @@ +{ + "name": "gpu driver bug list example", + "version": "0.9", + "entries": [ + { + "id": 1, + "description": "non-applicable feature: different OS", + "vendor_id": "0x10de", + "device_id": [ "0x0DE9" ], + "os": { + "type": "android" + }, + "features": [ + "wrong_feature1" + ] + }, + { + "id": 2, + "description": "non-applicable feature: ancient OS", + "vendor_id": "0x10de", + "device_id": [ "0x0DE9" ], + "os": { + "type": "win", + "version": { + "op": "<", + "value": "4.0" + } + }, + "features": [ + "wrong_feature2" + ] + }, + { + "id": 3, + "description": "non-applicable feature: excluded OS", + "vendor_id": "0x10de", + "device_id": [ "0x0DE9" ], + "exceptions": [ + { + "os": { + "type": "win" + } + } + ], + "features": [ + "wrong_feature3" + ] + }, + { + "id": 4, + "description": "non-applicable feature: wrong vendor", + "vendor_id": "0x10df", + "device_id": [ "0x0DE9" ], + "os": { + "type": "win" + }, + "features": [ + "wrong_feature4" + ] + }, + { + "id": 4, + "description": "non-applicable feature: wrong device", + "vendor_id": "0x10de", + "device_id": [ "0x0DEA" ], + "os": { + "type": "win" + }, + "features": [ + "wrong_feature5" + ] + }, + { + "id": 5, + "description": "feature 1", + "vendor_id": "0x10de", + "device_id": [ "0x0DE9" ], + "os": { + "type": "win" + }, + "driver_version": { + "op": ">", + "value": "9.18.0.0" + }, + "features": [ + "feature1" + ] + }, + { + "id": 6, + "description": "non-applicable feature: too new driver", + "vendor_id": "0x10de", + "device_id": [ "0x0DE9" ], + "os": { + "type": "win" + }, + "driver_version": { + "op": "<=", + "value": "9.17.13.4344" + }, + "features": [ + "feature1" + ] + } + ] +} diff --git a/tests/auto/gui/qopenglconfig/qopenglconfig.pro b/tests/auto/gui/qopenglconfig/qopenglconfig.pro index ebeb509d0b..bcf7215eea 100644 --- a/tests/auto/gui/qopenglconfig/qopenglconfig.pro +++ b/tests/auto/gui/qopenglconfig/qopenglconfig.pro @@ -8,3 +8,4 @@ TARGET = tst_qopenglconfig QT += gui-private core-private testlib SOURCES += tst_qopenglconfig.cpp +OTHER_FILES = buglist.json diff --git a/tests/auto/gui/qopenglconfig/tst_qopenglconfig.cpp b/tests/auto/gui/qopenglconfig/tst_qopenglconfig.cpp index 49838cc812..bfb2623508 100644 --- a/tests/auto/gui/qopenglconfig/tst_qopenglconfig.cpp +++ b/tests/auto/gui/qopenglconfig/tst_qopenglconfig.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include @@ -105,6 +106,7 @@ class tst_QOpenGlConfig : public QObject private slots: void testConfiguration(); void testGlConfiguration(); + void testBugList(); }; static void dumpConfiguration(QTextStream &str) @@ -217,6 +219,41 @@ void tst_QOpenGlConfig::testGlConfiguration() qDebug().noquote() << '\n' << result; } +static inline QByteArray msgSetMismatch(const QSet &expected, + const QSet &actual) +{ + const QString result = QStringList(expected.toList()).join(QLatin1Char(',')) + + QLatin1String(" != ") + + QStringList(actual.toList()).join(QLatin1Char(',')); + return result.toLatin1(); +} + +void tst_QOpenGlConfig::testBugList() +{ + // Check bug list parsing for some arbitrary NVidia card + // faking Windows OS. + const QString fileName = QFINDTESTDATA("buglist.json"); + QVERIFY(!fileName.isEmpty()); + + QSet expectedFeatures; + expectedFeatures << "feature1"; + + QOpenGLConfig::Gpu gpu; + gpu.vendorId = 0x10DE; + gpu.deviceId = 0x0DE9; + +#ifdef Q_COMPILER_INITIALIZER_LISTS + gpu.driverVersion = QVersionNumber({9, 18, 13, 4460}); +#else + gpu.driverVersion = QVersionNumber(QVector() << 9 << 18 << 13 << 4460); +#endif + const QSet actualFeatures = + QOpenGLConfig::gpuFeatures(gpu, QStringLiteral("win"), + QVersionNumber(6, 3), fileName); + QVERIFY2(expectedFeatures == actualFeatures, + msgSetMismatch(expectedFeatures, actualFeatures)); +} + QTEST_MAIN(tst_QOpenGlConfig) #include "tst_qopenglconfig.moc"