/**************************************************************************** ** ** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qsgrhiatlastexture_p.h" #include #include #include #include #include #include #include #include #include #if 0 #include #include #endif QT_BEGIN_NAMESPACE int qt_sg_envInt(const char *name, int defaultValue); static QElapsedTimer qsg_renderer_timer; //DEFINE_BOOL_CONFIG_OPTION(qsgEnableCompressedAtlas, QSG_ENABLE_COMPRESSED_ATLAS) namespace QSGRhiAtlasTexture { Manager::Manager(QSGDefaultRenderContext *rc, const QSize &surfacePixelSize, QSurface *maybeSurface) : m_rc(rc) , m_rhi(rc->rhi()) { const int maxSize = m_rhi->resourceLimit(QRhi::TextureSizeMax); int w = qMin(maxSize, qt_sg_envInt("QSG_ATLAS_WIDTH", qMax(512U, qNextPowerOfTwo(surfacePixelSize.width() - 1)))); int h = qMin(maxSize, qt_sg_envInt("QSG_ATLAS_HEIGHT", qMax(512U, qNextPowerOfTwo(surfacePixelSize.height() - 1)))); if (maybeSurface && maybeSurface->surfaceClass() == QSurface::Window) { QWindow *window = static_cast(maybeSurface); // Coverwindows, optimize for memory rather than speed if ((window->type() & Qt::CoverWindow) == Qt::CoverWindow) { w /= 2; h /= 2; } } m_atlas_size_limit = qt_sg_envInt("QSG_ATLAS_SIZE_LIMIT", qMax(w, h) / 2); m_atlas_size = QSize(w, h); qCDebug(QSG_LOG_INFO, "rhi texture atlas dimensions: %dx%d", w, h); } Manager::~Manager() { Q_ASSERT(m_atlas == nullptr); Q_ASSERT(m_atlases.isEmpty()); } void Manager::invalidate() { if (m_atlas) { m_atlas->invalidate(); m_atlas->deleteLater(); m_atlas = nullptr; } #if 0 QHash::iterator i = m_atlases.begin(); while (i != m_atlases.end()) { i.value()->invalidate(); i.value()->deleteLater(); ++i; } m_atlases.clear(); #endif } QSGTexture *Manager::create(const QImage &image, bool hasAlphaChannel) { Texture *t = nullptr; if (image.width() < m_atlas_size_limit && image.height() < m_atlas_size_limit) { if (!m_atlas) m_atlas = new Atlas(m_rc, m_atlas_size); t = m_atlas->create(image); if (t && !hasAlphaChannel && t->hasAlphaChannel()) t->setHasAlphaChannel(false); } return t; } QSGTexture *Manager::create(const QSGCompressedTextureFactory *factory) { Q_UNUSED(factory); return nullptr; // ### #if 0 QSGTexture *t = nullptr; if (!qsgEnableCompressedAtlas() || !factory->m_textureData.isValid()) return t; // TODO: further abstract the atlas and remove this restriction unsigned int format = factory->m_textureData.glInternalFormat(); switch (format) { case QOpenGLTexture::RGB8_ETC1: case QOpenGLTexture::RGB8_ETC2: case QOpenGLTexture::RGBA8_ETC2_EAC: case QOpenGLTexture::RGB8_PunchThrough_Alpha1_ETC2: break; default: return t; } QSize size = factory->m_textureData.size(); if (size.width() < m_atlas_size_limit && size.height() < m_atlas_size_limit) { QHash::iterator i = m_atlases.find(format); if (i == m_atlases.end()) i = m_atlases.insert(format, new QSGCompressedAtlasTexture::Atlas(m_atlas_size, format)); // must be multiple of 4 QSize paddedSize(((size.width() + 3) / 4) * 4, ((size.height() + 3) / 4) * 4); QByteArray data = factory->m_textureData.data(); t = i.value()->create(data, factory->m_textureData.dataLength(), factory->m_textureData.dataOffset(), size, paddedSize); } #endif } AtlasBase::AtlasBase(QSGDefaultRenderContext *rc, const QSize &size) : m_rc(rc) , m_rhi(rc->rhi()) , m_allocator(size) , m_size(size) { } AtlasBase::~AtlasBase() { Q_ASSERT(!m_texture); } void AtlasBase::invalidate() { delete m_texture; m_texture = nullptr; } void AtlasBase::updateRhiTexture(QRhiResourceUpdateBatch *resourceUpdates) { if (!m_allocated) { m_allocated = true; if (!generateTexture()) { qWarning("QSGTextureAtlas: Failed to create texture"); return; } } for (TextureBase *t : m_pending_uploads) { // ### this profiling is all wrong, the real work is done elsewhere bool profileFrames = QSG_LOG_TIME_TEXTURE().isDebugEnabled(); if (profileFrames) qsg_renderer_timer.start(); Q_TRACE_SCOPE(QSG_texture_prepare); Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphTexturePrepare); // Skip bind, convert, swizzle; they're irrelevant Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphTexturePrepare, QQuickProfiler::SceneGraphTexturePrepareStart, 3); Q_TRACE(QSG_texture_upload_entry); enqueueTextureUpload(t, resourceUpdates); Q_TRACE(QSG_texture_upload_exit); Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphTexturePrepare, QQuickProfiler::SceneGraphTexturePrepareUpload); // Skip mipmap; unused Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphTexturePrepare, QQuickProfiler::SceneGraphTexturePrepareUpload, 1); Q_QUICK_SG_PROFILE_REPORT(QQuickProfiler::SceneGraphTexturePrepare, QQuickProfiler::SceneGraphTexturePrepareMipmap); } m_pending_uploads.clear(); } void AtlasBase::remove(TextureBase *t) { QRect atlasRect = t->atlasSubRect(); m_allocator.deallocate(atlasRect); m_pending_uploads.removeOne(t); } Atlas::Atlas(QSGDefaultRenderContext *rc, const QSize &size) : AtlasBase(rc, size) { // use RGBA texture internally as that is the only one guaranteed to be always supported m_format = QRhiTexture::RGBA8; m_debug_overlay = qt_sg_envInt("QSG_ATLAS_OVERLAY", 0); // images smaller than this will retain their QImage. // by default no images are retained (favoring memory) // set to a very large value to retain all images (allowing quick removal from the atlas) m_atlas_transient_image_threshold = qt_sg_envInt("QSG_ATLAS_TRANSIENT_IMAGE_THRESHOLD", 0); } Atlas::~Atlas() { } Texture *Atlas::create(const QImage &image) { // No need to lock, as manager already locked it. QRect rect = m_allocator.allocate(QSize(image.width() + 2, image.height() + 2)); if (rect.width() > 0 && rect.height() > 0) { Texture *t = new Texture(this, rect, image); m_pending_uploads << t; return t; } return nullptr; } bool Atlas::generateTexture() { m_texture = m_rhi->newTexture(m_format, m_size, 1, QRhiTexture::UsedAsTransferSource); if (!m_texture) return false; if (!m_texture->build()) { delete m_texture; m_texture = nullptr; return false; } return true; } void Atlas::enqueueTextureUpload(TextureBase *t, QRhiResourceUpdateBatch *resourceUpdates) { Texture *tex = static_cast(t); const QRect &r = tex->atlasSubRect(); QImage image = tex->image(); if (image.isNull()) return; if (image.format() != QImage::Format_RGBA8888_Premultiplied) image = std::move(image).convertToFormat(QImage::Format_RGBA8888_Premultiplied); if (m_debug_overlay) { QPainter p(&image); p.setCompositionMode(QPainter::CompositionMode_SourceAtop); p.fillRect(0, 0, image.width(), image.height(), QBrush(QColor::fromRgbF(0, 1, 1, 0.5))); } const int iw = image.width(); const int ih = image.height(); const int bpl = image.bytesPerLine() / 4; QVarLengthArray tmpBits(qMax(iw + 2, ih + 2)); const int tmpBitsSize = tmpBits.size() * 4; const quint32 *src = reinterpret_cast(image.constBits()); quint32 *dst = tmpBits.data(); QVarLengthArray entries; // top row, padding corners dst[0] = src[0]; memcpy(dst + 1, src, iw * sizeof(quint32)); dst[1 + iw] = src[iw - 1]; { QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y())); subresDesc.setSourceSize(QSize(iw + 2, 1)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); } // bottom row, padded corners const quint32 *lastRow = src + bpl * (ih - 1); dst[0] = lastRow[0]; memcpy(dst + 1, lastRow, iw * sizeof(quint32)); dst[1 + iw] = lastRow[iw - 1]; { QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + ih + 1)); subresDesc.setSourceSize(QSize(iw + 2, 1)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); } // left column for (int i = 0; i < ih; ++i) dst[i] = src[i * bpl]; { QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + 1)); subresDesc.setSourceSize(QSize(1, ih)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); } // right column for (int i = 0; i < ih; ++i) dst[i] = src[i * bpl + iw - 1]; { QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); subresDesc.setDestinationTopLeft(QPoint(r.x() + iw + 1, r.y() + 1)); subresDesc.setSourceSize(QSize(1, ih)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); } // Inner part of the image.... if (bpl != iw) { int sy = r.y() + 1; int ey = sy + r.height() - 2; entries.reserve(4 + (ey - sy)); for (int y = sy; y < ey; ++y) { QRhiTextureSubresourceUploadDescription subresDesc(src, image.bytesPerLine()); subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, y)); subresDesc.setSourceSize(QSize(r.width() - 2, 1)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); src += bpl; } } else { QRhiTextureSubresourceUploadDescription subresDesc(src, image.sizeInBytes()); subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, r.y() + 1)); subresDesc.setSourceSize(QSize(r.width() - 2, r.height() - 2)); entries.append(QRhiTextureUploadEntry(0, 0, subresDesc)); } QRhiTextureUploadDescription desc; desc.setEntries(entries.cbegin(), entries.cend()); resourceUpdates->uploadTexture(m_texture, desc); const QSize textureSize = t->textureSize(); if (textureSize.width() > m_atlas_transient_image_threshold || textureSize.height() > m_atlas_transient_image_threshold) tex->releaseImage(); qCDebug(QSG_LOG_TIME_TEXTURE, "atlastexture upload enqueued in: %lldms (%dx%d)", qsg_renderer_timer.elapsed(), t->textureSize().width(), t->textureSize().height()); } TextureBase::TextureBase(AtlasBase *atlas, const QRect &textureRect) : QSGTexture(*(new TextureBasePrivate)) , m_allocated_rect(textureRect) , m_atlas(atlas) { } TextureBase::~TextureBase() { m_atlas->remove(this); } QRhiResourceUpdateBatch *TextureBase::workResourceUpdateBatch() const { Q_D(const TextureBase); return d->workResourceUpdateBatch; } int TextureBasePrivate::comparisonKey() const { Q_Q(const TextureBase); // We need special care here: a typical comparisonKey() implementation // returns a unique result when there is no underlying texture yet. This is // not quite ideal for atlasing however since textures with the same atlas // should be considered equal regardless of the state of the underlying // graphics resources. // base the comparison on the atlas ptr; this way textures for the same // atlas are considered equal return int(qintptr(q->m_atlas)); } QRhiTexture *TextureBasePrivate::rhiTexture() const { Q_Q(const TextureBase); return q->m_atlas->m_texture; } void TextureBasePrivate::updateRhiTexture(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates) { Q_Q(TextureBase); #ifdef QT_NO_DEBUG Q_UNUSED(rhi); #endif Q_ASSERT(rhi == q->m_atlas->m_rhi); q->m_atlas->updateRhiTexture(resourceUpdates); } Texture::Texture(Atlas *atlas, const QRect &textureRect, const QImage &image) : TextureBase(atlas, textureRect) , m_image(image) , m_has_alpha(image.hasAlphaChannel()) { float w = atlas->size().width(); float h = atlas->size().height(); QRect nopad = atlasSubRectWithoutPadding(); m_texture_coords_rect = QRectF(nopad.x() / w, nopad.y() / h, nopad.width() / w, nopad.height() / h); } Texture::~Texture() { if (m_nonatlas_texture) delete m_nonatlas_texture; } QSGTexture *Texture::removedFromAtlas() const { if (!m_nonatlas_texture) { m_nonatlas_texture = new QSGPlainTexture; if (!m_image.isNull()) { m_nonatlas_texture->setImage(m_image); m_nonatlas_texture->setFiltering(filtering()); } else { QSGDefaultRenderContext *rc = m_atlas->renderContext(); QRhi *rhi = m_atlas->rhi(); Q_ASSERT(rhi->isRecordingFrame()); const QRect r = atlasSubRectWithoutPadding(); QRhiTexture *extractTex = rhi->newTexture(m_atlas->texture()->format(), r.size()); if (extractTex->build()) { bool ownResUpd = false; QRhiResourceUpdateBatch *resUpd = workResourceUpdateBatch(); // ### Qt 6: should be an arg to this function if (!resUpd) { ownResUpd = true; resUpd = rhi->nextResourceUpdateBatch(); } QRhiTextureCopyDescription desc; desc.setSourceTopLeft(r.topLeft()); desc.setPixelSize(r.size()); resUpd->copyTexture(extractTex, m_atlas->texture(), desc); if (ownResUpd) rc->currentFrameCommandBuffer()->resourceUpdate(resUpd); } m_nonatlas_texture->setTexture(extractTex); m_nonatlas_texture->setOwnsTexture(true); m_nonatlas_texture->setHasAlphaChannel(m_has_alpha); m_nonatlas_texture->setTextureSize(r.size()); } } m_nonatlas_texture->setMipmapFiltering(mipmapFiltering()); m_nonatlas_texture->setFiltering(filtering()); return m_nonatlas_texture; } } QT_END_NAMESPACE #include "moc_qsgrhiatlastexture_p.cpp"