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 | |
22 | QT_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 | |
52 | QGfxShaderBuilder::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 | |
116 | QGfxShaderBuilder::~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 | |
178 | static qreal qgfx_gaussian(qreal x, qreal d) |
179 | { |
180 | return qExp(v: - x * x / (2 * d * d)); |
181 | } |
182 | |
183 | struct 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 | |
195 | static 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 | |
204 | static 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 | |
249 | void 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 | |
264 | QByteArray 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 | |
298 | QByteArray 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 | |
340 | static 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 | |
360 | static 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 | |
409 | QVariantMap QGfxShaderBuilder::gaussianBlur(const QJSValue ¶meters) |
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 | |
441 | QUrl QGfxShaderBuilder::buildFragmentShader(const QByteArray &code) |
442 | { |
443 | return buildShader(code, stage: QShader::FragmentStage); |
444 | } |
445 | |
446 | QUrl QGfxShaderBuilder::buildVertexShader(const QByteArray &code) |
447 | { |
448 | return buildShader(code, stage: QShader::VertexStage); |
449 | } |
450 | |
451 | QUrl 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 | |
497 | QT_END_NAMESPACE |
498 | |
499 | #include "moc_qgfxshaderbuilder_p.cpp" |
500 | |