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

source code of qtbase/src/gui/opengl/qopenglprogrambinarycache.cpp