1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtGui module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qopenglprogrambinarycache_p.h" |
41 | #include <QOpenGLContext> |
42 | #include <QOpenGLExtraFunctions> |
43 | #include <QSysInfo> |
44 | #include <QStandardPaths> |
45 | #include <QDir> |
46 | #include <QSaveFile> |
47 | #include <QCoreApplication> |
48 | #include <QCryptographicHash> |
49 | |
50 | #ifdef Q_OS_UNIX |
51 | #include <sys/mman.h> |
52 | #include <private/qcore_unix_p.h> |
53 | #endif |
54 | |
55 | QT_BEGIN_NAMESPACE |
56 | |
57 | Q_LOGGING_CATEGORY(lcOpenGLProgramDiskCache, "qt.opengl.diskcache" ) |
58 | |
59 | #ifndef GL_CONTEXT_LOST |
60 | #define GL_CONTEXT_LOST 0x0507 |
61 | #endif |
62 | |
63 | #ifndef GL_PROGRAM_BINARY_LENGTH |
64 | #define GL_PROGRAM_BINARY_LENGTH 0x8741 |
65 | #endif |
66 | |
67 | #ifndef GL_NUM_PROGRAM_BINARY_FORMATS |
68 | #define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE |
69 | #endif |
70 | |
71 | const quint32 BINSHADER_MAGIC = 0x5174; |
72 | const quint32 BINSHADER_VERSION = 0x3; |
73 | const quint32 BINSHADER_QTVERSION = QT_VERSION; |
74 | |
75 | namespace { |
76 | struct GLEnvInfo |
77 | { |
78 | GLEnvInfo(); |
79 | |
80 | QByteArray glvendor; |
81 | QByteArray glrenderer; |
82 | QByteArray glversion; |
83 | }; |
84 | } |
85 | |
86 | GLEnvInfo::GLEnvInfo() |
87 | { |
88 | QOpenGLContext *ctx = QOpenGLContext::currentContext(); |
89 | Q_ASSERT(ctx); |
90 | QOpenGLFunctions *f = ctx->functions(); |
91 | const char *vendor = reinterpret_cast<const char *>(f->glGetString(GL_VENDOR)); |
92 | const char *renderer = reinterpret_cast<const char *>(f->glGetString(GL_RENDERER)); |
93 | const char *version = reinterpret_cast<const char *>(f->glGetString(GL_VERSION)); |
94 | if (vendor) |
95 | glvendor = QByteArray(vendor); |
96 | if (renderer) |
97 | glrenderer = QByteArray(renderer); |
98 | if (version) |
99 | glversion = QByteArray(version); |
100 | } |
101 | |
102 | QByteArray QOpenGLProgramBinaryCache::ProgramDesc::cacheKey() const |
103 | { |
104 | QCryptographicHash keyBuilder(QCryptographicHash::Sha1); |
105 | for (const QOpenGLProgramBinaryCache::ShaderDesc &shader : shaders) |
106 | keyBuilder.addData(shader.source); |
107 | |
108 | return keyBuilder.result().toHex(); |
109 | } |
110 | |
111 | static inline bool qt_ensureWritableDir(const QString &name) |
112 | { |
113 | QDir::root().mkpath(name); |
114 | return QFileInfo(name).isWritable(); |
115 | } |
116 | |
117 | QOpenGLProgramBinaryCache::QOpenGLProgramBinaryCache() |
118 | : m_cacheWritable(false) |
119 | { |
120 | const QString subPath = QLatin1String("/qtshadercache-" ) + QSysInfo::buildAbi() + QLatin1Char('/'); |
121 | const QString sharedCachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); |
122 | if (!sharedCachePath.isEmpty()) { |
123 | m_cacheDir = sharedCachePath + subPath; |
124 | m_cacheWritable = qt_ensureWritableDir(m_cacheDir); |
125 | } |
126 | if (!m_cacheWritable) { |
127 | m_cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + subPath; |
128 | m_cacheWritable = qt_ensureWritableDir(m_cacheDir); |
129 | } |
130 | qCDebug(lcOpenGLProgramDiskCache, "Cache location '%s' writable = %d" , qPrintable(m_cacheDir), m_cacheWritable); |
131 | } |
132 | |
133 | QString QOpenGLProgramBinaryCache::cacheFileName(const QByteArray &cacheKey) const |
134 | { |
135 | return m_cacheDir + QString::fromUtf8(cacheKey); |
136 | } |
137 | |
138 | #define (int(4 * sizeof(quint32))) |
139 | #define (stringsSize) (BASE_HEADER_SIZE + 12 + stringsSize + 8) |
140 | #define PADDING_SIZE(fullHeaderSize) (((fullHeaderSize + 3) & ~3) - fullHeaderSize) |
141 | |
142 | static inline quint32 readUInt(const uchar **p) |
143 | { |
144 | quint32 v; |
145 | memcpy(&v, *p, sizeof(quint32)); |
146 | *p += sizeof(quint32); |
147 | return v; |
148 | } |
149 | |
150 | static inline QByteArray readStr(const uchar **p) |
151 | { |
152 | quint32 len = readUInt(p); |
153 | QByteArray ba = QByteArray::fromRawData(reinterpret_cast<const char *>(*p), len); |
154 | *p += len; |
155 | return ba; |
156 | } |
157 | |
158 | bool QOpenGLProgramBinaryCache::(const QByteArray &buf) const |
159 | { |
160 | if (buf.size() < BASE_HEADER_SIZE) { |
161 | qCDebug(lcOpenGLProgramDiskCache, "Cached size too small" ); |
162 | return false; |
163 | } |
164 | const uchar *p = reinterpret_cast<const uchar *>(buf.constData()); |
165 | if (readUInt(&p) != BINSHADER_MAGIC) { |
166 | qCDebug(lcOpenGLProgramDiskCache, "Magic does not match" ); |
167 | return false; |
168 | } |
169 | if (readUInt(&p) != BINSHADER_VERSION) { |
170 | qCDebug(lcOpenGLProgramDiskCache, "Version does not match" ); |
171 | return false; |
172 | } |
173 | if (readUInt(&p) != BINSHADER_QTVERSION) { |
174 | qCDebug(lcOpenGLProgramDiskCache, "Qt version does not match" ); |
175 | return false; |
176 | } |
177 | if (readUInt(&p) != sizeof(quintptr)) { |
178 | qCDebug(lcOpenGLProgramDiskCache, "Architecture does not match" ); |
179 | return false; |
180 | } |
181 | return true; |
182 | } |
183 | |
184 | bool QOpenGLProgramBinaryCache::setProgramBinary(uint programId, uint blobFormat, const void *p, uint blobSize) |
185 | { |
186 | QOpenGLContext *context = QOpenGLContext::currentContext(); |
187 | QOpenGLExtraFunctions *funcs = context->extraFunctions(); |
188 | while (true) { |
189 | GLenum error = funcs->glGetError(); |
190 | if (error == GL_NO_ERROR || error == GL_CONTEXT_LOST) |
191 | break; |
192 | } |
193 | #if QT_CONFIG(opengles2) |
194 | if (context->isOpenGLES() && context->format().majorVersion() < 3) { |
195 | initializeProgramBinaryOES(context); |
196 | programBinaryOES(programId, blobFormat, p, blobSize); |
197 | } else |
198 | #endif |
199 | funcs->glProgramBinary(programId, blobFormat, p, blobSize); |
200 | |
201 | GLenum err = funcs->glGetError(); |
202 | if (err != GL_NO_ERROR) { |
203 | qCDebug(lcOpenGLProgramDiskCache, "Program binary failed to load for program %u, size %d, " |
204 | "format 0x%x, err = 0x%x" , |
205 | programId, blobSize, blobFormat, err); |
206 | return false; |
207 | } |
208 | GLint linkStatus = 0; |
209 | funcs->glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus); |
210 | if (linkStatus != GL_TRUE) { |
211 | qCDebug(lcOpenGLProgramDiskCache, "Program binary failed to load for program %u, size %d, " |
212 | "format 0x%x, linkStatus = 0x%x, err = 0x%x" , |
213 | programId, blobSize, blobFormat, linkStatus, err); |
214 | return false; |
215 | } |
216 | |
217 | qCDebug(lcOpenGLProgramDiskCache, "Program binary set for program %u, size %d, format 0x%x, err = 0x%x" , |
218 | programId, blobSize, blobFormat, err); |
219 | return true; |
220 | } |
221 | |
222 | #ifdef Q_OS_UNIX |
223 | class FdWrapper |
224 | { |
225 | public: |
226 | FdWrapper(const QString &fn) |
227 | : ptr(MAP_FAILED) |
228 | { |
229 | fd = qt_safe_open(QFile::encodeName(fn).constData(), O_RDONLY); |
230 | } |
231 | ~FdWrapper() |
232 | { |
233 | if (ptr != MAP_FAILED) |
234 | munmap(ptr, mapSize); |
235 | if (fd != -1) |
236 | qt_safe_close(fd); |
237 | } |
238 | bool map() |
239 | { |
240 | off_t offs = lseek(fd, 0, SEEK_END); |
241 | if (offs == (off_t) -1) { |
242 | qErrnoWarning(errno, "lseek failed for program binary" ); |
243 | return false; |
244 | } |
245 | mapSize = static_cast<size_t>(offs); |
246 | ptr = mmap(nullptr, mapSize, PROT_READ, MAP_SHARED, fd, 0); |
247 | return ptr != MAP_FAILED; |
248 | } |
249 | |
250 | int fd; |
251 | void *ptr; |
252 | size_t mapSize; |
253 | }; |
254 | #endif |
255 | |
256 | class DeferredFileRemove |
257 | { |
258 | public: |
259 | DeferredFileRemove(const QString &fn) |
260 | : fn(fn), |
261 | active(false) |
262 | { |
263 | } |
264 | ~DeferredFileRemove() |
265 | { |
266 | if (active) |
267 | QFile(fn).remove(); |
268 | } |
269 | void setActive() |
270 | { |
271 | active = true; |
272 | } |
273 | |
274 | QString fn; |
275 | bool active; |
276 | }; |
277 | |
278 | bool QOpenGLProgramBinaryCache::load(const QByteArray &cacheKey, uint programId) |
279 | { |
280 | QMutexLocker lock(&m_mutex); |
281 | if (const MemCacheEntry *e = m_memCache.object(cacheKey)) |
282 | return setProgramBinary(programId, e->format, e->blob.constData(), e->blob.size()); |
283 | |
284 | QByteArray buf; |
285 | const QString fn = cacheFileName(cacheKey); |
286 | DeferredFileRemove undertaker(fn); |
287 | #ifdef Q_OS_UNIX |
288 | FdWrapper fdw(fn); |
289 | if (fdw.fd == -1) |
290 | return false; |
291 | char [BASE_HEADER_SIZE]; |
292 | qint64 bytesRead = qt_safe_read(fdw.fd, header, BASE_HEADER_SIZE); |
293 | if (bytesRead == BASE_HEADER_SIZE) |
294 | buf = QByteArray::fromRawData(header, BASE_HEADER_SIZE); |
295 | #else |
296 | QFile f(fn); |
297 | if (!f.open(QIODevice::ReadOnly)) |
298 | return false; |
299 | buf = f.read(BASE_HEADER_SIZE); |
300 | #endif |
301 | |
302 | if (!verifyHeader(buf)) { |
303 | undertaker.setActive(); |
304 | return false; |
305 | } |
306 | |
307 | const uchar *p; |
308 | #ifdef Q_OS_UNIX |
309 | if (!fdw.map()) { |
310 | undertaker.setActive(); |
311 | return false; |
312 | } |
313 | p = static_cast<const uchar *>(fdw.ptr) + BASE_HEADER_SIZE; |
314 | #else |
315 | buf = f.readAll(); |
316 | p = reinterpret_cast<const uchar *>(buf.constData()); |
317 | #endif |
318 | |
319 | GLEnvInfo info; |
320 | |
321 | QByteArray vendor = readStr(&p); |
322 | if (vendor != info.glvendor) { |
323 | // readStr returns non-null terminated strings just pointing to inside |
324 | // 'p' so must print these via the stream qCDebug and not constData(). |
325 | qCDebug(lcOpenGLProgramDiskCache) << "GL_VENDOR does not match" << vendor << info.glvendor; |
326 | undertaker.setActive(); |
327 | return false; |
328 | } |
329 | QByteArray renderer = readStr(&p); |
330 | if (renderer != info.glrenderer) { |
331 | qCDebug(lcOpenGLProgramDiskCache) << "GL_RENDERER does not match" << renderer << info.glrenderer; |
332 | undertaker.setActive(); |
333 | return false; |
334 | } |
335 | QByteArray version = readStr(&p); |
336 | if (version != info.glversion) { |
337 | qCDebug(lcOpenGLProgramDiskCache) << "GL_VERSION does not match" << version << info.glversion; |
338 | undertaker.setActive(); |
339 | return false; |
340 | } |
341 | |
342 | quint32 blobFormat = readUInt(&p); |
343 | quint32 blobSize = readUInt(&p); |
344 | |
345 | p += PADDING_SIZE(FULL_HEADER_SIZE(vendor.size() + renderer.size() + version.size())); |
346 | |
347 | return setProgramBinary(programId, blobFormat, p, blobSize) |
348 | && m_memCache.insert(cacheKey, new MemCacheEntry(p, blobSize, blobFormat)); |
349 | } |
350 | |
351 | static inline void writeUInt(uchar **p, quint32 value) |
352 | { |
353 | memcpy(*p, &value, sizeof(quint32)); |
354 | *p += sizeof(quint32); |
355 | } |
356 | |
357 | static inline void writeStr(uchar **p, const QByteArray &str) |
358 | { |
359 | writeUInt(p, str.size()); |
360 | memcpy(*p, str.constData(), str.size()); |
361 | *p += str.size(); |
362 | } |
363 | |
364 | void QOpenGLProgramBinaryCache::save(const QByteArray &cacheKey, uint programId) |
365 | { |
366 | if (!m_cacheWritable) |
367 | return; |
368 | |
369 | GLEnvInfo info; |
370 | |
371 | QOpenGLContext *context = QOpenGLContext::currentContext(); |
372 | QOpenGLExtraFunctions *funcs = context->extraFunctions(); |
373 | GLint blobSize = 0; |
374 | while (true) { |
375 | GLenum error = funcs->glGetError(); |
376 | if (error == GL_NO_ERROR || error == GL_CONTEXT_LOST) |
377 | break; |
378 | } |
379 | funcs->glGetProgramiv(programId, GL_PROGRAM_BINARY_LENGTH, &blobSize); |
380 | |
381 | const int = FULL_HEADER_SIZE(info.glvendor.size() + info.glrenderer.size() + info.glversion.size()); |
382 | |
383 | // Add padding to make the blob start 4-byte aligned in order to support |
384 | // OpenGL implementations on ARM that choke on non-aligned pointers passed |
385 | // to glProgramBinary. |
386 | const int paddingSize = PADDING_SIZE(headerSize); |
387 | |
388 | const int totalSize = headerSize + paddingSize + blobSize; |
389 | |
390 | qCDebug(lcOpenGLProgramDiskCache, "Program binary is %d bytes, err = 0x%x, total %d" , blobSize, funcs->glGetError(), totalSize); |
391 | if (!blobSize) |
392 | return; |
393 | |
394 | QByteArray blob(totalSize, Qt::Uninitialized); |
395 | uchar *p = reinterpret_cast<uchar *>(blob.data()); |
396 | |
397 | writeUInt(&p, BINSHADER_MAGIC); |
398 | writeUInt(&p, BINSHADER_VERSION); |
399 | writeUInt(&p, BINSHADER_QTVERSION); |
400 | writeUInt(&p, sizeof(quintptr)); |
401 | |
402 | writeStr(&p, info.glvendor); |
403 | writeStr(&p, info.glrenderer); |
404 | writeStr(&p, info.glversion); |
405 | |
406 | quint32 blobFormat = 0; |
407 | uchar *blobFormatPtr = p; |
408 | writeUInt(&p, blobFormat); |
409 | writeUInt(&p, blobSize); |
410 | |
411 | for (int i = 0; i < paddingSize; ++i) |
412 | *p++ = 0; |
413 | |
414 | GLint outSize = 0; |
415 | #if QT_CONFIG(opengles2) |
416 | if (context->isOpenGLES() && context->format().majorVersion() < 3) { |
417 | QMutexLocker lock(&m_mutex); |
418 | initializeProgramBinaryOES(context); |
419 | getProgramBinaryOES(programId, blobSize, &outSize, &blobFormat, p); |
420 | } else |
421 | #endif |
422 | funcs->glGetProgramBinary(programId, blobSize, &outSize, &blobFormat, p); |
423 | if (blobSize != outSize) { |
424 | qCDebug(lcOpenGLProgramDiskCache, "glGetProgramBinary returned size %d instead of %d" , outSize, blobSize); |
425 | return; |
426 | } |
427 | |
428 | writeUInt(&blobFormatPtr, blobFormat); |
429 | |
430 | #if QT_CONFIG(temporaryfile) |
431 | QSaveFile f(cacheFileName(cacheKey)); |
432 | if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { |
433 | f.write(blob); |
434 | if (!f.commit()) |
435 | #else |
436 | QFile f(cacheFileName(cacheKey)); |
437 | if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { |
438 | if (f.write(blob) < blob.length()) |
439 | #endif |
440 | qCDebug(lcOpenGLProgramDiskCache, "Failed to write %s to shader cache" , qPrintable(f.fileName())); |
441 | } else { |
442 | qCDebug(lcOpenGLProgramDiskCache, "Failed to create %s in shader cache" , qPrintable(f.fileName())); |
443 | } |
444 | } |
445 | |
446 | #if QT_CONFIG(opengles2) |
447 | void QOpenGLProgramBinaryCache::initializeProgramBinaryOES(QOpenGLContext *context) |
448 | { |
449 | if (m_programBinaryOESInitialized) |
450 | return; |
451 | m_programBinaryOESInitialized = true; |
452 | |
453 | Q_ASSERT(context); |
454 | getProgramBinaryOES = (void (QOPENGLF_APIENTRYP)(GLuint program, GLsizei bufSize, GLsizei *length, GLenum *binaryFormat, GLvoid *binary))context->getProcAddress("glGetProgramBinaryOES" ); |
455 | programBinaryOES = (void (QOPENGLF_APIENTRYP)(GLuint program, GLenum binaryFormat, const GLvoid *binary, GLint length))context->getProcAddress("glProgramBinaryOES" ); |
456 | } |
457 | #endif |
458 | |
459 | QOpenGLProgramBinarySupportCheck::QOpenGLProgramBinarySupportCheck(QOpenGLContext *context) |
460 | : QOpenGLSharedResource(context->shareGroup()), |
461 | m_supported(false) |
462 | { |
463 | if (QCoreApplication::testAttribute(Qt::AA_DisableShaderDiskCache)) { |
464 | qCDebug(lcOpenGLProgramDiskCache, "Shader cache disabled via app attribute" ); |
465 | return; |
466 | } |
467 | if (qEnvironmentVariableIntValue("QT_DISABLE_SHADER_DISK_CACHE" )) { |
468 | qCDebug(lcOpenGLProgramDiskCache, "Shader cache disabled via env var" ); |
469 | return; |
470 | } |
471 | |
472 | QOpenGLContext *ctx = QOpenGLContext::currentContext(); |
473 | if (ctx) { |
474 | if (ctx->isOpenGLES()) { |
475 | qCDebug(lcOpenGLProgramDiskCache, "OpenGL ES v%d context" , ctx->format().majorVersion()); |
476 | if (ctx->format().majorVersion() >= 3) { |
477 | m_supported = true; |
478 | } else { |
479 | const bool hasExt = ctx->hasExtension("GL_OES_get_program_binary" ); |
480 | qCDebug(lcOpenGLProgramDiskCache, "GL_OES_get_program_binary support = %d" , hasExt); |
481 | if (hasExt) |
482 | m_supported = true; |
483 | } |
484 | } else { |
485 | const bool hasExt = ctx->hasExtension("GL_ARB_get_program_binary" ); |
486 | qCDebug(lcOpenGLProgramDiskCache, "GL_ARB_get_program_binary support = %d" , hasExt); |
487 | if (hasExt) |
488 | m_supported = true; |
489 | } |
490 | if (m_supported) { |
491 | GLint fmtCount = 0; |
492 | ctx->functions()->glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &fmtCount); |
493 | qCDebug(lcOpenGLProgramDiskCache, "Supported binary format count = %d" , fmtCount); |
494 | m_supported = fmtCount > 0; |
495 | } |
496 | } |
497 | qCDebug(lcOpenGLProgramDiskCache, "Shader cache supported = %d" , m_supported); |
498 | } |
499 | |
500 | QT_END_NAMESPACE |
501 | |