1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qopengl.h" |
5 | #include "qopengl_p.h" |
6 | |
7 | #include "qopenglcontext.h" |
8 | #include "qopenglfunctions.h" |
9 | #include "qoperatingsystemversion.h" |
10 | #include "qoffscreensurface.h" |
11 | |
12 | #include <QtCore/QDebug> |
13 | #include <QtCore/QJsonDocument> |
14 | #include <QtCore/QJsonValue> |
15 | #include <QtCore/QJsonObject> |
16 | #include <QtCore/QJsonArray> |
17 | #include <QtCore/QTextStream> |
18 | #include <QtCore/QFile> |
19 | #include <QtCore/QDir> |
20 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | using namespace Qt::StringLiterals; |
24 | |
25 | #if defined(QT_OPENGL_3) |
26 | typedef const GLubyte * (QOPENGLF_APIENTRYP qt_glGetStringi)(GLenum, GLuint); |
27 | #endif |
28 | |
29 | #ifndef GL_CONTEXT_LOST |
30 | #define GL_CONTEXT_LOST 0x0507 |
31 | #endif |
32 | |
33 | QOpenGLExtensionMatcher::QOpenGLExtensionMatcher() |
34 | { |
35 | QOpenGLContext *ctx = QOpenGLContext::currentContext(); |
36 | if (!ctx) { |
37 | qWarning(msg: "QOpenGLExtensionMatcher::QOpenGLExtensionMatcher: No context" ); |
38 | return; |
39 | } |
40 | QOpenGLFunctions *funcs = ctx->functions(); |
41 | const char *extensionStr = nullptr; |
42 | |
43 | if (ctx->isOpenGLES() || ctx->format().majorVersion() < 3) |
44 | extensionStr = reinterpret_cast<const char *>(funcs->glGetString(GL_EXTENSIONS)); |
45 | |
46 | if (extensionStr) { |
47 | QByteArray ba(extensionStr); |
48 | QList<QByteArray> extensions = ba.split(sep: ' '); |
49 | m_extensions = QSet<QByteArray>(extensions.constBegin(), extensions.constEnd()); |
50 | } else { |
51 | #ifdef QT_OPENGL_3 |
52 | // clear error state |
53 | while (true) { // Clear error state. |
54 | GLenum error = funcs->glGetError(); |
55 | if (error == GL_NO_ERROR) |
56 | break; |
57 | if (error == GL_CONTEXT_LOST) |
58 | return; |
59 | }; |
60 | qt_glGetStringi glGetStringi = (qt_glGetStringi)ctx->getProcAddress(procName: "glGetStringi" ); |
61 | |
62 | if (!glGetStringi) |
63 | return; |
64 | |
65 | GLint numExtensions = 0; |
66 | funcs->glGetIntegerv(GL_NUM_EXTENSIONS, params: &numExtensions); |
67 | |
68 | for (int i = 0; i < numExtensions; ++i) { |
69 | const char *str = reinterpret_cast<const char *>(glGetStringi(GL_EXTENSIONS, i)); |
70 | m_extensions.insert(value: str); |
71 | } |
72 | #endif // QT_OPENGL_3 |
73 | } |
74 | } |
75 | |
76 | /* Helpers to read out the list of features matching a device from |
77 | * a Chromium driver bug list. Note that not all keys are supported and |
78 | * some may behave differently: gl_vendor is a substring match instead of regex. |
79 | { |
80 | "entries": [ |
81 | { |
82 | "id": 20, |
83 | "description": "Disable EXT_draw_buffers on GeForce GT 650M on Linux due to driver bugs", |
84 | "os": { |
85 | "type": "linux" |
86 | }, |
87 | // Optional: "exceptions" list |
88 | "vendor_id": "0x10de", |
89 | "device_id": ["0x0fd5"], |
90 | "multi_gpu_category": "any", |
91 | "features": [ |
92 | "disable_ext_draw_buffers" |
93 | ] |
94 | }, |
95 | .... |
96 | } |
97 | */ |
98 | |
99 | QDebug operator<<(QDebug d, const QOpenGLConfig::Gpu &g) |
100 | { |
101 | QDebugStateSaver s(d); |
102 | d.nospace(); |
103 | d << "Gpu(" ; |
104 | if (g.isValid()) { |
105 | d << "vendor=" << Qt::hex << Qt::showbase <<g.vendorId << ", device=" << g.deviceId |
106 | << "version=" << g.driverVersion; |
107 | } else { |
108 | d << 0; |
109 | } |
110 | d << ')'; |
111 | return d; |
112 | } |
113 | |
114 | typedef QJsonArray::ConstIterator JsonArrayConstIt; |
115 | |
116 | static inline bool contains(const QJsonArray &haystack, unsigned needle) |
117 | { |
118 | for (JsonArrayConstIt it = haystack.constBegin(), cend = haystack.constEnd(); it != cend; ++it) { |
119 | if (needle == it->toString().toUInt(ok: nullptr, /* base */ 0)) |
120 | return true; |
121 | } |
122 | return false; |
123 | } |
124 | |
125 | static inline bool contains(const QJsonArray &haystack, const QString &needle) |
126 | { |
127 | for (JsonArrayConstIt it = haystack.constBegin(), cend = haystack.constEnd(); it != cend; ++it) { |
128 | if (needle == it->toString()) |
129 | return true; |
130 | } |
131 | return false; |
132 | } |
133 | |
134 | namespace { |
135 | enum Operator { NotEqual, LessThan, LessEqualThan, Equals, GreaterThan, GreaterEqualThan }; |
136 | static const char operators[][3] = {"!=" , "<" , "<=" , "=" , ">" , ">=" }; |
137 | |
138 | // VersionTerm describing a version term consisting of number and operator |
139 | // found in os.version and driver_version. |
140 | struct VersionTerm { |
141 | VersionTerm() : op(NotEqual) {} |
142 | static VersionTerm fromJson(const QJsonValue &v); |
143 | bool isNull() const { return number.isNull(); } |
144 | bool matches(const QVersionNumber &other) const; |
145 | |
146 | QVersionNumber number; |
147 | Operator op; |
148 | }; |
149 | |
150 | bool VersionTerm::matches(const QVersionNumber &other) const |
151 | { |
152 | if (isNull() || other.isNull()) { |
153 | qWarning(msg: "called with invalid parameters" ); |
154 | return false; |
155 | } |
156 | switch (op) { |
157 | case NotEqual: |
158 | return other != number; |
159 | case LessThan: |
160 | return other < number; |
161 | case LessEqualThan: |
162 | return other <= number; |
163 | case Equals: |
164 | return other == number; |
165 | case GreaterThan: |
166 | return other > number; |
167 | case GreaterEqualThan: |
168 | return other >= number; |
169 | } |
170 | return false; |
171 | } |
172 | |
173 | VersionTerm VersionTerm::fromJson(const QJsonValue &v) |
174 | { |
175 | VersionTerm result; |
176 | if (!v.isObject()) |
177 | return result; |
178 | const QJsonObject o = v.toObject(); |
179 | result.number = QVersionNumber::fromString(string: o.value(key: "value"_L1 ).toString()); |
180 | const QString opS = o.value(key: "op"_L1 ).toString(); |
181 | for (size_t i = 0; i < sizeof(operators) / sizeof(operators[0]); ++i) { |
182 | if (opS == QLatin1StringView(operators[i])) { |
183 | result.op = static_cast<Operator>(i); |
184 | break; |
185 | } |
186 | } |
187 | return result; |
188 | } |
189 | |
190 | // OS term consisting of name and optional version found in |
191 | // under "os" in main array and in "exceptions" lists. |
192 | struct OsTypeTerm |
193 | { |
194 | static OsTypeTerm fromJson(const QJsonValue &v); |
195 | static QString hostOs(); |
196 | static QVersionNumber hostKernelVersion() { return QVersionNumber::fromString(string: QSysInfo::kernelVersion()); } |
197 | static QString hostOsRelease() { |
198 | #ifdef Q_OS_WIN |
199 | if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11) |
200 | return u"11"_s ; |
201 | return u"10"_s ; |
202 | #else |
203 | return {}; |
204 | #endif |
205 | } |
206 | |
207 | bool isNull() const { return type.isEmpty(); } |
208 | bool matches(const QString &osName, const QVersionNumber &kernelVersion, const QString &osRelease) const |
209 | { |
210 | if (isNull() || osName.isEmpty() || kernelVersion.isNull()) { |
211 | qWarning(msg: "called with invalid parameters" ); |
212 | return false; |
213 | } |
214 | if (type != osName) |
215 | return false; |
216 | if (!versionTerm.isNull() && !versionTerm.matches(other: kernelVersion)) |
217 | return false; |
218 | // release is a list of Windows versions where the rule should match |
219 | if (!release.isEmpty() && !contains(haystack: release, needle: osRelease)) |
220 | return false; |
221 | return true; |
222 | } |
223 | |
224 | QString type; |
225 | VersionTerm versionTerm; |
226 | QJsonArray release; |
227 | }; |
228 | |
229 | OsTypeTerm OsTypeTerm::fromJson(const QJsonValue &v) |
230 | { |
231 | OsTypeTerm result; |
232 | if (!v.isObject()) |
233 | return result; |
234 | const QJsonObject o = v.toObject(); |
235 | result.type = o.value(key: "type"_L1 ).toString(); |
236 | result.versionTerm = VersionTerm::fromJson(v: o.value(key: "version"_L1 )); |
237 | result.release = o.value(key: "release"_L1 ).toArray(); |
238 | return result; |
239 | } |
240 | |
241 | QString OsTypeTerm::hostOs() |
242 | { |
243 | // Determine Host OS. |
244 | #if defined(Q_OS_WIN) |
245 | return QStringLiteral("win" ); |
246 | #elif defined(Q_OS_LINUX) |
247 | return QStringLiteral("linux" ); |
248 | #elif defined(Q_OS_MACOS) |
249 | return QStringLiteral("macosx" ); |
250 | #elif defined(Q_OS_ANDROID) |
251 | return QStringLiteral("android" ); |
252 | #else |
253 | return QString(); |
254 | #endif |
255 | } |
256 | } // anonymous namespace |
257 | |
258 | static QString msgSyntaxWarning(const QJsonObject &object, const QString &what) |
259 | { |
260 | QString result; |
261 | QTextStream(&result) << "Id " << object.value(key: "id"_L1 ).toInt() |
262 | << " (\"" << object.value(key: "description"_L1 ).toString() |
263 | << "\"): " << what; |
264 | return result; |
265 | } |
266 | |
267 | // Check whether an entry matches. Called recursively for |
268 | // "exceptions" list. |
269 | |
270 | static bool matches(const QJsonObject &object, |
271 | const QString &osName, |
272 | const QVersionNumber &kernelVersion, |
273 | const QString &osRelease, |
274 | const QOpenGLConfig::Gpu &gpu) |
275 | { |
276 | const OsTypeTerm os = OsTypeTerm::fromJson(v: object.value(key: "os"_L1 )); |
277 | if (!os.isNull() && !os.matches(osName, kernelVersion, osRelease)) |
278 | return false; |
279 | |
280 | const QJsonValue exceptionsV = object.value(key: "exceptions"_L1 ); |
281 | if (exceptionsV.isArray()) { |
282 | const QJsonArray exceptionsA = exceptionsV.toArray(); |
283 | for (JsonArrayConstIt it = exceptionsA.constBegin(), cend = exceptionsA.constEnd(); it != cend; ++it) { |
284 | if (matches(object: it->toObject(), osName, kernelVersion, osRelease, gpu)) |
285 | return false; |
286 | } |
287 | } |
288 | |
289 | const QJsonValue vendorV = object.value(key: "vendor_id"_L1 ); |
290 | if (vendorV.isString()) { |
291 | if (gpu.vendorId != vendorV.toString().toUInt(ok: nullptr, /* base */ 0)) |
292 | return false; |
293 | } else { |
294 | if (object.contains(key: "gl_vendor"_L1 )) { |
295 | const QByteArray glVendorV = object.value(key: "gl_vendor"_L1 ).toString().toUtf8(); |
296 | if (!gpu.glVendor.contains(bv: glVendorV)) |
297 | return false; |
298 | } |
299 | } |
300 | |
301 | if (gpu.deviceId) { |
302 | const QJsonValue deviceIdV = object.value(key: "device_id"_L1 ); |
303 | switch (deviceIdV.type()) { |
304 | case QJsonValue::Array: |
305 | if (!contains(haystack: deviceIdV.toArray(), needle: gpu.deviceId)) |
306 | return false; |
307 | break; |
308 | case QJsonValue::Undefined: |
309 | case QJsonValue::Null: |
310 | break; |
311 | default: |
312 | qWarning().noquote() |
313 | << msgSyntaxWarning(object, what: "Device ID must be of type array."_L1 ); |
314 | } |
315 | } |
316 | if (!gpu.driverVersion.isNull()) { |
317 | const QJsonValue driverVersionV = object.value(key: "driver_version"_L1 ); |
318 | switch (driverVersionV.type()) { |
319 | case QJsonValue::Object: |
320 | if (!VersionTerm::fromJson(v: driverVersionV).matches(other: gpu.driverVersion)) |
321 | return false; |
322 | break; |
323 | case QJsonValue::Undefined: |
324 | case QJsonValue::Null: |
325 | break; |
326 | default: |
327 | qWarning().noquote() |
328 | << msgSyntaxWarning(object, what: "Driver version must be of type object."_L1 ); |
329 | } |
330 | } |
331 | |
332 | if (!gpu.driverDescription.isEmpty()) { |
333 | const QJsonValue driverDescriptionV = object.value(key: "driver_description"_L1 ); |
334 | if (driverDescriptionV.isString()) { |
335 | if (!gpu.driverDescription.contains(bv: driverDescriptionV.toString().toUtf8())) |
336 | return false; |
337 | } |
338 | } |
339 | |
340 | return true; |
341 | } |
342 | |
343 | static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, |
344 | const QString &osName, |
345 | const QVersionNumber &kernelVersion, |
346 | const QString &osRelease, |
347 | const QJsonDocument &doc, |
348 | QSet<QString> *result, |
349 | QString *errorMessage) |
350 | { |
351 | result->clear(); |
352 | errorMessage->clear(); |
353 | const QJsonValue entriesV = doc.object().value(key: "entries"_L1 ); |
354 | if (!entriesV.isArray()) { |
355 | *errorMessage = "No entries read."_L1 ; |
356 | return false; |
357 | } |
358 | |
359 | const QJsonArray entriesA = entriesV.toArray(); |
360 | for (JsonArrayConstIt eit = entriesA.constBegin(), ecend = entriesA.constEnd(); eit != ecend; ++eit) { |
361 | if (eit->isObject()) { |
362 | const QJsonObject object = eit->toObject(); |
363 | if (matches(object, osName, kernelVersion, osRelease, gpu)) { |
364 | const QJsonValue featuresListV = object.value(key: "features"_L1 ); |
365 | if (featuresListV.isArray()) { |
366 | const QJsonArray featuresListA = featuresListV.toArray(); |
367 | for (JsonArrayConstIt fit = featuresListA.constBegin(), fcend = featuresListA.constEnd(); fit != fcend; ++fit) |
368 | result->insert(value: fit->toString()); |
369 | } |
370 | } |
371 | } |
372 | } |
373 | return true; |
374 | } |
375 | |
376 | static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, |
377 | const QString &osName, |
378 | const QVersionNumber &kernelVersion, |
379 | const QString &osRelease, |
380 | const QByteArray &jsonAsciiData, |
381 | QSet<QString> *result, QString *errorMessage) |
382 | { |
383 | result->clear(); |
384 | errorMessage->clear(); |
385 | QJsonParseError error; |
386 | const QJsonDocument document = QJsonDocument::fromJson(json: jsonAsciiData, error: &error); |
387 | if (document.isNull()) { |
388 | const qsizetype lineNumber = |
389 | QByteArrayView(jsonAsciiData).left(n: error.offset).count(ch: '\n') + 1; |
390 | QTextStream str(errorMessage); |
391 | str << "Failed to parse data: \"" << error.errorString() |
392 | << "\" at line " << lineNumber << " (offset: " |
393 | << error.offset << ")." ; |
394 | return false; |
395 | } |
396 | return readGpuFeatures(gpu, osName, kernelVersion, osRelease, doc: document, result, errorMessage); |
397 | } |
398 | |
399 | static bool readGpuFeatures(const QOpenGLConfig::Gpu &gpu, |
400 | const QString &osName, |
401 | const QVersionNumber &kernelVersion, |
402 | const QString &osRelease, |
403 | const QString &fileName, |
404 | QSet<QString> *result, QString *errorMessage) |
405 | { |
406 | result->clear(); |
407 | errorMessage->clear(); |
408 | QFile file(fileName); |
409 | if (!file.open(flags: QIODevice::ReadOnly)) { |
410 | QTextStream str(errorMessage); |
411 | str << "Cannot open \"" << QDir::toNativeSeparators(pathName: fileName) << "\": " |
412 | << file.errorString(); |
413 | return false; |
414 | } |
415 | const bool success = readGpuFeatures(gpu, osName, kernelVersion, osRelease, jsonAsciiData: file.readAll(), result, errorMessage); |
416 | if (!success) { |
417 | errorMessage->prepend(s: "Error reading \""_L1 |
418 | + QDir::toNativeSeparators(pathName: fileName) |
419 | + "\": "_L1 ); |
420 | } |
421 | return success; |
422 | } |
423 | |
424 | QSet<QString> QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu, |
425 | const QString &osName, |
426 | const QVersionNumber &kernelVersion, |
427 | const QString &osRelease, |
428 | const QJsonDocument &doc) |
429 | { |
430 | QSet<QString> result; |
431 | QString errorMessage; |
432 | if (!readGpuFeatures(gpu, osName, kernelVersion, osRelease, doc, result: &result, errorMessage: &errorMessage)) |
433 | qWarning().noquote() << errorMessage; |
434 | return result; |
435 | } |
436 | |
437 | QSet<QString> QOpenGLConfig::gpuFeatures(const QOpenGLConfig::Gpu &gpu, |
438 | const QString &osName, |
439 | const QVersionNumber &kernelVersion, |
440 | const QString &osRelease, |
441 | const QString &fileName) |
442 | { |
443 | QSet<QString> result; |
444 | QString errorMessage; |
445 | if (!readGpuFeatures(gpu, osName, kernelVersion, osRelease, fileName, result: &result, errorMessage: &errorMessage)) |
446 | qWarning().noquote() << errorMessage; |
447 | return result; |
448 | } |
449 | |
450 | QSet<QString> QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QJsonDocument &doc) |
451 | { |
452 | return gpuFeatures(gpu, osName: OsTypeTerm::hostOs(), kernelVersion: OsTypeTerm::hostKernelVersion(), osRelease: OsTypeTerm::hostOsRelease(), doc); |
453 | } |
454 | |
455 | QSet<QString> QOpenGLConfig::gpuFeatures(const Gpu &gpu, const QString &fileName) |
456 | { |
457 | return gpuFeatures(gpu, osName: OsTypeTerm::hostOs(), kernelVersion: OsTypeTerm::hostKernelVersion(), osRelease: OsTypeTerm::hostOsRelease(), fileName); |
458 | } |
459 | |
460 | QOpenGLConfig::Gpu QOpenGLConfig::Gpu::fromContext() |
461 | { |
462 | QOpenGLContext *ctx = QOpenGLContext::currentContext(); |
463 | QScopedPointer<QOpenGLContext> tmpContext; |
464 | QScopedPointer<QOffscreenSurface> tmpSurface; |
465 | if (!ctx) { |
466 | tmpContext.reset(other: new QOpenGLContext); |
467 | if (!tmpContext->create()) { |
468 | qWarning(msg: "QOpenGLConfig::Gpu::fromContext: Failed to create temporary context" ); |
469 | return QOpenGLConfig::Gpu(); |
470 | } |
471 | tmpSurface.reset(other: new QOffscreenSurface); |
472 | tmpSurface->setFormat(tmpContext->format()); |
473 | tmpSurface->create(); |
474 | tmpContext->makeCurrent(surface: tmpSurface.data()); |
475 | } |
476 | |
477 | QOpenGLConfig::Gpu gpu; |
478 | ctx = QOpenGLContext::currentContext(); |
479 | const GLubyte *p = ctx->functions()->glGetString(GL_VENDOR); |
480 | if (p) |
481 | gpu.glVendor = QByteArray(reinterpret_cast<const char *>(p)); |
482 | |
483 | return gpu; |
484 | } |
485 | |
486 | QT_END_NAMESPACE |
487 | |