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
18QT_BEGIN_NAMESPACE
19
20int qt_sg_envInt(const char *name, int defaultValue);
21
22static QElapsedTimer qsg_renderer_timer;
23
24DEFINE_BOOL_CONFIG_OPTION(qsgEnableCompressedAtlas, QSG_ENABLE_COMPRESSED_ATLAS)
25
26namespace QSGRhiAtlasTexture
27{
28
29Manager::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
57Manager::~Manager()
58{
59 Q_ASSERT(m_atlas == nullptr);
60 Q_ASSERT(m_atlases.isEmpty());
61}
62
63void 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
80QSGTexture *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
93QSGTexture *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
118AtlasBase::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
126AtlasBase::~AtlasBase()
127{
128 Q_ASSERT(!m_texture);
129}
130
131void AtlasBase::invalidate()
132{
133 delete m_texture;
134 m_texture = nullptr;
135}
136
137void 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
153void AtlasBase::remove(TextureBase *t)
154{
155 QRect atlasRect = t->atlasSubRect();
156 m_allocator.deallocate(rect: atlasRect);
157 m_pending_uploads.removeOne(t);
158}
159
160Atlas::Atlas(QSGDefaultRenderContext *rc, const QSize &size)
161 : AtlasBase(rc, size)
162{
163 // use RGBA texture internally as that is the only one guaranteed to be always supported
164 m_format = QRhiTexture::RGBA8;
165
166 m_debug_overlay = qt_sg_envInt(name: "QSG_ATLAS_OVERLAY", defaultValue: 0);
167
168 // images smaller than this will retain their QImage.
169 // by default no images are retained (favoring memory)
170 // set to a very large value to retain all images (allowing quick removal from the atlas)
171 m_atlas_transient_image_threshold = qt_sg_envInt(name: "QSG_ATLAS_TRANSIENT_IMAGE_THRESHOLD", defaultValue: 0);
172}
173
174Atlas::~Atlas()
175{
176}
177
178Texture *Atlas::create(const QImage &image)
179{
180 // No need to lock, as manager already locked it.
181 QRect rect = m_allocator.allocate(size: QSize(image.width() + 2, image.height() + 2));
182 if (rect.width() > 0 && rect.height() > 0) {
183 Texture *t = new Texture(this, rect, image);
184 m_pending_uploads << t;
185 return t;
186 }
187 return nullptr;
188}
189
190bool Atlas::generateTexture()
191{
192 m_texture = m_rhi->newTexture(format: m_format, pixelSize: m_size, sampleCount: 1, flags: QRhiTexture::UsedAsTransferSource);
193 if (!m_texture)
194 return false;
195
196 if (!m_texture->create()) {
197 delete m_texture;
198 m_texture = nullptr;
199 return false;
200 }
201
202 return true;
203}
204
205void Atlas::enqueueTextureUpload(TextureBase *t, QRhiResourceUpdateBatch *resourceUpdates)
206{
207 Texture *tex = static_cast<Texture *>(t);
208 const QRect &r = tex->atlasSubRect();
209 QImage image = tex->image();
210
211 if (image.isNull())
212 return;
213
214 if (image.format() != QImage::Format_RGBA8888_Premultiplied)
215 image = std::move(image).convertToFormat(f: QImage::Format_RGBA8888_Premultiplied);
216
217 if (m_debug_overlay) {
218 QPainter p(&image);
219 p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
220 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)));
221 }
222
223 const int iw = image.width();
224 const int ih = image.height();
225 const int bpl = image.bytesPerLine() / 4;
226 QVarLengthArray<quint32, 1024> tmpBits(qMax(a: iw + 2, b: ih + 2));
227 const int tmpBitsSize = tmpBits.size() * 4;
228 const quint32 *src = reinterpret_cast<const quint32 *>(image.constBits());
229 quint32 *dst = tmpBits.data();
230 QVarLengthArray<QRhiTextureUploadEntry, 5> entries;
231
232 // top row, padding corners
233 dst[0] = src[0];
234 memcpy(dest: dst + 1, src: src, n: iw * sizeof(quint32));
235 dst[1 + iw] = src[iw - 1];
236 {
237 QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize);
238 subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y()));
239 subresDesc.setSourceSize(QSize(iw + 2, 1));
240 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
241 }
242
243 // bottom row, padded corners
244 const quint32 *lastRow = src + bpl * (ih - 1);
245 dst[0] = lastRow[0];
246 memcpy(dest: dst + 1, src: lastRow, n: iw * sizeof(quint32));
247 dst[1 + iw] = lastRow[iw - 1];
248 {
249 QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize);
250 subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + ih + 1));
251 subresDesc.setSourceSize(QSize(iw + 2, 1));
252 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
253 }
254
255 // left column
256 for (int i = 0; i < ih; ++i)
257 dst[i] = src[i * bpl];
258 {
259 QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize);
260 subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + 1));
261 subresDesc.setSourceSize(QSize(1, ih));
262 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
263 }
264
265
266 // right column
267 for (int i = 0; i < ih; ++i)
268 dst[i] = src[i * bpl + iw - 1];
269 {
270 QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize);
271 subresDesc.setDestinationTopLeft(QPoint(r.x() + iw + 1, r.y() + 1));
272 subresDesc.setSourceSize(QSize(1, ih));
273 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
274 }
275
276 // Inner part of the image....
277 if (bpl != iw) {
278 int sy = r.y() + 1;
279 int ey = sy + r.height() - 2;
280 entries.reserve(sz: 4 + (ey - sy));
281 for (int y = sy; y < ey; ++y) {
282 QRhiTextureSubresourceUploadDescription subresDesc(src, image.bytesPerLine());
283 subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, y));
284 subresDesc.setSourceSize(QSize(r.width() - 2, 1));
285 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
286 src += bpl;
287 }
288 } else {
289 QRhiTextureSubresourceUploadDescription subresDesc(src, image.sizeInBytes());
290 subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, r.y() + 1));
291 subresDesc.setSourceSize(QSize(r.width() - 2, r.height() - 2));
292 entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc));
293 }
294
295 QRhiTextureUploadDescription desc;
296 desc.setEntries(first: entries.cbegin(), last: entries.cend());
297 resourceUpdates->uploadTexture(tex: m_texture, desc);
298
299 const QSize textureSize = t->textureSize();
300 if (textureSize.width() > m_atlas_transient_image_threshold || textureSize.height() > m_atlas_transient_image_threshold)
301 tex->releaseImage();
302
303 qCDebug(QSG_LOG_TIME_TEXTURE, "atlastexture upload enqueued in: %lldms (%dx%d)",
304 qsg_renderer_timer.elapsed(),
305 t->textureSize().width(),
306 t->textureSize().height());
307}
308
309TextureBase::TextureBase(AtlasBase *atlas, const QRect &textureRect)
310 : QSGTexture(*(new QSGTexturePrivate(this)))
311 , m_allocated_rect(textureRect)
312 , m_atlas(atlas)
313{
314}
315
316TextureBase::~TextureBase()
317{
318 m_atlas->remove(t: this);
319}
320
321qint64 TextureBase::comparisonKey() const
322{
323 // We need special care here: a typical comparisonKey() implementation
324 // returns a unique result when there is no underlying texture yet. This is
325 // not quite ideal for atlasing however since textures with the same atlas
326 // should be considered equal regardless of the state of the underlying
327 // graphics resources.
328
329 // base the comparison on the atlas ptr; this way textures for the same
330 // atlas are considered equal
331 return qint64(m_atlas);
332}
333
334QRhiTexture *TextureBase::rhiTexture() const
335{
336 return m_atlas->m_texture;
337}
338
339void TextureBase::commitTextureOperations(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates)
340{
341#ifdef QT_NO_DEBUG
342 Q_UNUSED(rhi);
343#endif
344 Q_ASSERT(rhi == m_atlas->m_rhi);
345 m_atlas->commitTextureOperations(resourceUpdates);
346}
347
348Texture::Texture(Atlas *atlas, const QRect &textureRect, const QImage &image)
349 : TextureBase(atlas, textureRect)
350 , m_image(image)
351 , m_has_alpha(image.hasAlphaChannel())
352{
353 float w = atlas->size().width();
354 float h = atlas->size().height();
355 QRect nopad = atlasSubRectWithoutPadding();
356 m_texture_coords_rect = QRectF(nopad.x() / w,
357 nopad.y() / h,
358 nopad.width() / w,
359 nopad.height() / h);
360}
361
362Texture::~Texture()
363{
364 if (m_nonatlas_texture)
365 delete m_nonatlas_texture;
366}
367
368QSGTexture *Texture::removedFromAtlas(QRhiResourceUpdateBatch *resourceUpdates) const
369{
370 if (!m_nonatlas_texture) {
371 m_nonatlas_texture = new QSGPlainTexture;
372 if (!m_image.isNull()) {
373 m_nonatlas_texture->setImage(m_image);
374 m_nonatlas_texture->setFiltering(filtering());
375 } else {
376 QSGDefaultRenderContext *rc = m_atlas->renderContext();
377 QRhi *rhi = m_atlas->rhi();
378 Q_ASSERT(rhi->isRecordingFrame());
379 const QRect r = atlasSubRectWithoutPadding();
380
381 QRhiTexture *extractTex = rhi->newTexture(format: m_atlas->texture()->format(), pixelSize: r.size());
382 if (extractTex->create()) {
383 bool ownResUpd = false;
384 QRhiResourceUpdateBatch *resUpd = resourceUpdates;
385 if (!resUpd) {
386 ownResUpd = true;
387 resUpd = rhi->nextResourceUpdateBatch();
388 }
389 QRhiTextureCopyDescription desc;
390 desc.setSourceTopLeft(r.topLeft());
391 desc.setPixelSize(r.size());
392 resUpd->copyTexture(dst: extractTex, src: m_atlas->texture(), desc);
393 if (ownResUpd)
394 rc->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates: resUpd);
395 }
396
397 m_nonatlas_texture->setTexture(extractTex);
398 m_nonatlas_texture->setOwnsTexture(true);
399 m_nonatlas_texture->setHasAlphaChannel(m_has_alpha);
400 m_nonatlas_texture->setTextureSize(r.size());
401 }
402 }
403
404 m_nonatlas_texture->setMipmapFiltering(mipmapFiltering());
405 m_nonatlas_texture->setFiltering(filtering());
406 return m_nonatlas_texture;
407}
408
409}
410
411QT_END_NAMESPACE
412
413#include "moc_qsgrhiatlastexture_p.cpp"
414

source code of qtdeclarative/src/quick/scenegraph/util/qsgrhiatlastexture.cpp