Revision control
Copy as Markdown
// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qsgrhiatlastexture_p.h"
#include <QtCore/QVarLengthArray>
#include <QtCore/QElapsedTimer>
#include <QtCore/QtMath>
#include <QtGui/QWindow>
#include <private/qqmlglobal_p.h>
#include <private/qsgdefaultrendercontext_p.h>
#include <private/qsgtexture_p.h>
#include <private/qsgcompressedtexture_p.h>
#include <private/qsgcompressedatlastexture_p.h>
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);
// surfacePixelSize is just a hint that was passed in when initializing the
// rendercontext, likely based on the window size, if it was available,
// that is. Therefore, it may be anything, incl. zero and negative.
const int widthHint = qMax(1, surfacePixelSize.width());
const int heightHint = qMax(1, surfacePixelSize.height());
int w = qMin(maxSize, qt_sg_envInt("QSG_ATLAS_WIDTH", qMax(512U, qNextPowerOfTwo(widthHint - 1))));
int h = qMin(maxSize, qt_sg_envInt("QSG_ATLAS_HEIGHT", qMax(512U, qNextPowerOfTwo(heightHint - 1))));
if (maybeSurface && maybeSurface->surfaceClass() == QSurface::Window) {
QWindow *window = static_cast<QWindow *>(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;
}
QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::iterator i = m_atlases.begin();
while (i != m_atlases.end()) {
i.value()->invalidate();
i.value()->deleteLater();
++i;
}
m_atlases.clear();
}
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)
{
QSGTexture *t = nullptr;
if (!qsgEnableCompressedAtlas() || !factory->textureData()->isValid())
return t;
unsigned int format = factory->textureData()->glInternalFormat();
QSGCompressedTexture::FormatInfo fmt = QSGCompressedTexture::formatInfo(format);
if (!m_rhi->isTextureFormatSupported(fmt.rhiFormat))
return t;
QSize size = factory->textureData()->size();
if (size.width() < m_atlas_size_limit && size.height() < m_atlas_size_limit) {
QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::const_iterator i = m_atlases.constFind(format);
if (i == m_atlases.cend()) {
auto newAtlas = new QSGCompressedAtlasTexture::Atlas(m_rc, m_atlas_size, format);
i = m_atlases.insert(format, newAtlas);
}
const QTextureFileData *cmpData = factory->textureData();
t = i.value()->create(cmpData->getDataView(), size);
}
return t;
}
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::commitTextureOperations(QRhiResourceUpdateBatch *resourceUpdates)
{
if (!m_allocated) {
m_allocated = true;
if (!generateTexture()) {
qWarning("QSGTextureAtlas: Failed to create texture");
return;
}
}
for (TextureBase *t : std::as_const(m_pending_uploads))
enqueueTextureUpload(t, resourceUpdates);
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)
{
m_format = QRhiTexture::RGBA8;
// Mirror QSGPlainTexture by playing nice with ARGB32[_Pre], because due to
// legacy that's what most images come in, not the byte-ordered
// RGBA8888[_Pre]. (i.e. with this the behavior matches 5.15) However,
// QSGPlainTexture can make a separate decision for each image (texture),
// the atlas cannot, so the downside is that now images that come in the
// modern byte-ordered formats need a conversion. So perhaps reconsider this
// at some point in the future.
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
if (rc->rhi()->isTextureFormatSupported(QRhiTexture::BGRA8))
m_format = QRhiTexture::BGRA8;
#endif
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->create()) {
delete m_texture;
m_texture = nullptr;
return false;
}
return true;
}
void Atlas::enqueueTextureUpload(TextureBase *t, QRhiResourceUpdateBatch *resourceUpdates)
{
Texture *tex = static_cast<Texture *>(t);
const QRect &r = tex->atlasSubRect();
QImage image = tex->image();
if (image.isNull())
return;
if (m_format == QRhiTexture::BGRA8) {
if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32_Premultiplied)
image = std::move(image).convertToFormat(QImage::Format_ARGB32_Premultiplied);
} else 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<quint32, 1024> tmpBits(qMax(iw + 2, ih + 2));
const int tmpBitsSize = tmpBits.size() * 4;
const quint32 *src = reinterpret_cast<const quint32 *>(image.constBits());
quint32 *dst = tmpBits.data();
QVarLengthArray<QRhiTextureUploadEntry, 5> 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 QSGTexturePrivate(this)))
, m_allocated_rect(textureRect)
, m_atlas(atlas)
{
}
TextureBase::~TextureBase()
{
m_atlas->remove(this);
}
qint64 TextureBase::comparisonKey() const
{
// 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 qint64(m_atlas);
}
QRhiTexture *TextureBase::rhiTexture() const
{
return m_atlas->m_texture;
}
void TextureBase::commitTextureOperations(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates)
{
#ifdef QT_NO_DEBUG
Q_UNUSED(rhi);
#endif
Q_ASSERT(rhi == m_atlas->m_rhi);
m_atlas->commitTextureOperations(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(QRhiResourceUpdateBatch *resourceUpdates) 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->create()) {
bool ownResUpd = false;
QRhiResourceUpdateBatch *resUpd = resourceUpdates;
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"