1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2019 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQuick 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 "qsgplaintexture_p.h" |
41 | #include <QtQuick/private/qsgcontext_p.h> |
42 | #include <qmath.h> |
43 | #include <private/qquickprofiler_p.h> |
44 | #include <private/qqmlglobal_p.h> |
45 | #include <QtGui/qguiapplication.h> |
46 | #include <QtGui/qpa/qplatformnativeinterface.h> |
47 | #if QT_CONFIG(opengl) |
48 | # include <QtGui/qopenglcontext.h> |
49 | # include <QtGui/qopenglfunctions.h> |
50 | # include <QtGui/private/qopengltextureuploader_p.h> |
51 | # include <private/qsgdefaultrendercontext_p.h> |
52 | #endif |
53 | #include <QtGui/private/qrhi_p.h> |
54 | |
55 | #include <qtquick_tracepoints_p.h> |
56 | |
57 | #if QT_CONFIG(opengl) |
58 | static QElapsedTimer qsg_renderer_timer; |
59 | #endif |
60 | |
61 | #ifndef GL_BGRA |
62 | #define GL_BGRA 0x80E1 |
63 | #endif |
64 | |
65 | #ifndef GL_TEXTURE_MAX_ANISOTROPY_EXT |
66 | #define GL_TEXTURE_MAX_ANISOTROPY_EXT 0x84FE |
67 | #endif |
68 | |
69 | QT_BEGIN_NAMESPACE |
70 | |
71 | QSGPlainTexture::QSGPlainTexture() |
72 | : QSGTexture(*(new QSGPlainTexturePrivate)) |
73 | , m_texture_id(0) |
74 | , m_texture(nullptr) |
75 | , m_has_alpha(false) |
76 | , m_dirty_texture(false) |
77 | , m_dirty_bind_options(false) |
78 | , m_owns_texture(true) |
79 | , m_mipmaps_generated(false) |
80 | , m_retain_image(false) |
81 | , m_mipmap_warned(false) |
82 | { |
83 | } |
84 | |
85 | QSGPlainTexture::QSGPlainTexture(QSGPlainTexturePrivate &dd) |
86 | : QSGTexture(dd) |
87 | , m_texture_id(0) |
88 | , m_texture(nullptr) |
89 | , m_has_alpha(false) |
90 | , m_dirty_texture(false) |
91 | , m_dirty_bind_options(false) |
92 | , m_owns_texture(true) |
93 | , m_mipmaps_generated(false) |
94 | , m_retain_image(false) |
95 | , m_mipmap_warned(false) |
96 | { |
97 | } |
98 | |
99 | QSGPlainTexture::~QSGPlainTexture() |
100 | { |
101 | #if QT_CONFIG(opengl) |
102 | if (m_texture_id && m_owns_texture && QOpenGLContext::currentContext()) |
103 | QOpenGLContext::currentContext()->functions()->glDeleteTextures(n: 1, textures: &m_texture_id); |
104 | #endif |
105 | if (m_texture && m_owns_texture) |
106 | delete m_texture; |
107 | } |
108 | |
109 | void QSGPlainTexture::setImage(const QImage &image) |
110 | { |
111 | m_image = image; |
112 | m_texture_size = image.size(); |
113 | m_has_alpha = image.hasAlphaChannel(); |
114 | m_dirty_texture = true; |
115 | m_dirty_bind_options = true; |
116 | m_mipmaps_generated = false; |
117 | } |
118 | |
119 | int QSGPlainTexture::textureId() const // legacy (GL-only) |
120 | { |
121 | if (m_dirty_texture) { |
122 | if (m_image.isNull()) { |
123 | // The actual texture and id will be updated/deleted in a later bind() |
124 | // or ~QSGPlainTexture so just keep it minimal here. |
125 | return 0; |
126 | } else if (m_texture_id == 0){ |
127 | #if QT_CONFIG(opengl) |
128 | // Generate a texture id for use later and return it. |
129 | QOpenGLContext::currentContext()->functions()->glGenTextures(n: 1, textures: &const_cast<QSGPlainTexture *>(this)->m_texture_id); |
130 | #endif |
131 | return m_texture_id; |
132 | } |
133 | } |
134 | return m_texture_id; |
135 | } |
136 | |
137 | void QSGPlainTexture::setTextureId(int id) // legacy (GL-only) |
138 | { |
139 | #if QT_CONFIG(opengl) |
140 | if (m_texture_id && m_owns_texture) |
141 | QOpenGLContext::currentContext()->functions()->glDeleteTextures(n: 1, textures: &m_texture_id); |
142 | #endif |
143 | |
144 | m_texture_id = id; |
145 | m_dirty_texture = false; |
146 | m_dirty_bind_options = true; |
147 | m_image = QImage(); |
148 | m_mipmaps_generated = false; |
149 | } |
150 | |
151 | void QSGPlainTexture::bind() // legacy (GL-only) |
152 | { |
153 | #if QT_CONFIG(opengl) |
154 | Q_TRACE_SCOPE(QSG_texture_prepare); |
155 | QOpenGLContext *context = QOpenGLContext::currentContext(); |
156 | QOpenGLFunctions *funcs = context->functions(); |
157 | if (!m_dirty_texture) { |
158 | Q_TRACE_SCOPE(QSG_texture_bind); |
159 | funcs->glBindTexture(GL_TEXTURE_2D, texture: m_texture_id); |
160 | if (mipmapFiltering() != QSGTexture::None && !m_mipmaps_generated) { |
161 | funcs->glGenerateMipmap(GL_TEXTURE_2D); |
162 | m_mipmaps_generated = true; |
163 | } |
164 | updateBindOptions(force: m_dirty_bind_options); |
165 | m_dirty_bind_options = false; |
166 | return; |
167 | } |
168 | |
169 | m_dirty_texture = false; |
170 | |
171 | bool profileFrames = QSG_LOG_TIME_TEXTURE().isDebugEnabled(); |
172 | if (profileFrames) |
173 | qsg_renderer_timer.start(); |
174 | Q_QUICK_SG_PROFILE_START_SYNCHRONIZED(QQuickProfiler::SceneGraphTexturePrepare, |
175 | QQuickProfiler::SceneGraphTextureDeletion); |
176 | |
177 | |
178 | if (m_image.isNull()) { |
179 | if (m_texture_id && m_owns_texture) { |
180 | Q_TRACE_SCOPE(QSG_texture_delete); |
181 | funcs->glDeleteTextures(n: 1, textures: &m_texture_id); |
182 | qCDebug(QSG_LOG_TIME_TEXTURE, "plain texture deleted in %dms - %dx%d" , |
183 | (int) qsg_renderer_timer.elapsed(), |
184 | m_texture_size.width(), |
185 | m_texture_size.height()); |
186 | Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphTextureDeletion, |
187 | QQuickProfiler::SceneGraphTextureDeletionDelete); |
188 | } |
189 | m_texture_id = 0; |
190 | m_texture_size = QSize(); |
191 | m_has_alpha = false; |
192 | |
193 | return; |
194 | } |
195 | |
196 | Q_TRACE(QSG_texture_bind_entry); |
197 | |
198 | if (m_texture_id == 0) |
199 | funcs->glGenTextures(n: 1, textures: &m_texture_id); |
200 | funcs->glBindTexture(GL_TEXTURE_2D, texture: m_texture_id); |
201 | |
202 | qint64 bindTime = 0; |
203 | if (profileFrames) |
204 | bindTime = qsg_renderer_timer.nsecsElapsed(); |
205 | Q_TRACE(QSG_texture_bind_exit); |
206 | Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphTexturePrepare, |
207 | QQuickProfiler::SceneGraphTexturePrepareBind); |
208 | Q_TRACE(QSG_texture_upload_entry); |
209 | |
210 | // ### TODO: check for out-of-memory situations... |
211 | |
212 | QOpenGLTextureUploader::BindOptions options = QOpenGLTextureUploader::PremultipliedAlphaBindOption; |
213 | |
214 | // Downscale the texture to fit inside the max texture limit if it is too big. |
215 | // It would be better if the image was already downscaled to the right size, |
216 | // but this information is not always available at that time, so as a last |
217 | // resort we can do it here. Texture coordinates are normalized, so it |
218 | // won't cause any problems and actual texture sizes will be written |
219 | // based on QSGTexture::textureSize which is updated after this, so that |
220 | // should be ok. |
221 | int max; |
222 | if (auto rc = QSGDefaultRenderContext::from(context)) |
223 | max = rc->maxTextureSize(); |
224 | else |
225 | funcs->glGetIntegerv(GL_MAX_TEXTURE_SIZE, params: &max); |
226 | |
227 | m_texture_size = m_texture_size.boundedTo(otherSize: QSize(max, max)); |
228 | |
229 | // Scale to a power of two size if mipmapping is requested and the |
230 | // texture is npot and npot textures are not properly supported. |
231 | if (mipmapFiltering() != QSGTexture::None |
232 | && !funcs->hasOpenGLFeature(feature: QOpenGLFunctions::NPOTTextures)) { |
233 | options |= QOpenGLTextureUploader::PowerOfTwoBindOption; |
234 | } |
235 | |
236 | updateBindOptions(force: m_dirty_bind_options); |
237 | |
238 | QOpenGLTextureUploader::textureImage(GL_TEXTURE_2D, image: m_image, options, maxSize: QSize(max, max)); |
239 | |
240 | qint64 uploadTime = 0; |
241 | if (profileFrames) |
242 | uploadTime = qsg_renderer_timer.nsecsElapsed(); |
243 | Q_TRACE(QSG_texture_upload_exit); |
244 | Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphTexturePrepare, |
245 | QQuickProfiler::SceneGraphTexturePrepareUpload); |
246 | Q_TRACE(QSG_texture_mipmap_entry); |
247 | |
248 | if (mipmapFiltering() != QSGTexture::None) { |
249 | funcs->glGenerateMipmap(GL_TEXTURE_2D); |
250 | m_mipmaps_generated = true; |
251 | } |
252 | |
253 | qint64 mipmapTime = 0; |
254 | if (profileFrames) { |
255 | mipmapTime = qsg_renderer_timer.nsecsElapsed(); |
256 | qCDebug(QSG_LOG_TIME_TEXTURE, |
257 | "plain texture uploaded in: %dms (%dx%d), bind=%d, upload=%d, mipmap=%d%s" , |
258 | int(mipmapTime / 1000000), |
259 | m_texture_size.width(), m_texture_size.height(), |
260 | int(bindTime / 1000000), |
261 | int((uploadTime - bindTime)/1000000), |
262 | int((mipmapTime - uploadTime)/1000000), |
263 | m_texture_size != m_image.size() ? " (scaled to GL_MAX_TEXTURE_SIZE)" : "" ); |
264 | } |
265 | Q_TRACE(QSG_texture_mipmap_exit); |
266 | Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphTexturePrepare, |
267 | QQuickProfiler::SceneGraphTexturePrepareMipmap); |
268 | |
269 | m_texture_rect = QRectF(0, 0, 1, 1); |
270 | |
271 | m_dirty_bind_options = false; |
272 | if (!m_retain_image) |
273 | m_image = QImage(); |
274 | #endif |
275 | } |
276 | |
277 | void QSGPlainTexture::setTexture(QRhiTexture *texture) // RHI only |
278 | { |
279 | if (m_texture && m_owns_texture && m_texture != texture) |
280 | delete m_texture; |
281 | |
282 | m_texture = texture; |
283 | m_dirty_texture = false; |
284 | m_dirty_bind_options = true; |
285 | m_image = QImage(); |
286 | m_mipmaps_generated = false; |
287 | } |
288 | |
289 | void QSGPlainTexture::setTextureFromNativeObject(QRhi *rhi, QQuickWindow::NativeObjectType type, |
290 | const void *nativeObjectPtr, int nativeLayout, |
291 | const QSize &size, bool mipmap) |
292 | { |
293 | Q_UNUSED(type); |
294 | |
295 | QRhiTexture::Flags flags; |
296 | if (mipmap) |
297 | flags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips; |
298 | |
299 | QRhiTexture *t = rhi->newTexture(format: QRhiTexture::RGBA8, pixelSize: size, sampleCount: 1, flags); |
300 | |
301 | // ownership of the native object is never taken |
302 | t->buildFrom(src: {.object: nativeObjectPtr, .layout: nativeLayout}); |
303 | |
304 | setTexture(t); |
305 | } |
306 | |
307 | int QSGPlainTexturePrivate::comparisonKey() const |
308 | { |
309 | Q_Q(const QSGPlainTexture); |
310 | |
311 | // not textureId() as that would create an id when not yet done - that's not wanted here |
312 | if (q->m_texture_id) |
313 | return q->m_texture_id; |
314 | |
315 | if (q->m_texture) |
316 | return int(qintptr(q->m_texture)); |
317 | |
318 | // two textures (and so materials) with not-yet-created texture underneath are never equal |
319 | return int(qintptr(q)); |
320 | } |
321 | |
322 | QRhiTexture *QSGPlainTexturePrivate::rhiTexture() const |
323 | { |
324 | Q_Q(const QSGPlainTexture); |
325 | return q->m_texture; |
326 | } |
327 | |
328 | void QSGPlainTexturePrivate::updateRhiTexture(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates) |
329 | { |
330 | Q_Q(QSGPlainTexture); |
331 | |
332 | const bool hasMipMaps = q->mipmapFiltering() != QSGTexture::None; |
333 | const bool mipmappingChanged = q->m_texture && ((hasMipMaps && !q->m_texture->flags().testFlag(flag: QRhiTexture::MipMapped)) // did not have it before |
334 | || (!hasMipMaps && q->m_texture->flags().testFlag(flag: QRhiTexture::MipMapped))); // does not have it anymore |
335 | |
336 | if (!q->m_dirty_texture) { |
337 | if (!q->m_texture) |
338 | return; |
339 | if (q->m_texture && !mipmappingChanged) { |
340 | if (hasMipMaps && !q->m_mipmaps_generated) { |
341 | resourceUpdates->generateMips(tex: q->m_texture); |
342 | q->m_mipmaps_generated = true; |
343 | } |
344 | return; |
345 | } |
346 | } |
347 | |
348 | if (q->m_image.isNull()) { |
349 | if (!q->m_dirty_texture && mipmappingChanged) { |
350 | // Full Mipmap Panic! |
351 | if (!q->m_mipmap_warned) { |
352 | qWarning(msg: "QSGPlainTexture: Mipmap settings changed without having image data available. " |
353 | "Call setImage() again or enable m_retain_image. " |
354 | "Falling back to previous mipmap filtering mode." ); |
355 | q->m_mipmap_warned = true; |
356 | } |
357 | // leave the texture valid and rather ignore the mipmap mode change attempt |
358 | q->setMipmapFiltering(m_last_mipmap_filter); |
359 | return; |
360 | } |
361 | |
362 | if (q->m_texture && q->m_owns_texture) |
363 | delete q->m_texture; |
364 | |
365 | q->m_texture = nullptr; |
366 | q->m_texture_size = QSize(); |
367 | q->m_has_alpha = false; |
368 | |
369 | q->m_dirty_texture = false; |
370 | return; |
371 | } |
372 | |
373 | q->m_dirty_texture = false; |
374 | |
375 | QImage tmp; |
376 | bool bgra = false; |
377 | bool needsConvert = false; |
378 | if (q->m_image.format() == QImage::Format_RGB32 || q->m_image.format() == QImage::Format_ARGB32_Premultiplied) { |
379 | #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN |
380 | if (rhi->isTextureFormatSupported(format: QRhiTexture::BGRA8)) { |
381 | tmp = q->m_image; |
382 | bgra = true; |
383 | } else { |
384 | needsConvert = true; |
385 | } |
386 | #else |
387 | needsConvert = true; |
388 | #endif |
389 | } else if (q->m_image.format() == QImage::Format_RGBX8888 || q->m_image.format() == QImage::Format_RGBA8888_Premultiplied) { |
390 | tmp = q->m_image; |
391 | } else { |
392 | needsConvert = true; |
393 | } |
394 | |
395 | if (needsConvert) |
396 | tmp = q->m_image.convertToFormat(f: QImage::Format_RGBA8888_Premultiplied); |
397 | |
398 | // Downscale the texture to fit inside the max texture limit if it is too big. |
399 | // It would be better if the image was already downscaled to the right size, |
400 | // but this information is not always available at that time, so as a last |
401 | // resort we can do it here. Texture coordinates are normalized, so it |
402 | // won't cause any problems and actual texture sizes will be written |
403 | // based on QSGTexture::textureSize which is updated after this, so that |
404 | // should be ok. |
405 | const int max = rhi->resourceLimit(limit: QRhi::TextureSizeMax); |
406 | if (tmp.width() > max || tmp.height() > max) { |
407 | tmp = tmp.scaled(w: qMin(a: max, b: tmp.width()), h: qMin(a: max, b: tmp.height()), aspectMode: Qt::IgnoreAspectRatio, mode: Qt::SmoothTransformation); |
408 | q->m_texture_size = tmp.size(); |
409 | } |
410 | |
411 | if ((q->mipmapFiltering() != QSGTexture::None |
412 | || q->horizontalWrapMode() != QSGTexture::ClampToEdge |
413 | || q->verticalWrapMode() != QSGTexture::ClampToEdge) |
414 | && !rhi->isFeatureSupported(feature: QRhi::NPOTTextureRepeat)) |
415 | { |
416 | const int w = qNextPowerOfTwo(v: tmp.width() - 1); |
417 | const int h = qNextPowerOfTwo(v: tmp.height() - 1); |
418 | if (tmp.width() != w || tmp.height() != h) { |
419 | tmp = tmp.scaled(w, h, aspectMode: Qt::IgnoreAspectRatio, mode: Qt::SmoothTransformation); |
420 | q->m_texture_size = tmp.size(); |
421 | } |
422 | } |
423 | |
424 | bool needsRebuild = q->m_texture && q->m_texture->pixelSize() != q->m_texture_size; |
425 | |
426 | if (mipmappingChanged) { |
427 | QRhiTexture::Flags f = q->m_texture->flags(); |
428 | f.setFlag(flag: QRhiTexture::MipMapped, on: hasMipMaps); |
429 | f.setFlag(flag: QRhiTexture::UsedWithGenerateMips, on: hasMipMaps); |
430 | q->m_texture->setFlags(f); |
431 | needsRebuild = true; |
432 | } |
433 | |
434 | if (!q->m_texture) { |
435 | QRhiTexture::Flags f; |
436 | if (hasMipMaps) |
437 | f |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips; |
438 | |
439 | q->m_texture = rhi->newTexture(format: bgra ? QRhiTexture::BGRA8 : QRhiTexture::RGBA8, pixelSize: q->m_texture_size, sampleCount: 1, flags: f); |
440 | needsRebuild = true; |
441 | } |
442 | |
443 | if (needsRebuild) { |
444 | if (!q->m_texture->build()) { |
445 | qWarning(msg: "Failed to build texture for QSGPlainTexture (size %dx%d)" , |
446 | q->m_texture_size.width(), q->m_texture_size.height()); |
447 | return; |
448 | } |
449 | } |
450 | |
451 | if (tmp.width() * 4 != tmp.bytesPerLine()) |
452 | tmp = tmp.copy(); |
453 | |
454 | resourceUpdates->uploadTexture(tex: q->m_texture, image: tmp); |
455 | |
456 | if (hasMipMaps) { |
457 | resourceUpdates->generateMips(tex: q->m_texture); |
458 | q->m_mipmaps_generated = true; |
459 | } |
460 | |
461 | m_last_mipmap_filter = q->mipmapFiltering(); |
462 | q->m_texture_rect = QRectF(0, 0, 1, 1); |
463 | |
464 | if (!q->m_retain_image) |
465 | q->m_image = QImage(); |
466 | } |
467 | |
468 | QT_END_NAMESPACE |
469 | |