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