| 1 | // Copyright (C) 2019 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qsgrhiatlastexture_p.h" |
| 5 | |
| 6 | #include <QtCore/QVarLengthArray> |
| 7 | #include <QtCore/QElapsedTimer> |
| 8 | #include <QtCore/QtMath> |
| 9 | |
| 10 | #include <QtGui/QWindow> |
| 11 | |
| 12 | #include <private/qqmlglobal_p.h> |
| 13 | #include <private/qsgdefaultrendercontext_p.h> |
| 14 | #include <private/qsgtexture_p.h> |
| 15 | #include <private/qsgcompressedtexture_p.h> |
| 16 | #include <private/qsgcompressedatlastexture_p.h> |
| 17 | |
| 18 | QT_BEGIN_NAMESPACE |
| 19 | |
| 20 | int qt_sg_envInt(const char *name, int defaultValue); |
| 21 | |
| 22 | static QElapsedTimer qsg_renderer_timer; |
| 23 | |
| 24 | DEFINE_BOOL_CONFIG_OPTION(qsgEnableCompressedAtlas, QSG_ENABLE_COMPRESSED_ATLAS) |
| 25 | |
| 26 | namespace QSGRhiAtlasTexture |
| 27 | { |
| 28 | |
| 29 | Manager::Manager(QSGDefaultRenderContext *rc, const QSize &surfacePixelSize, QSurface *maybeSurface) |
| 30 | : m_rc(rc) |
| 31 | , m_rhi(rc->rhi()) |
| 32 | { |
| 33 | const int maxSize = m_rhi->resourceLimit(limit: QRhi::TextureSizeMax); |
| 34 | // surfacePixelSize is just a hint that was passed in when initializing the |
| 35 | // rendercontext, likely based on the window size, if it was available, |
| 36 | // that is. Therefore, it may be anything, incl. zero and negative. |
| 37 | const int widthHint = qMax(a: 1, b: surfacePixelSize.width()); |
| 38 | const int heightHint = qMax(a: 1, b: surfacePixelSize.height()); |
| 39 | int w = qMin(a: maxSize, b: qt_sg_envInt(name: "QSG_ATLAS_WIDTH" , defaultValue: qMax(a: 512U, b: qNextPowerOfTwo(v: widthHint - 1)))); |
| 40 | int h = qMin(a: maxSize, b: qt_sg_envInt(name: "QSG_ATLAS_HEIGHT" , defaultValue: qMax(a: 512U, b: qNextPowerOfTwo(v: heightHint - 1)))); |
| 41 | |
| 42 | if (maybeSurface && maybeSurface->surfaceClass() == QSurface::Window) { |
| 43 | QWindow *window = static_cast<QWindow *>(maybeSurface); |
| 44 | // Coverwindows, optimize for memory rather than speed |
| 45 | if ((window->type() & Qt::CoverWindow) == Qt::CoverWindow) { |
| 46 | w /= 2; |
| 47 | h /= 2; |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | m_atlas_size_limit = qt_sg_envInt(name: "QSG_ATLAS_SIZE_LIMIT" , defaultValue: qMax(a: w, b: h) / 2); |
| 52 | m_atlas_size = QSize(w, h); |
| 53 | |
| 54 | qCDebug(QSG_LOG_INFO, "rhi texture atlas dimensions: %dx%d" , w, h); |
| 55 | } |
| 56 | |
| 57 | Manager::~Manager() |
| 58 | { |
| 59 | Q_ASSERT(m_atlas == nullptr); |
| 60 | Q_ASSERT(m_atlases.isEmpty()); |
| 61 | } |
| 62 | |
| 63 | void Manager::invalidate() |
| 64 | { |
| 65 | if (m_atlas) { |
| 66 | m_atlas->invalidate(); |
| 67 | m_atlas->deleteLater(); |
| 68 | m_atlas = nullptr; |
| 69 | } |
| 70 | |
| 71 | QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::iterator i = m_atlases.begin(); |
| 72 | while (i != m_atlases.end()) { |
| 73 | i.value()->invalidate(); |
| 74 | i.value()->deleteLater(); |
| 75 | ++i; |
| 76 | } |
| 77 | m_atlases.clear(); |
| 78 | } |
| 79 | |
| 80 | QSGTexture *Manager::create(const QImage &image, bool hasAlphaChannel) |
| 81 | { |
| 82 | Texture *t = nullptr; |
| 83 | if (image.width() < m_atlas_size_limit && image.height() < m_atlas_size_limit) { |
| 84 | if (!m_atlas) |
| 85 | m_atlas = new Atlas(m_rc, m_atlas_size); |
| 86 | t = m_atlas->create(image); |
| 87 | if (t && !hasAlphaChannel && t->hasAlphaChannel()) |
| 88 | t->setHasAlphaChannel(false); |
| 89 | } |
| 90 | return t; |
| 91 | } |
| 92 | |
| 93 | QSGTexture *Manager::create(const QSGCompressedTextureFactory *factory) |
| 94 | { |
| 95 | QSGTexture *t = nullptr; |
| 96 | if (!qsgEnableCompressedAtlas() || !factory->textureData()->isValid()) |
| 97 | return t; |
| 98 | |
| 99 | unsigned int format = factory->textureData()->glInternalFormat(); |
| 100 | QSGCompressedTexture::FormatInfo fmt = QSGCompressedTexture::formatInfo(glTextureFormat: format); |
| 101 | if (!m_rhi->isTextureFormatSupported(format: fmt.rhiFormat)) |
| 102 | return t; |
| 103 | |
| 104 | QSize size = factory->textureData()->size(); |
| 105 | if (size.width() < m_atlas_size_limit && size.height() < m_atlas_size_limit) { |
| 106 | QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::iterator i = m_atlases.find(key: format); |
| 107 | if (i == m_atlases.cend()) { |
| 108 | auto newAtlas = new QSGCompressedAtlasTexture::Atlas(m_rc, m_atlas_size, format); |
| 109 | i = m_atlases.insert(key: format, value: newAtlas); |
| 110 | } |
| 111 | const QTextureFileData *cmpData = factory->textureData(); |
| 112 | t = i.value()->create(data: cmpData->getDataView(), size); |
| 113 | } |
| 114 | |
| 115 | return t; |
| 116 | } |
| 117 | |
| 118 | AtlasBase::AtlasBase(QSGDefaultRenderContext *rc, const QSize &size) |
| 119 | : m_rc(rc) |
| 120 | , m_rhi(rc->rhi()) |
| 121 | , m_allocator(size) |
| 122 | , m_size(size) |
| 123 | { |
| 124 | } |
| 125 | |
| 126 | AtlasBase::~AtlasBase() |
| 127 | { |
| 128 | Q_ASSERT(!m_texture); |
| 129 | } |
| 130 | |
| 131 | void AtlasBase::invalidate() |
| 132 | { |
| 133 | delete m_texture; |
| 134 | m_texture = nullptr; |
| 135 | } |
| 136 | |
| 137 | void AtlasBase::commitTextureOperations(QRhiResourceUpdateBatch *resourceUpdates) |
| 138 | { |
| 139 | if (!m_allocated) { |
| 140 | m_allocated = true; |
| 141 | if (!generateTexture()) { |
| 142 | qWarning(msg: "QSGTextureAtlas: Failed to create texture" ); |
| 143 | return; |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | for (TextureBase *t : m_pending_uploads) |
| 148 | enqueueTextureUpload(t, resourceUpdates); |
| 149 | |
| 150 | m_pending_uploads.clear(); |
| 151 | } |
| 152 | |
| 153 | void AtlasBase::remove(TextureBase *t) |
| 154 | { |
| 155 | QRect atlasRect = t->atlasSubRect(); |
| 156 | m_allocator.deallocate(rect: atlasRect); |
| 157 | m_pending_uploads.removeOne(t); |
| 158 | } |
| 159 | |
| 160 | Atlas::Atlas(QSGDefaultRenderContext *rc, const QSize &size) |
| 161 | : AtlasBase(rc, size) |
| 162 | { |
| 163 | m_format = QRhiTexture::RGBA8; |
| 164 | |
| 165 | // Mirror QSGPlainTexture by playing nice with ARGB32[_Pre], because due to |
| 166 | // legacy that's what most images come in, not the byte-ordered |
| 167 | // RGBA8888[_Pre]. (i.e. with this the behavior matches 5.15) However, |
| 168 | // QSGPlainTexture can make a separate decision for each image (texture), |
| 169 | // the atlas cannot, so the downside is that now images that come in the |
| 170 | // modern byte-ordered formats need a conversion. So perhaps reconsider this |
| 171 | // at some point in the future. |
| 172 | #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN |
| 173 | if (rc->rhi()->isTextureFormatSupported(format: QRhiTexture::BGRA8)) |
| 174 | m_format = QRhiTexture::BGRA8; |
| 175 | #endif |
| 176 | |
| 177 | m_debug_overlay = qt_sg_envInt(name: "QSG_ATLAS_OVERLAY" , defaultValue: 0); |
| 178 | |
| 179 | // images smaller than this will retain their QImage. |
| 180 | // by default no images are retained (favoring memory) |
| 181 | // set to a very large value to retain all images (allowing quick removal from the atlas) |
| 182 | m_atlas_transient_image_threshold = qt_sg_envInt(name: "QSG_ATLAS_TRANSIENT_IMAGE_THRESHOLD" , defaultValue: 0); |
| 183 | } |
| 184 | |
| 185 | Atlas::~Atlas() |
| 186 | { |
| 187 | } |
| 188 | |
| 189 | Texture *Atlas::create(const QImage &image) |
| 190 | { |
| 191 | // No need to lock, as manager already locked it. |
| 192 | QRect rect = m_allocator.allocate(size: QSize(image.width() + 2, image.height() + 2)); |
| 193 | if (rect.width() > 0 && rect.height() > 0) { |
| 194 | Texture *t = new Texture(this, rect, image); |
| 195 | m_pending_uploads << t; |
| 196 | return t; |
| 197 | } |
| 198 | return nullptr; |
| 199 | } |
| 200 | |
| 201 | bool Atlas::generateTexture() |
| 202 | { |
| 203 | m_texture = m_rhi->newTexture(format: m_format, pixelSize: m_size, sampleCount: 1, flags: QRhiTexture::UsedAsTransferSource); |
| 204 | if (!m_texture) |
| 205 | return false; |
| 206 | |
| 207 | if (!m_texture->create()) { |
| 208 | delete m_texture; |
| 209 | m_texture = nullptr; |
| 210 | return false; |
| 211 | } |
| 212 | |
| 213 | return true; |
| 214 | } |
| 215 | |
| 216 | void Atlas::enqueueTextureUpload(TextureBase *t, QRhiResourceUpdateBatch *resourceUpdates) |
| 217 | { |
| 218 | Texture *tex = static_cast<Texture *>(t); |
| 219 | const QRect &r = tex->atlasSubRect(); |
| 220 | QImage image = tex->image(); |
| 221 | |
| 222 | if (image.isNull()) |
| 223 | return; |
| 224 | |
| 225 | if (m_format == QRhiTexture::BGRA8) { |
| 226 | if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32_Premultiplied) |
| 227 | image = std::move(image).convertToFormat(f: QImage::Format_ARGB32_Premultiplied); |
| 228 | } else if (image.format() != QImage::Format_RGBA8888_Premultiplied) { |
| 229 | image = std::move(image).convertToFormat(f: QImage::Format_RGBA8888_Premultiplied); |
| 230 | } |
| 231 | |
| 232 | if (m_debug_overlay) { |
| 233 | QPainter p(&image); |
| 234 | p.setCompositionMode(QPainter::CompositionMode_SourceAtop); |
| 235 | p.fillRect(x: 0, y: 0, w: image.width(), h: image.height(), b: QBrush(QColor::fromRgbF(r: 0, g: 1, b: 1, a: 0.5))); |
| 236 | } |
| 237 | |
| 238 | const int iw = image.width(); |
| 239 | const int ih = image.height(); |
| 240 | const int bpl = image.bytesPerLine() / 4; |
| 241 | QVarLengthArray<quint32, 1024> tmpBits(qMax(a: iw + 2, b: ih + 2)); |
| 242 | const int tmpBitsSize = tmpBits.size() * 4; |
| 243 | const quint32 *src = reinterpret_cast<const quint32 *>(image.constBits()); |
| 244 | quint32 *dst = tmpBits.data(); |
| 245 | QVarLengthArray<QRhiTextureUploadEntry, 5> entries; |
| 246 | |
| 247 | // top row, padding corners |
| 248 | dst[0] = src[0]; |
| 249 | memcpy(dest: dst + 1, src: src, n: iw * sizeof(quint32)); |
| 250 | dst[1 + iw] = src[iw - 1]; |
| 251 | { |
| 252 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
| 253 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y())); |
| 254 | subresDesc.setSourceSize(QSize(iw + 2, 1)); |
| 255 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 256 | } |
| 257 | |
| 258 | // bottom row, padded corners |
| 259 | const quint32 *lastRow = src + bpl * (ih - 1); |
| 260 | dst[0] = lastRow[0]; |
| 261 | memcpy(dest: dst + 1, src: lastRow, n: iw * sizeof(quint32)); |
| 262 | dst[1 + iw] = lastRow[iw - 1]; |
| 263 | { |
| 264 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
| 265 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + ih + 1)); |
| 266 | subresDesc.setSourceSize(QSize(iw + 2, 1)); |
| 267 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 268 | } |
| 269 | |
| 270 | // left column |
| 271 | for (int i = 0; i < ih; ++i) |
| 272 | dst[i] = src[i * bpl]; |
| 273 | { |
| 274 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
| 275 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + 1)); |
| 276 | subresDesc.setSourceSize(QSize(1, ih)); |
| 277 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 278 | } |
| 279 | |
| 280 | |
| 281 | // right column |
| 282 | for (int i = 0; i < ih; ++i) |
| 283 | dst[i] = src[i * bpl + iw - 1]; |
| 284 | { |
| 285 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
| 286 | subresDesc.setDestinationTopLeft(QPoint(r.x() + iw + 1, r.y() + 1)); |
| 287 | subresDesc.setSourceSize(QSize(1, ih)); |
| 288 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 289 | } |
| 290 | |
| 291 | // Inner part of the image.... |
| 292 | if (bpl != iw) { |
| 293 | int sy = r.y() + 1; |
| 294 | int ey = sy + r.height() - 2; |
| 295 | entries.reserve(sz: 4 + (ey - sy)); |
| 296 | for (int y = sy; y < ey; ++y) { |
| 297 | QRhiTextureSubresourceUploadDescription subresDesc(src, image.bytesPerLine()); |
| 298 | subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, y)); |
| 299 | subresDesc.setSourceSize(QSize(r.width() - 2, 1)); |
| 300 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 301 | src += bpl; |
| 302 | } |
| 303 | } else { |
| 304 | QRhiTextureSubresourceUploadDescription subresDesc(src, image.sizeInBytes()); |
| 305 | subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, r.y() + 1)); |
| 306 | subresDesc.setSourceSize(QSize(r.width() - 2, r.height() - 2)); |
| 307 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
| 308 | } |
| 309 | |
| 310 | QRhiTextureUploadDescription desc; |
| 311 | desc.setEntries(first: entries.cbegin(), last: entries.cend()); |
| 312 | resourceUpdates->uploadTexture(tex: m_texture, desc); |
| 313 | |
| 314 | const QSize textureSize = t->textureSize(); |
| 315 | if (textureSize.width() > m_atlas_transient_image_threshold || textureSize.height() > m_atlas_transient_image_threshold) |
| 316 | tex->releaseImage(); |
| 317 | |
| 318 | qCDebug(QSG_LOG_TIME_TEXTURE, "atlastexture upload enqueued in: %lldms (%dx%d)" , |
| 319 | qsg_renderer_timer.elapsed(), |
| 320 | t->textureSize().width(), |
| 321 | t->textureSize().height()); |
| 322 | } |
| 323 | |
| 324 | TextureBase::TextureBase(AtlasBase *atlas, const QRect &textureRect) |
| 325 | : QSGTexture(*(new QSGTexturePrivate(this))) |
| 326 | , m_allocated_rect(textureRect) |
| 327 | , m_atlas(atlas) |
| 328 | { |
| 329 | } |
| 330 | |
| 331 | TextureBase::~TextureBase() |
| 332 | { |
| 333 | m_atlas->remove(t: this); |
| 334 | } |
| 335 | |
| 336 | qint64 TextureBase::comparisonKey() const |
| 337 | { |
| 338 | // We need special care here: a typical comparisonKey() implementation |
| 339 | // returns a unique result when there is no underlying texture yet. This is |
| 340 | // not quite ideal for atlasing however since textures with the same atlas |
| 341 | // should be considered equal regardless of the state of the underlying |
| 342 | // graphics resources. |
| 343 | |
| 344 | // base the comparison on the atlas ptr; this way textures for the same |
| 345 | // atlas are considered equal |
| 346 | return qint64(m_atlas); |
| 347 | } |
| 348 | |
| 349 | QRhiTexture *TextureBase::rhiTexture() const |
| 350 | { |
| 351 | return m_atlas->m_texture; |
| 352 | } |
| 353 | |
| 354 | void TextureBase::commitTextureOperations(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates) |
| 355 | { |
| 356 | #ifdef QT_NO_DEBUG |
| 357 | Q_UNUSED(rhi); |
| 358 | #endif |
| 359 | Q_ASSERT(rhi == m_atlas->m_rhi); |
| 360 | m_atlas->commitTextureOperations(resourceUpdates); |
| 361 | } |
| 362 | |
| 363 | Texture::Texture(Atlas *atlas, const QRect &textureRect, const QImage &image) |
| 364 | : TextureBase(atlas, textureRect) |
| 365 | , m_image(image) |
| 366 | , m_has_alpha(image.hasAlphaChannel()) |
| 367 | { |
| 368 | float w = atlas->size().width(); |
| 369 | float h = atlas->size().height(); |
| 370 | QRect nopad = atlasSubRectWithoutPadding(); |
| 371 | m_texture_coords_rect = QRectF(nopad.x() / w, |
| 372 | nopad.y() / h, |
| 373 | nopad.width() / w, |
| 374 | nopad.height() / h); |
| 375 | } |
| 376 | |
| 377 | Texture::~Texture() |
| 378 | { |
| 379 | if (m_nonatlas_texture) |
| 380 | delete m_nonatlas_texture; |
| 381 | } |
| 382 | |
| 383 | QSGTexture *Texture::removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdates) const |
| 384 | { |
| 385 | if (!m_nonatlas_texture) { |
| 386 | m_nonatlas_texture = new QSGPlainTexture; |
| 387 | if (!m_image.isNull()) { |
| 388 | m_nonatlas_texture->setImage(m_image); |
| 389 | m_nonatlas_texture->setFiltering(filtering()); |
| 390 | } else { |
| 391 | QSGDefaultRenderContext *rc = m_atlas->renderContext(); |
| 392 | QRhi *rhi = m_atlas->rhi(); |
| 393 | Q_ASSERT(rhi->isRecordingFrame()); |
| 394 | const QRect r = atlasSubRectWithoutPadding(); |
| 395 | |
| 396 | QRhiTexture * = rhi->newTexture(format: m_atlas->texture()->format(), pixelSize: r.size()); |
| 397 | if (extractTex->create()) { |
| 398 | bool ownResUpd = false; |
| 399 | QRhiResourceUpdateBatch *resUpd = resourceUpdates; |
| 400 | if (!resUpd) { |
| 401 | ownResUpd = true; |
| 402 | resUpd = rhi->nextResourceUpdateBatch(); |
| 403 | } |
| 404 | QRhiTextureCopyDescription desc; |
| 405 | desc.setSourceTopLeft(r.topLeft()); |
| 406 | desc.setPixelSize(r.size()); |
| 407 | resUpd->copyTexture(dst: extractTex, src: m_atlas->texture(), desc); |
| 408 | if (ownResUpd) |
| 409 | rc->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates: resUpd); |
| 410 | } |
| 411 | |
| 412 | m_nonatlas_texture->setTexture(extractTex); |
| 413 | m_nonatlas_texture->setOwnsTexture(true); |
| 414 | m_nonatlas_texture->setHasAlphaChannel(m_has_alpha); |
| 415 | m_nonatlas_texture->setTextureSize(r.size()); |
| 416 | } |
| 417 | } |
| 418 | |
| 419 | m_nonatlas_texture->setMipmapFiltering(mipmapFiltering()); |
| 420 | m_nonatlas_texture->setFiltering(filtering()); |
| 421 | return m_nonatlas_texture; |
| 422 | } |
| 423 | |
| 424 | } |
| 425 | |
| 426 | QT_END_NAMESPACE |
| 427 | |
| 428 | #include "moc_qsgrhiatlastexture_p.cpp" |
| 429 | |