1// Copyright (C) 2016 Jolla Ltd, author: <gunnar.sletta@jollamobile.com>
2// Copyright (C) 2022 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qgfxshaderbuilder_p.h"
6
7#include <QtCore/QDebug>
8#include <QtCore/QUrl>
9#include <QtCore/QVarLengthArray>
10#include <QtCore/QStandardPaths>
11#include <QtCore/QCryptographicHash>
12#include <QtCore/QDir>
13#include <QtGui/QOffscreenSurface>
14#include <QtGui/QOpenGLContext>
15#include <QtGui/QOpenGLFunctions>
16
17#include <QtQuick/qquickwindow.h>
18
19#include <qmath.h>
20#include <qnumeric.h>
21
22QT_BEGIN_NAMESPACE
23
24#ifndef GL_MAX_VARYING_COMPONENTS
25#define GL_MAX_VARYING_COMPONENTS 0x8B4B
26#endif
27
28#ifndef GL_MAX_VARYING_FLOATS
29#define GL_MAX_VARYING_FLOATS 0x8B4B
30#endif
31
32#ifndef GL_MAX_VARYING_VECTORS
33#define GL_MAX_VARYING_VECTORS 0x8DFC
34#endif
35
36#ifndef GL_MAX_VERTEX_OUTPUT_COMPONENTS
37#define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122
38#endif
39
40#if !defined(QT5COMPAT_MAX_BLUR_SAMPLES)
41#define QT5COMPAT_MAX_BLUR_SAMPLES 15 // Conservative estimate for maximum varying vectors in
42 // shaders (maximum 60 components on some Metal
43 // implementations, hence 15 vectors of 4 components each)
44#elif !defined(QT5COMPAT_MAX_BLUR_SAMPLES_GL)
45#define QT5COMPAT_MAX_BLUR_SAMPLES_GL QT5COMPAT_MAX_BLUR_SAMPLES
46#endif
47
48#if !defined(QT5COMPAT_MAX_BLUR_SAMPLES_GL)
49#define QT5COMPAT_MAX_BLUR_SAMPLES_GL 8 // minimum number of varyings in the ES 2.0 spec.
50#endif
51
52QGfxShaderBuilder::QGfxShaderBuilder()
53{
54 QList<QShaderBaker::GeneratedShader> targets =
55 {
56 { QShader::HlslShader, QShaderVersion(50) },
57 { QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) },
58 { QShader::GlslShader, QShaderVersion(120) },
59 { QShader::GlslShader, QShaderVersion(150) },
60 { QShader::MslShader, QShaderVersion(12) },
61 { QShader::SpirvShader, QShaderVersion(100) }
62 };
63
64 m_shaderBaker.setGeneratedShaders(targets);
65 m_shaderBaker.setGeneratedShaderVariants({ QShader::StandardShader,
66 QShader::BatchableVertexShader });
67
68#ifndef QT_NO_OPENGL
69 QSGRendererInterface::GraphicsApi graphicsApi = QQuickWindow::graphicsApi();
70 if (graphicsApi == QSGRendererInterface::OpenGL) {
71 // The following code makes the assumption that an OpenGL context the GUI
72 // thread will get the same capabilities as the render thread's OpenGL
73 // context. Not 100% accurate, but it works...
74 QOpenGLContext context;
75 if (!context.create()) {
76 qDebug() << "failed to acquire GL context to resolve capabilities, using defaults..";
77 m_maxBlurSamples = QT5COMPAT_MAX_BLUR_SAMPLES_GL;
78 return;
79 }
80
81 QOffscreenSurface surface;
82 // In very odd cases, we can get incompatible configs here unless we pass the
83 // GL context's format on to the offscreen format.
84 surface.setFormat(context.format());
85 surface.create();
86
87 QOpenGLContext *oldContext = QOpenGLContext::currentContext();
88 QSurface *oldSurface = oldContext ? oldContext->surface() : 0;
89 if (context.makeCurrent(surface: &surface)) {
90 QOpenGLFunctions *gl = context.functions();
91 const bool coreProfile = context.format().profile() == QSurfaceFormat::CoreProfile;
92 if (context.isOpenGLES()) {
93 gl->glGetIntegerv(GL_MAX_VARYING_VECTORS, params: &m_maxBlurSamples);
94 } else if (context.format().majorVersion() >= 3) {
95 int components;
96 gl->glGetIntegerv(pname: coreProfile ? GL_MAX_VERTEX_OUTPUT_COMPONENTS : GL_MAX_VARYING_COMPONENTS, params: &components);
97 m_maxBlurSamples = components / 2.0;
98 } else {
99 int floats;
100 gl->glGetIntegerv(GL_MAX_VARYING_FLOATS, params: &floats);
101 m_maxBlurSamples = floats / 2.0;
102 }
103 if (oldContext && oldSurface)
104 oldContext->makeCurrent(surface: oldSurface);
105 else
106 context.doneCurrent();
107 } else {
108 qDebug() << "QGfxShaderBuilder: Failed to acquire GL context to resolve capabilities, using defaults.";
109 m_maxBlurSamples = QT5COMPAT_MAX_BLUR_SAMPLES_GL;
110 }
111 } else
112#endif
113 m_maxBlurSamples = QT5COMPAT_MAX_BLUR_SAMPLES;
114}
115
116QGfxShaderBuilder::~QGfxShaderBuilder()
117 = default;
118
119/*
120
121 The algorithm works like this..
122
123 For every two pixels we want to sample we take one sample between those
124 two pixels and rely on linear interpoliation to get both values at the
125 cost of one texture sample. The sample point is calculated based on the
126 gaussian weights at the two texels.
127
128 I've included the table here for future reference:
129
130 Requested Effective Actual Actual
131 Samples Radius/Kernel Samples Radius(*)
132 -------------------------------------------------
133 0 0 / 1x1 1 0
134 1 0 / 1x1 1 0
135 2 1 / 3x3 2 0
136 3 1 / 3x3 2 0
137 4 2 / 5x5 3 1
138 5 2 / 5x5 3 1
139 6 3 / 7x7 4 1
140 7 3 / 7x7 4 1
141 8 4 / 9x9 5 2
142 9 4 / 9x9 5 2
143 10 5 / 11x11 6 2
144 11 5 / 11x11 6 2
145 12 6 / 13x13 7 3
146 13 6 / 13x13 7 3
147 ... ... ... ...
148
149 When ActualSamples is an 'odd' nunber, sample center pixel separately:
150 EffectiveRadius: 4
151 EffectiveKernel: 9x9
152 ActualSamples: 5
153 -4 -3 -2 -1 0 +1 +2 +3 +4
154 | | | | | | | | | |
155 \ / \ / | \ / \ /
156 tL2 tL1 tC tR1 tR2
157
158 When ActualSamples is an 'even' number, sample 3 center pixels with two
159 samples:
160 EffectiveRadius: 3
161 EffectiveKernel: 7x7
162 ActualSamples: 4
163 -3 -2 -1 0 +1 +2 +3
164 | | | | | | | |
165 \ / \ / | \ /
166 tL1 tL0 tR0 tR2
167
168 From this table we have the following formulas:
169 EffectiveRadius = RequestedSamples / 2;
170 EffectiveKernel = EffectiveRadius * 2 + 1
171 ActualSamples = 1 + RequstedSamples / 2;
172 ActualRadius = RequestedSamples / 4;
173
174 (*) ActualRadius excludes the pixel pair sampled in the center
175 for even 'actual sample' counts
176*/
177
178static qreal qgfx_gaussian(qreal x, qreal d)
179{
180 return qExp(v: - x * x / (2 * d * d));
181}
182
183struct QGfxGaussSample
184{
185 QByteArray name;
186 qreal pos;
187 qreal weight;
188 inline void set(const QByteArray &n, qreal p, qreal w) {
189 name = n;
190 pos = p;
191 weight = w;
192 }
193};
194
195static void qgfx_declareBlur(QByteArray &shader, const QByteArray& direction, QGfxGaussSample *s, int samples)
196{
197 for (int i=0; i<samples; ++i) {
198 shader += "layout(location = " + QByteArray::number(i) + ") " + direction + " vec2 ";
199 shader += s[i].name;
200 shader += ";\n";
201 }
202}
203
204static void qgfx_buildGaussSamplePoints(QGfxGaussSample *p, int samples, int radius, qreal deviation)
205{
206
207 if ((samples % 2) == 1) {
208 p[radius].set(n: "tC", p: 0, w: 1);
209 for (int i=0; i<radius; ++i) {
210 qreal p0 = (i + 1) * 2 - 1;
211 qreal p1 = (i + 1) * 2;
212 qreal w0 = qgfx_gaussian(x: p0, d: deviation);
213 qreal w1 = qgfx_gaussian(x: p1, d: deviation);
214 qreal w = w0 + w1;
215 qreal samplePos = (p0 * w0 + p1 * w1) / w;
216 if (qIsNaN(d: samplePos)) {
217 samplePos = 0;
218 w = 0;
219 }
220 p[radius - i - 1].set(n: "tL" + QByteArray::number(i), p: samplePos, w);
221 p[radius + i + 1].set(n: "tR" + QByteArray::number(i), p: -samplePos, w);
222 }
223 } else {
224 { // tL0
225 qreal wl = qgfx_gaussian(x: -1.0, d: deviation);
226 qreal wc = qgfx_gaussian(x: 0.0, d: deviation);
227 qreal w = wl + wc;
228 p[radius].set(n: "tL0", p: -1.0 * wl / w, w);
229 p[radius+1].set(n: "tR0", p: 1.0, w: wl); // reuse wl as gauss(-1)==gauss(1);
230 }
231 for (int i=0; i<radius; ++i) {
232 qreal p0 = (i + 1) * 2;
233 qreal p1 = (i + 1) * 2 + 1;
234 qreal w0 = qgfx_gaussian(x: p0, d: deviation);
235 qreal w1 = qgfx_gaussian(x: p1, d: deviation);
236 qreal w = w0 + w1;
237 qreal samplePos = (p0 * w0 + p1 * w1) / w;
238 if (qIsNaN(d: samplePos)) {
239 samplePos = 0;
240 w = 0;
241 }
242 p[radius - i - 1].set(n: "tL" + QByteArray::number(i+1), p: samplePos, w);
243 p[radius + i + 2].set(n: "tR" + QByteArray::number(i+1), p: -samplePos, w);
244
245 }
246 }
247}
248
249void qgfx_declareUniforms(QByteArray &shader, bool alphaOnly)
250{
251 shader += "layout(std140, binding = 0) uniform buf {\n"
252 " mat4 qt_Matrix;\n"
253 " float qt_Opacity;\n"
254 " float spread;\n"
255 " vec2 dirstep;\n";
256
257 if (alphaOnly) {
258 shader += " vec4 color;\n"
259 " float thickness;\n";
260 }
261 shader += "};\n\n";
262}
263
264QByteArray qgfx_gaussianVertexShader(QGfxGaussSample *p, int samples, bool alphaOnly)
265{
266 QByteArray shader;
267 shader.reserve(asize: 1024);
268 shader += "#version 440\n\n"
269 "layout(location = 0) in vec4 qt_Vertex;\n"
270 "layout(location = 1) in vec2 qt_MultiTexCoord0;\n\n";
271
272 qgfx_declareUniforms(shader, alphaOnly);
273
274 shader += "out gl_PerVertex { vec4 gl_Position; };\n\n";
275
276 qgfx_declareBlur(shader, direction: "out", s: p, samples);
277
278 shader += "\nvoid main() {\n"
279 " gl_Position = qt_Matrix * qt_Vertex;\n\n";
280
281 for (int i=0; i<samples; ++i) {
282 shader += " ";
283 shader += p[i].name;
284 shader += " = qt_MultiTexCoord0";
285 if (p[i].pos != 0.0) {
286 shader += " + spread * dirstep * float(";
287 shader += QByteArray::number(p[i].pos);
288 shader += ')';
289 }
290 shader += ";\n";
291 }
292
293 shader += "}\n";
294
295 return shader;
296}
297
298QByteArray qgfx_gaussianFragmentShader(QGfxGaussSample *p, int samples, bool alphaOnly)
299{
300 QByteArray shader;
301 shader.reserve(asize: 1024);
302 shader += "#version 440\n\n";
303
304 qgfx_declareUniforms(shader, alphaOnly);
305
306 shader += "layout(binding = 1) uniform sampler2D source;";
307 shader += "layout(location = 0) out vec4 fragColor;\n";
308
309 qgfx_declareBlur(shader, direction: "in", s: p, samples);
310
311 shader += "\nvoid main() {\n"
312 " fragColor = ";
313 if (alphaOnly)
314 shader += "mix(vec4(0), color, clamp((";
315 else
316 shader += "(";
317
318 qreal sum = 0;
319 for (int i=0; i<samples; ++i)
320 sum += p[i].weight;
321
322 for (int i=0; i<samples; ++i) {
323 shader += "\n + float(";
324 shader += QByteArray::number(p[i].weight / sum);
325 shader += ") * texture(source, ";
326 shader += p[i].name;
327 shader += ")";
328 if (alphaOnly)
329 shader += ".a";
330 }
331
332 shader += "\n )";
333 if (alphaOnly)
334 shader += "/thickness, 0.0, 1.0))";
335 shader += "* qt_Opacity;\n}";
336
337 return shader;
338}
339
340static QByteArray qgfx_fallbackVertexShader(bool alphaOnly)
341{
342 QByteArray vertexShader =
343 "#version 440\n"
344 "layout(location = 0) in vec4 qt_Vertex;\n"
345 "layout(location = 1) in vec2 qt_MultiTexCoord0;\n\n";
346
347 qgfx_declareUniforms(shader&: vertexShader, alphaOnly);
348
349 vertexShader +=
350 "layout(location = 0) out vec2 qt_TexCoord0;\n"
351 "out gl_PerVertex { vec4 gl_Position; };\n"
352 "void main() {\n"
353 " gl_Position = qt_Matrix * qt_Vertex;\n"
354 " qt_TexCoord0 = qt_MultiTexCoord0;\n"
355 "}\n";
356
357 return vertexShader;
358}
359
360static QByteArray qgfx_fallbackFragmentShader(int requestedRadius, qreal deviation, bool masked, bool alphaOnly)
361{
362 QByteArray fragShader = "#version 440\n\n";
363
364 qgfx_declareUniforms(shader&: fragShader, alphaOnly);
365
366 fragShader += "layout(binding = 1) uniform sampler2D source;\n";
367 if (masked)
368 fragShader += "layout(binding = 2) uniform sampler2D mask;\n";
369
370 fragShader +=
371 "layout(location = 0) out vec4 fragColor;\n"
372 "layout(location = 0) in vec2 qt_TexCoord0;\n"
373 "\n"
374 "void main() {\n";
375 if (alphaOnly)
376 fragShader += " float result = 0.0;\n";
377 else
378 fragShader += " vec4 result = vec4(0);\n";
379 fragShader += " vec2 pixelStep = dirstep * spread;\n";
380 if (masked)
381 fragShader += " pixelStep *= texture(mask, qt_TexCoord0).a;\n";
382
383 float wSum = 0;
384 for (int r=-requestedRadius; r<=requestedRadius; ++r) {
385 float w = qgfx_gaussian(x: r, d: deviation);
386 wSum += w;
387 fragShader += " result += float(";
388 fragShader += QByteArray::number(w);
389 fragShader += ") * texture(source, qt_TexCoord0 + pixelStep * float(";
390 fragShader += QByteArray::number(r);
391 fragShader += "))";
392 if (alphaOnly)
393 fragShader += ".a";
394 fragShader += ";\n";
395 }
396 fragShader += " const float wSum = float(";
397 fragShader += QByteArray::number(wSum);
398 fragShader += ");\n"
399 " fragColor = ";
400 if (alphaOnly)
401 fragShader += "mix(vec4(0), color, clamp((result / wSum) / thickness, 0.0, 1.0)) * qt_Opacity;\n";
402 else
403 fragShader += "(qt_Opacity / wSum) * result;\n";
404 fragShader += "}\n";
405
406 return fragShader;
407}
408
409QVariantMap QGfxShaderBuilder::gaussianBlur(const QJSValue &parameters)
410{
411 int requestedRadius = qMax(a: 0.0, b: parameters.property(QStringLiteral("radius")).toNumber());
412 qreal deviation = parameters.property(QStringLiteral("deviation")).toNumber();
413 bool masked = parameters.property(QStringLiteral("masked")).toBool();
414 bool alphaOnly = parameters.property(QStringLiteral("alphaOnly")).toBool();
415
416 int requestedSamples = requestedRadius * 2 + 1;
417 int samples = 1 + requestedSamples / 2;
418 int radius = requestedSamples / 4;
419 bool fallback = parameters.property(QStringLiteral("fallback")).toBool();
420
421 QVariantMap result;
422
423 QByteArray vertexShader;
424 QByteArray fragmentShader;
425 if (samples > m_maxBlurSamples || masked || fallback) {
426 fragmentShader = qgfx_fallbackFragmentShader(requestedRadius, deviation, masked, alphaOnly);
427 vertexShader = qgfx_fallbackVertexShader(alphaOnly);
428 } else {
429 QVarLengthArray<QGfxGaussSample, 64> p(samples);
430 qgfx_buildGaussSamplePoints(p: p.data(), samples, radius, deviation);
431
432 fragmentShader = qgfx_gaussianFragmentShader(p: p.data(), samples, alphaOnly);
433 vertexShader = qgfx_gaussianVertexShader(p: p.data(), samples, alphaOnly);
434 }
435
436 result["fragmentShader"] = buildFragmentShader(code: fragmentShader);
437 result["vertexShader"] = buildVertexShader(code: vertexShader);
438 return result;
439}
440
441QUrl QGfxShaderBuilder::buildFragmentShader(const QByteArray &code)
442{
443 return buildShader(code, stage: QShader::FragmentStage);
444}
445
446QUrl QGfxShaderBuilder::buildVertexShader(const QByteArray &code)
447{
448 return buildShader(code, stage: QShader::VertexStage);
449}
450
451QUrl QGfxShaderBuilder::buildShader(const QByteArray &code,
452 QShader::Stage stage)
453{
454 static bool recreateShaders = qEnvironmentVariableIntValue(varName: "QT_GFXSHADERBUILDER_REFRESH_CACHE");
455
456 QCryptographicHash fileNameHash(QCryptographicHash::Sha1);
457 fileNameHash.addData(data: code);
458
459 QString path = QStandardPaths::writableLocation(type: QStandardPaths::CacheLocation)
460 + QStringLiteral("/_qt_QGfxShaderBuilder_")
461 + QStringLiteral(QT_VERSION_STR)
462 + QStringLiteral("/");
463 QString filePath = path
464 + fileNameHash.result().toHex()
465 + QStringLiteral(".qsb");
466
467 if (!QFile::exists(fileName: filePath) || recreateShaders) {
468 if (!QDir().mkpath(dirPath: path)) {
469 qWarning() << "QGfxShaderBuilder: Failed to create path:" << path;
470 return QUrl{};
471
472 }
473
474 QFile output(filePath);
475 if (!output.open(flags: QIODevice::WriteOnly)) {
476 qWarning() << "QGfxShaderBuilder: Failed to store shader cache in file:" << filePath;
477 return QUrl{};
478 }
479
480 m_shaderBaker.setSourceString(sourceString: code, stage, fileName: filePath);
481 {
482 QShader compiledShader = m_shaderBaker.bake();
483 if (!compiledShader.isValid()) {
484 qWarning() << "QGfxShaderBuilder: Failed to compile shader for stage "
485 << stage << ": "
486 << m_shaderBaker.errorMessage()
487 << QString(code).replace(before: '\n', after: QChar(QChar::LineFeed));
488 return QUrl{};
489 }
490 output.write(data: compiledShader.serialized());
491 }
492 }
493
494 return QUrl::fromLocalFile(localfile: filePath);
495}
496
497QT_END_NAMESPACE
498
499#include "moc_qgfxshaderbuilder_p.cpp"
500

source code of qt5compat/src/imports/graphicaleffects5/private/qgfxshaderbuilder.cpp