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 | // 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 | |
174 | Atlas::~Atlas() |
175 | { |
176 | } |
177 | |
178 | Texture *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 | |
190 | bool 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 | |
205 | void 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 | |
309 | TextureBase::TextureBase(AtlasBase *atlas, const QRect &textureRect) |
310 | : QSGTexture(*(new QSGTexturePrivate(this))) |
311 | , m_allocated_rect(textureRect) |
312 | , m_atlas(atlas) |
313 | { |
314 | } |
315 | |
316 | TextureBase::~TextureBase() |
317 | { |
318 | m_atlas->remove(t: this); |
319 | } |
320 | |
321 | qint64 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 | |
334 | QRhiTexture *TextureBase::rhiTexture() const |
335 | { |
336 | return m_atlas->m_texture; |
337 | } |
338 | |
339 | void 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 | |
348 | Texture::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 | |
362 | Texture::~Texture() |
363 | { |
364 | if (m_nonatlas_texture) |
365 | delete m_nonatlas_texture; |
366 | } |
367 | |
368 | QSGTexture *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 * = 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 | |
411 | QT_END_NAMESPACE |
412 | |
413 | #include "moc_qsgrhiatlastexture_p.cpp" |
414 | |