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