1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2019 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQuick module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qsgrhiatlastexture_p.h" |
41 | |
42 | #include <QtCore/QVarLengthArray> |
43 | #include <QtCore/QElapsedTimer> |
44 | #include <QtCore/QtMath> |
45 | |
46 | #include <QtGui/QWindow> |
47 | |
48 | #include <private/qqmlglobal_p.h> |
49 | #include <private/qquickprofiler_p.h> |
50 | #include <private/qsgdefaultrendercontext_p.h> |
51 | #include <private/qsgtexture_p.h> |
52 | |
53 | #include <qtquick_tracepoints_p.h> |
54 | |
55 | #if 0 |
56 | #include <private/qsgcompressedtexture_p.h> |
57 | #include <private/qsgcompressedatlastexture_p.h> |
58 | #endif |
59 | |
60 | QT_BEGIN_NAMESPACE |
61 | |
62 | int qt_sg_envInt(const char *name, int defaultValue); |
63 | |
64 | static QElapsedTimer qsg_renderer_timer; |
65 | |
66 | //DEFINE_BOOL_CONFIG_OPTION(qsgEnableCompressedAtlas, QSG_ENABLE_COMPRESSED_ATLAS) |
67 | |
68 | namespace QSGRhiAtlasTexture |
69 | { |
70 | |
71 | Manager::Manager(QSGDefaultRenderContext *rc, const QSize &surfacePixelSize, QSurface *maybeSurface) |
72 | : m_rc(rc) |
73 | , m_rhi(rc->rhi()) |
74 | { |
75 | const int maxSize = m_rhi->resourceLimit(limit: QRhi::TextureSizeMax); |
76 | int w = qMin(a: maxSize, b: qt_sg_envInt(name: "QSG_ATLAS_WIDTH" , defaultValue: qMax(a: 512U, b: qNextPowerOfTwo(v: surfacePixelSize.width() - 1)))); |
77 | int h = qMin(a: maxSize, b: qt_sg_envInt(name: "QSG_ATLAS_HEIGHT" , defaultValue: qMax(a: 512U, b: qNextPowerOfTwo(v: surfacePixelSize.height() - 1)))); |
78 | |
79 | if (maybeSurface && maybeSurface->surfaceClass() == QSurface::Window) { |
80 | QWindow *window = static_cast<QWindow *>(maybeSurface); |
81 | // Coverwindows, optimize for memory rather than speed |
82 | if ((window->type() & Qt::CoverWindow) == Qt::CoverWindow) { |
83 | w /= 2; |
84 | h /= 2; |
85 | } |
86 | } |
87 | |
88 | m_atlas_size_limit = qt_sg_envInt(name: "QSG_ATLAS_SIZE_LIMIT" , defaultValue: qMax(a: w, b: h) / 2); |
89 | m_atlas_size = QSize(w, h); |
90 | |
91 | qCDebug(QSG_LOG_INFO, "rhi texture atlas dimensions: %dx%d" , w, h); |
92 | } |
93 | |
94 | Manager::~Manager() |
95 | { |
96 | Q_ASSERT(m_atlas == nullptr); |
97 | Q_ASSERT(m_atlases.isEmpty()); |
98 | } |
99 | |
100 | void Manager::invalidate() |
101 | { |
102 | if (m_atlas) { |
103 | m_atlas->invalidate(); |
104 | m_atlas->deleteLater(); |
105 | m_atlas = nullptr; |
106 | } |
107 | |
108 | #if 0 |
109 | QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::iterator i = m_atlases.begin(); |
110 | while (i != m_atlases.end()) { |
111 | i.value()->invalidate(); |
112 | i.value()->deleteLater(); |
113 | ++i; |
114 | } |
115 | m_atlases.clear(); |
116 | #endif |
117 | } |
118 | |
119 | QSGTexture *Manager::create(const QImage &image, bool hasAlphaChannel) |
120 | { |
121 | Texture *t = nullptr; |
122 | if (image.width() < m_atlas_size_limit && image.height() < m_atlas_size_limit) { |
123 | if (!m_atlas) |
124 | m_atlas = new Atlas(m_rc, m_atlas_size); |
125 | t = m_atlas->create(image); |
126 | if (t && !hasAlphaChannel && t->hasAlphaChannel()) |
127 | t->setHasAlphaChannel(false); |
128 | } |
129 | return t; |
130 | } |
131 | |
132 | QSGTexture *Manager::create(const QSGCompressedTextureFactory *factory) |
133 | { |
134 | Q_UNUSED(factory); |
135 | return nullptr; |
136 | // ### |
137 | |
138 | #if 0 |
139 | QSGTexture *t = nullptr; |
140 | if (!qsgEnableCompressedAtlas() || !factory->m_textureData.isValid()) |
141 | return t; |
142 | |
143 | // TODO: further abstract the atlas and remove this restriction |
144 | unsigned int format = factory->m_textureData.glInternalFormat(); |
145 | switch (format) { |
146 | case QOpenGLTexture::RGB8_ETC1: |
147 | case QOpenGLTexture::RGB8_ETC2: |
148 | case QOpenGLTexture::RGBA8_ETC2_EAC: |
149 | case QOpenGLTexture::RGB8_PunchThrough_Alpha1_ETC2: |
150 | break; |
151 | default: |
152 | return t; |
153 | } |
154 | |
155 | QSize size = factory->m_textureData.size(); |
156 | if (size.width() < m_atlas_size_limit && size.height() < m_atlas_size_limit) { |
157 | QHash<unsigned int, QSGCompressedAtlasTexture::Atlas*>::iterator i = m_atlases.find(format); |
158 | if (i == m_atlases.end()) |
159 | i = m_atlases.insert(format, new QSGCompressedAtlasTexture::Atlas(m_atlas_size, format)); |
160 | // must be multiple of 4 |
161 | QSize paddedSize(((size.width() + 3) / 4) * 4, ((size.height() + 3) / 4) * 4); |
162 | QByteArray data = factory->m_textureData.data(); |
163 | t = i.value()->create(data, factory->m_textureData.dataLength(), factory->m_textureData.dataOffset(), size, paddedSize); |
164 | } |
165 | #endif |
166 | } |
167 | |
168 | AtlasBase::AtlasBase(QSGDefaultRenderContext *rc, const QSize &size) |
169 | : m_rc(rc) |
170 | , m_rhi(rc->rhi()) |
171 | , m_allocator(size) |
172 | , m_size(size) |
173 | { |
174 | } |
175 | |
176 | AtlasBase::~AtlasBase() |
177 | { |
178 | Q_ASSERT(!m_texture); |
179 | } |
180 | |
181 | void AtlasBase::invalidate() |
182 | { |
183 | delete m_texture; |
184 | m_texture = nullptr; |
185 | } |
186 | |
187 | void AtlasBase::updateRhiTexture(QRhiResourceUpdateBatch *resourceUpdates) |
188 | { |
189 | if (!m_allocated) { |
190 | m_allocated = true; |
191 | if (!generateTexture()) { |
192 | qWarning(msg: "QSGTextureAtlas: Failed to create texture" ); |
193 | return; |
194 | } |
195 | } |
196 | |
197 | for (TextureBase *t : m_pending_uploads) { |
198 | // ### this profiling is all wrong, the real work is done elsewhere |
199 | bool profileFrames = QSG_LOG_TIME_TEXTURE().isDebugEnabled(); |
200 | if (profileFrames) |
201 | qsg_renderer_timer.start(); |
202 | |
203 | Q_TRACE_SCOPE(QSG_texture_prepare); |
204 | Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphTexturePrepare); |
205 | |
206 | // Skip bind, convert, swizzle; they're irrelevant |
207 | Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphTexturePrepare, |
208 | QQuickProfiler::SceneGraphTexturePrepareStart, 3); |
209 | Q_TRACE(QSG_texture_upload_entry); |
210 | |
211 | enqueueTextureUpload(t, resourceUpdates); |
212 | |
213 | Q_TRACE(QSG_texture_upload_exit); |
214 | Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphTexturePrepare, |
215 | QQuickProfiler::SceneGraphTexturePrepareUpload); |
216 | |
217 | // Skip mipmap; unused |
218 | Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphTexturePrepare, |
219 | QQuickProfiler::SceneGraphTexturePrepareUpload, 1); |
220 | Q_QUICK_SG_PROFILE_REPORT(QQuickProfiler::SceneGraphTexturePrepare, |
221 | QQuickProfiler::SceneGraphTexturePrepareMipmap); |
222 | } |
223 | |
224 | m_pending_uploads.clear(); |
225 | } |
226 | |
227 | void AtlasBase::remove(TextureBase *t) |
228 | { |
229 | QRect atlasRect = t->atlasSubRect(); |
230 | m_allocator.deallocate(rect: atlasRect); |
231 | m_pending_uploads.removeOne(t); |
232 | } |
233 | |
234 | Atlas::Atlas(QSGDefaultRenderContext *rc, const QSize &size) |
235 | : AtlasBase(rc, size) |
236 | { |
237 | // use RGBA texture internally as that is the only one guaranteed to be always supported |
238 | m_format = QRhiTexture::RGBA8; |
239 | |
240 | m_debug_overlay = qt_sg_envInt(name: "QSG_ATLAS_OVERLAY" , defaultValue: 0); |
241 | |
242 | // images smaller than this will retain their QImage. |
243 | // by default no images are retained (favoring memory) |
244 | // set to a very large value to retain all images (allowing quick removal from the atlas) |
245 | m_atlas_transient_image_threshold = qt_sg_envInt(name: "QSG_ATLAS_TRANSIENT_IMAGE_THRESHOLD" , defaultValue: 0); |
246 | } |
247 | |
248 | Atlas::~Atlas() |
249 | { |
250 | } |
251 | |
252 | Texture *Atlas::create(const QImage &image) |
253 | { |
254 | // No need to lock, as manager already locked it. |
255 | QRect rect = m_allocator.allocate(size: QSize(image.width() + 2, image.height() + 2)); |
256 | if (rect.width() > 0 && rect.height() > 0) { |
257 | Texture *t = new Texture(this, rect, image); |
258 | m_pending_uploads << t; |
259 | return t; |
260 | } |
261 | return nullptr; |
262 | } |
263 | |
264 | bool Atlas::generateTexture() |
265 | { |
266 | m_texture = m_rhi->newTexture(format: m_format, pixelSize: m_size, sampleCount: 1, flags: QRhiTexture::UsedAsTransferSource); |
267 | if (!m_texture) |
268 | return false; |
269 | |
270 | if (!m_texture->build()) { |
271 | delete m_texture; |
272 | m_texture = nullptr; |
273 | return false; |
274 | } |
275 | |
276 | return true; |
277 | } |
278 | |
279 | void Atlas::enqueueTextureUpload(TextureBase *t, QRhiResourceUpdateBatch *resourceUpdates) |
280 | { |
281 | Texture *tex = static_cast<Texture *>(t); |
282 | const QRect &r = tex->atlasSubRect(); |
283 | QImage image = tex->image(); |
284 | |
285 | if (image.isNull()) |
286 | return; |
287 | |
288 | if (image.format() != QImage::Format_RGBA8888_Premultiplied) |
289 | image = std::move(image).convertToFormat(f: QImage::Format_RGBA8888_Premultiplied); |
290 | |
291 | if (m_debug_overlay) { |
292 | QPainter p(&image); |
293 | p.setCompositionMode(QPainter::CompositionMode_SourceAtop); |
294 | 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))); |
295 | } |
296 | |
297 | const int iw = image.width(); |
298 | const int ih = image.height(); |
299 | const int bpl = image.bytesPerLine() / 4; |
300 | QVarLengthArray<quint32, 1024> tmpBits(qMax(a: iw + 2, b: ih + 2)); |
301 | const int tmpBitsSize = tmpBits.size() * 4; |
302 | const quint32 *src = reinterpret_cast<const quint32 *>(image.constBits()); |
303 | quint32 *dst = tmpBits.data(); |
304 | QVarLengthArray<QRhiTextureUploadEntry, 5> entries; |
305 | |
306 | // top row, padding corners |
307 | dst[0] = src[0]; |
308 | memcpy(dest: dst + 1, src: src, n: iw * sizeof(quint32)); |
309 | dst[1 + iw] = src[iw - 1]; |
310 | { |
311 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
312 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y())); |
313 | subresDesc.setSourceSize(QSize(iw + 2, 1)); |
314 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
315 | } |
316 | |
317 | // bottom row, padded corners |
318 | const quint32 *lastRow = src + bpl * (ih - 1); |
319 | dst[0] = lastRow[0]; |
320 | memcpy(dest: dst + 1, src: lastRow, n: iw * sizeof(quint32)); |
321 | dst[1 + iw] = lastRow[iw - 1]; |
322 | { |
323 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
324 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + ih + 1)); |
325 | subresDesc.setSourceSize(QSize(iw + 2, 1)); |
326 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
327 | } |
328 | |
329 | // left column |
330 | for (int i = 0; i < ih; ++i) |
331 | dst[i] = src[i * bpl]; |
332 | { |
333 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
334 | subresDesc.setDestinationTopLeft(QPoint(r.x(), r.y() + 1)); |
335 | subresDesc.setSourceSize(QSize(1, ih)); |
336 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
337 | } |
338 | |
339 | |
340 | // right column |
341 | for (int i = 0; i < ih; ++i) |
342 | dst[i] = src[i * bpl + iw - 1]; |
343 | { |
344 | QRhiTextureSubresourceUploadDescription subresDesc(dst, tmpBitsSize); |
345 | subresDesc.setDestinationTopLeft(QPoint(r.x() + iw + 1, r.y() + 1)); |
346 | subresDesc.setSourceSize(QSize(1, ih)); |
347 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
348 | } |
349 | |
350 | // Inner part of the image.... |
351 | if (bpl != iw) { |
352 | int sy = r.y() + 1; |
353 | int ey = sy + r.height() - 2; |
354 | entries.reserve(size: 4 + (ey - sy)); |
355 | for (int y = sy; y < ey; ++y) { |
356 | QRhiTextureSubresourceUploadDescription subresDesc(src, image.bytesPerLine()); |
357 | subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, y)); |
358 | subresDesc.setSourceSize(QSize(r.width() - 2, 1)); |
359 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
360 | src += bpl; |
361 | } |
362 | } else { |
363 | QRhiTextureSubresourceUploadDescription subresDesc(src, image.sizeInBytes()); |
364 | subresDesc.setDestinationTopLeft(QPoint(r.x() + 1, r.y() + 1)); |
365 | subresDesc.setSourceSize(QSize(r.width() - 2, r.height() - 2)); |
366 | entries.append(t: QRhiTextureUploadEntry(0, 0, subresDesc)); |
367 | } |
368 | |
369 | QRhiTextureUploadDescription desc; |
370 | desc.setEntries(first: entries.cbegin(), last: entries.cend()); |
371 | resourceUpdates->uploadTexture(tex: m_texture, desc); |
372 | |
373 | const QSize textureSize = t->textureSize(); |
374 | if (textureSize.width() > m_atlas_transient_image_threshold || textureSize.height() > m_atlas_transient_image_threshold) |
375 | tex->releaseImage(); |
376 | |
377 | qCDebug(QSG_LOG_TIME_TEXTURE, "atlastexture upload enqueued in: %lldms (%dx%d)" , |
378 | qsg_renderer_timer.elapsed(), |
379 | t->textureSize().width(), |
380 | t->textureSize().height()); |
381 | } |
382 | |
383 | TextureBase::TextureBase(AtlasBase *atlas, const QRect &textureRect) |
384 | : QSGTexture(*(new TextureBasePrivate)) |
385 | , m_allocated_rect(textureRect) |
386 | , m_atlas(atlas) |
387 | { |
388 | } |
389 | |
390 | TextureBase::~TextureBase() |
391 | { |
392 | m_atlas->remove(t: this); |
393 | } |
394 | |
395 | QRhiResourceUpdateBatch *TextureBase::workResourceUpdateBatch() const |
396 | { |
397 | Q_D(const TextureBase); |
398 | return d->workResourceUpdateBatch; |
399 | } |
400 | |
401 | int TextureBasePrivate::comparisonKey() const |
402 | { |
403 | Q_Q(const TextureBase); |
404 | |
405 | // We need special care here: a typical comparisonKey() implementation |
406 | // returns a unique result when there is no underlying texture yet. This is |
407 | // not quite ideal for atlasing however since textures with the same atlas |
408 | // should be considered equal regardless of the state of the underlying |
409 | // graphics resources. |
410 | |
411 | // base the comparison on the atlas ptr; this way textures for the same |
412 | // atlas are considered equal |
413 | return int(qintptr(q->m_atlas)); |
414 | } |
415 | |
416 | QRhiTexture *TextureBasePrivate::rhiTexture() const |
417 | { |
418 | Q_Q(const TextureBase); |
419 | return q->m_atlas->m_texture; |
420 | } |
421 | |
422 | void TextureBasePrivate::updateRhiTexture(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates) |
423 | { |
424 | Q_Q(TextureBase); |
425 | #ifdef QT_NO_DEBUG |
426 | Q_UNUSED(rhi); |
427 | #endif |
428 | Q_ASSERT(rhi == q->m_atlas->m_rhi); |
429 | q->m_atlas->updateRhiTexture(resourceUpdates); |
430 | } |
431 | |
432 | Texture::Texture(Atlas *atlas, const QRect &textureRect, const QImage &image) |
433 | : TextureBase(atlas, textureRect) |
434 | , m_image(image) |
435 | , m_has_alpha(image.hasAlphaChannel()) |
436 | { |
437 | float w = atlas->size().width(); |
438 | float h = atlas->size().height(); |
439 | QRect nopad = atlasSubRectWithoutPadding(); |
440 | m_texture_coords_rect = QRectF(nopad.x() / w, |
441 | nopad.y() / h, |
442 | nopad.width() / w, |
443 | nopad.height() / h); |
444 | } |
445 | |
446 | Texture::~Texture() |
447 | { |
448 | if (m_nonatlas_texture) |
449 | delete m_nonatlas_texture; |
450 | } |
451 | |
452 | QSGTexture *Texture::removedFromAtlas() const |
453 | { |
454 | if (!m_nonatlas_texture) { |
455 | m_nonatlas_texture = new QSGPlainTexture; |
456 | if (!m_image.isNull()) { |
457 | m_nonatlas_texture->setImage(m_image); |
458 | m_nonatlas_texture->setFiltering(filtering()); |
459 | } else { |
460 | QSGDefaultRenderContext *rc = m_atlas->renderContext(); |
461 | QRhi *rhi = m_atlas->rhi(); |
462 | Q_ASSERT(rhi->isRecordingFrame()); |
463 | const QRect r = atlasSubRectWithoutPadding(); |
464 | |
465 | QRhiTexture * = rhi->newTexture(format: m_atlas->texture()->format(), pixelSize: r.size()); |
466 | if (extractTex->build()) { |
467 | bool ownResUpd = false; |
468 | QRhiResourceUpdateBatch *resUpd = workResourceUpdateBatch(); // ### Qt 6: should be an arg to this function |
469 | if (!resUpd) { |
470 | ownResUpd = true; |
471 | resUpd = rhi->nextResourceUpdateBatch(); |
472 | } |
473 | QRhiTextureCopyDescription desc; |
474 | desc.setSourceTopLeft(r.topLeft()); |
475 | desc.setPixelSize(r.size()); |
476 | resUpd->copyTexture(dst: extractTex, src: m_atlas->texture(), desc); |
477 | if (ownResUpd) |
478 | rc->currentFrameCommandBuffer()->resourceUpdate(resourceUpdates: resUpd); |
479 | } |
480 | |
481 | m_nonatlas_texture->setTexture(extractTex); |
482 | m_nonatlas_texture->setOwnsTexture(true); |
483 | m_nonatlas_texture->setHasAlphaChannel(m_has_alpha); |
484 | m_nonatlas_texture->setTextureSize(r.size()); |
485 | } |
486 | } |
487 | |
488 | m_nonatlas_texture->setMipmapFiltering(mipmapFiltering()); |
489 | m_nonatlas_texture->setFiltering(filtering()); |
490 | return m_nonatlas_texture; |
491 | } |
492 | |
493 | } |
494 | |
495 | QT_END_NAMESPACE |
496 | |
497 | #include "moc_qsgrhiatlastexture_p.cpp" |
498 | |