| 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 | |