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