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
55QT_BEGIN_NAMESPACE
56
57Q_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
71const quint32 BINSHADER_MAGIC = 0x5174;
72const quint32 BINSHADER_VERSION = 0x3;
73const quint32 BINSHADER_QTVERSION = QT_VERSION;
74
75namespace {
76struct GLEnvInfo
77{
78 GLEnvInfo();
79
80 QByteArray glvendor;
81 QByteArray glrenderer;
82 QByteArray glversion;
83};
84}
85
86GLEnvInfo::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
102QByteArray 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
111static inline bool qt_ensureWritableDir(const QString &name)
112{
113 QDir::root().mkpath(name);
114 return QFileInfo(name).isWritable();
115}
116
117QOpenGLProgramBinaryCache::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
133QString QOpenGLProgramBinaryCache::cacheFileName(const QByteArray &cacheKey) const
134{
135 return m_cacheDir + QString::fromUtf8(cacheKey);
136}
137
138#define BASE_HEADER_SIZE (int(4 * sizeof(quint32)))
139#define FULL_HEADER_SIZE(stringsSize) (BASE_HEADER_SIZE + 12 + stringsSize + 8)
140#define PADDING_SIZE(fullHeaderSize) (((fullHeaderSize + 3) & ~3) - fullHeaderSize)
141
142static 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
150static 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
158bool QOpenGLProgramBinaryCache::verifyHeader(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
184bool 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
223class FdWrapper
224{
225public:
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
256class DeferredFileRemove
257{
258public:
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
278bool 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 header[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
351static inline void writeUInt(uchar **p, quint32 value)
352{
353 memcpy(*p, &value, sizeof(quint32));
354 *p += sizeof(quint32);
355}
356
357static 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
364void 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 headerSize = 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)
447void 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
459QOpenGLProgramBinarySupportCheck::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
500QT_END_NAMESPACE
501