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