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 "qsgrhidistancefieldglyphcache_p.h"
41#include "qsgcontext_p.h"
42#include <QtGui/private/qdistancefield_p.h>
43#include <QtCore/qelapsedtimer.h>
44#include <QtQml/private/qqmlglobal_p.h>
45#include <qmath.h>
46#include <qendian.h>
47
48QT_BEGIN_NAMESPACE
49
50DEFINE_BOOL_CONFIG_OPTION(qmlUseGlyphCacheWorkaround, QML_USE_GLYPHCACHE_WORKAROUND)
51DEFINE_BOOL_CONFIG_OPTION(qsgPreferFullSizeGlyphCacheTextures, QSG_PREFER_FULLSIZE_GLYPHCACHE_TEXTURES)
52
53#if !defined(QSG_RHI_DISTANCEFIELD_GLYPH_CACHE_PADDING)
54# define QSG_RHI_DISTANCEFIELD_GLYPH_CACHE_PADDING 2
55#endif
56
57QSGRhiDistanceFieldGlyphCache::QSGRhiDistanceFieldGlyphCache(QRhi *rhi, const QRawFont &font)
58 : QSGDistanceFieldGlyphCache(font)
59 , m_rhi(rhi)
60{
61 // Load a pregenerated cache if the font contains one
62 loadPregeneratedCache(font);
63}
64
65QSGRhiDistanceFieldGlyphCache::~QSGRhiDistanceFieldGlyphCache()
66{
67 for (int i = 0; i < m_textures.count(); ++i)
68 delete m_textures[i].texture;
69
70 delete m_areaAllocator;
71
72 // should be empty, but just in case
73 qDeleteAll(m_pendingDispose);
74}
75
76void QSGRhiDistanceFieldGlyphCache::requestGlyphs(const QSet<glyph_t> &glyphs)
77{
78 QList<GlyphPosition> glyphPositions;
79 QVector<glyph_t> glyphsToRender;
80
81 if (m_areaAllocator == nullptr)
82 m_areaAllocator = new QSGAreaAllocator(QSize(maxTextureSize(), m_maxTextureCount * maxTextureSize()));
83
84 for (QSet<glyph_t>::const_iterator it = glyphs.constBegin(); it != glyphs.constEnd() ; ++it) {
85 glyph_t glyphIndex = *it;
86
87 int padding = QSG_RHI_DISTANCEFIELD_GLYPH_CACHE_PADDING;
88 QRectF boundingRect = glyphData(glyphIndex).boundingRect;
89 int glyphWidth = qCeil(boundingRect.width()) + distanceFieldRadius() * 2;
90 int glyphHeight = qCeil(boundingRect.height()) + distanceFieldRadius() * 2;
91 QSize glyphSize(glyphWidth + padding * 2, glyphHeight + padding * 2);
92 QRect alloc = m_areaAllocator->allocate(glyphSize);
93
94 if (alloc.isNull()) {
95 // Unallocate unused glyphs until we can allocated the new glyph
96 while (alloc.isNull() && !m_unusedGlyphs.isEmpty()) {
97 glyph_t unusedGlyph = *m_unusedGlyphs.constBegin();
98
99 TexCoord unusedCoord = glyphTexCoord(unusedGlyph);
100 QRectF unusedGlyphBoundingRect = glyphData(unusedGlyph).boundingRect;
101 int unusedGlyphWidth = qCeil(unusedGlyphBoundingRect.width()) + distanceFieldRadius() * 2;
102 int unusedGlyphHeight = qCeil(unusedGlyphBoundingRect.height()) + distanceFieldRadius() * 2;
103 m_areaAllocator->deallocate(QRect(unusedCoord.x - padding,
104 unusedCoord.y - padding,
105 padding * 2 + unusedGlyphWidth,
106 padding * 2 + unusedGlyphHeight));
107
108 m_unusedGlyphs.remove(unusedGlyph);
109 m_glyphsTexture.remove(unusedGlyph);
110 removeGlyph(unusedGlyph);
111
112 alloc = m_areaAllocator->allocate(glyphSize);
113 }
114
115 // Not enough space left for this glyph... skip to the next one
116 if (alloc.isNull())
117 continue;
118 }
119
120 TextureInfo *tex = textureInfo(alloc.y() / maxTextureSize());
121 alloc = QRect(alloc.x(), alloc.y() % maxTextureSize(), alloc.width(), alloc.height());
122
123 tex->allocatedArea |= alloc;
124 Q_ASSERT(tex->padding == padding || tex->padding < 0);
125 tex->padding = padding;
126
127 GlyphPosition p;
128 p.glyph = glyphIndex;
129 p.position = alloc.topLeft() + QPoint(padding, padding);
130
131 glyphPositions.append(p);
132 glyphsToRender.append(glyphIndex);
133 m_glyphsTexture.insert(glyphIndex, tex);
134 }
135
136 setGlyphsPosition(glyphPositions);
137 markGlyphsToRender(glyphsToRender);
138}
139
140void QSGRhiDistanceFieldGlyphCache::storeGlyphs(const QList<QDistanceField> &glyphs)
141{
142 typedef QHash<TextureInfo *, QVector<glyph_t> > GlyphTextureHash;
143 typedef GlyphTextureHash::const_iterator GlyphTextureHashConstIt;
144
145 GlyphTextureHash glyphTextures;
146
147 QVarLengthArray<QRhiTextureUploadEntry, 32> uploads;
148 for (int i = 0; i < glyphs.size(); ++i) {
149 QDistanceField glyph = glyphs.at(i);
150 glyph_t glyphIndex = glyph.glyph();
151 TexCoord c = glyphTexCoord(glyphIndex);
152 TextureInfo *texInfo = m_glyphsTexture.value(glyphIndex);
153
154 resizeTexture(texInfo, texInfo->allocatedArea.width(), texInfo->allocatedArea.height());
155
156 glyphTextures[texInfo].append(glyphIndex);
157
158 int padding = texInfo->padding;
159 int expectedWidth = qCeil(c.width + c.xMargin * 2);
160 glyph = glyph.copy(-padding, -padding,
161 expectedWidth + padding * 2, glyph.height() + padding * 2);
162
163 if (useTextureResizeWorkaround()) {
164 uchar *inBits = glyph.scanLine(0);
165 uchar *outBits = texInfo->image.scanLine(int(c.y) - padding) + int(c.x) - padding;
166 for (int y = 0; y < glyph.height(); ++y) {
167 memcpy(outBits, inBits, glyph.width());
168 inBits += glyph.width();
169 outBits += texInfo->image.width();
170 }
171 }
172
173 QRhiTextureSubresourceUploadDescription subresDesc(glyph.constBits(), glyph.width() * glyph.height());
174 subresDesc.setSourceSize(QSize(glyph.width(), glyph.height()));
175 subresDesc.setDestinationTopLeft(QPoint(c.x - padding, c.y - padding));
176 texInfo->uploads.append(QRhiTextureUploadEntry(0, 0, subresDesc));
177 }
178
179 if (!m_resourceUpdates)
180 m_resourceUpdates = m_rhi->nextResourceUpdateBatch();
181
182 for (int i = 0; i < glyphs.size(); ++i) {
183 TextureInfo *texInfo = m_glyphsTexture.value(glyphs.at(i).glyph());
184 if (!texInfo->uploads.isEmpty()) {
185 m_resourceUpdates->uploadTexture(texInfo->texture, texInfo->uploads);
186 texInfo->uploads.clear();
187 }
188 }
189
190 for (GlyphTextureHashConstIt i = glyphTextures.constBegin(), cend = glyphTextures.constEnd(); i != cend; ++i) {
191 Texture t;
192 t.texture = i.key()->texture;
193 t.size = i.key()->size;
194 t.rhiBased = true;
195 setGlyphsTexture(i.value(), t);
196 }
197}
198
199void QSGRhiDistanceFieldGlyphCache::referenceGlyphs(const QSet<glyph_t> &glyphs)
200{
201 m_unusedGlyphs -= glyphs;
202}
203
204void QSGRhiDistanceFieldGlyphCache::releaseGlyphs(const QSet<glyph_t> &glyphs)
205{
206 m_unusedGlyphs += glyphs;
207}
208
209void QSGRhiDistanceFieldGlyphCache::createTexture(TextureInfo *texInfo,
210 int width,
211 int height)
212{
213 QByteArray zeroBuf(width * height, 0);
214 createTexture(texInfo, width, height, zeroBuf.constData());
215}
216
217void QSGRhiDistanceFieldGlyphCache::createTexture(TextureInfo *texInfo,
218 int width,
219 int height,
220 const void *pixels)
221{
222 if (useTextureResizeWorkaround() && texInfo->image.isNull()) {
223 texInfo->image = QDistanceField(width, height);
224 memcpy(texInfo->image.bits(), pixels, width * height);
225 }
226
227 texInfo->texture = m_rhi->newTexture(QRhiTexture::RED_OR_ALPHA8, QSize(width, height), 1, QRhiTexture::UsedAsTransferSource);
228 if (texInfo->texture->build()) {
229 if (!m_resourceUpdates)
230 m_resourceUpdates = m_rhi->nextResourceUpdateBatch();
231
232 QRhiTextureSubresourceUploadDescription subresDesc(pixels, width * height);
233 subresDesc.setSourceSize(QSize(width, height));
234 m_resourceUpdates->uploadTexture(texInfo->texture, QRhiTextureUploadEntry(0, 0, subresDesc));
235 } else {
236 qWarning("Failed to create distance field glyph cache");
237 }
238
239 texInfo->size = QSize(width, height);
240}
241
242void QSGRhiDistanceFieldGlyphCache::resizeTexture(TextureInfo *texInfo, int width, int height)
243{
244 int oldWidth = texInfo->size.width();
245 int oldHeight = texInfo->size.height();
246 if (width == oldWidth && height == oldHeight)
247 return;
248
249 QRhiTexture *oldTexture = texInfo->texture;
250 createTexture(texInfo, width, height);
251
252 if (!oldTexture)
253 return;
254
255 updateRhiTexture(oldTexture, texInfo->texture, texInfo->size);
256
257 if (!m_resourceUpdates)
258 m_resourceUpdates = m_rhi->nextResourceUpdateBatch();
259
260 if (useTextureResizeWorkaround()) {
261 QRhiTextureSubresourceUploadDescription subresDesc(texInfo->image.constBits(),
262 oldWidth * oldHeight);
263 subresDesc.setSourceSize(QSize(oldWidth, oldHeight));
264 m_resourceUpdates->uploadTexture(texInfo->texture, QRhiTextureUploadEntry(0, 0, subresDesc));
265 texInfo->image = texInfo->image.copy(0, 0, width, height);
266 } else {
267 m_resourceUpdates->copyTexture(texInfo->texture, oldTexture);
268 }
269
270 m_pendingDispose.insert(oldTexture);
271}
272
273bool QSGRhiDistanceFieldGlyphCache::useTextureResizeWorkaround() const
274{
275 static bool set = false;
276 static bool useWorkaround = false;
277 if (!set) {
278 useWorkaround = m_rhi->backend() == QRhi::OpenGLES2 || qmlUseGlyphCacheWorkaround();
279 set = true;
280 }
281 return useWorkaround;
282}
283
284bool QSGRhiDistanceFieldGlyphCache::createFullSizeTextures() const
285{
286 return qsgPreferFullSizeGlyphCacheTextures() && glyphCount() > QT_DISTANCEFIELD_HIGHGLYPHCOUNT();
287}
288
289int QSGRhiDistanceFieldGlyphCache::maxTextureSize() const
290{
291 if (!m_maxTextureSize)
292 m_maxTextureSize = m_rhi->resourceLimit(QRhi::TextureSizeMax);
293 return m_maxTextureSize;
294}
295
296namespace {
297 struct Qtdf {
298 // We need these structs to be tightly packed, but some compilers we use do not
299 // support #pragma pack(1), so we need to hardcode the offsets/sizes in the
300 // file format
301 enum TableSize {
302 HeaderSize = 14,
303 GlyphRecordSize = 46,
304 TextureRecordSize = 17
305 };
306
307 enum Offset {
308 // Header
309 majorVersion = 0,
310 minorVersion = 1,
311 pixelSize = 2,
312 textureSize = 4,
313 flags = 8,
314 headerPadding = 9,
315 numGlyphs = 10,
316
317 // Glyph record
318 glyphIndex = 0,
319 textureOffsetX = 4,
320 textureOffsetY = 8,
321 textureWidth = 12,
322 textureHeight = 16,
323 xMargin = 20,
324 yMargin = 24,
325 boundingRectX = 28,
326 boundingRectY = 32,
327 boundingRectWidth = 36,
328 boundingRectHeight = 40,
329 textureIndex = 44,
330
331 // Texture record
332 allocatedX = 0,
333 allocatedY = 4,
334 allocatedWidth = 8,
335 allocatedHeight = 12,
336 texturePadding = 16
337
338 };
339
340 template <typename T>
341 static inline T fetch(const char *data, Offset offset)
342 {
343 return qFromBigEndian<T>(data + int(offset));
344 }
345 };
346}
347
348bool QSGRhiDistanceFieldGlyphCache::loadPregeneratedCache(const QRawFont &font)
349{
350 // The pregenerated data must be loaded first, otherwise the area allocator
351 // will be wrong
352 if (m_areaAllocator != nullptr) {
353 qWarning("Font cache must be loaded before cache is used");
354 return false;
355 }
356
357 static QElapsedTimer timer;
358
359 bool profile = QSG_LOG_TIME_GLYPH().isDebugEnabled();
360 if (profile)
361 timer.start();
362
363 QByteArray qtdfTable = font.fontTable("qtdf");
364 if (qtdfTable.isEmpty())
365 return false;
366
367 typedef QHash<TextureInfo *, QVector<glyph_t> > GlyphTextureHash;
368
369 GlyphTextureHash glyphTextures;
370
371 if (uint(qtdfTable.size()) < Qtdf::HeaderSize) {
372 qWarning("Invalid qtdf table in font '%s'",
373 qPrintable(font.familyName()));
374 return false;
375 }
376
377 const char *qtdfTableStart = qtdfTable.constData();
378 const char *qtdfTableEnd = qtdfTableStart + qtdfTable.size();
379
380 int padding = 0;
381 int textureCount = 0;
382 {
383 quint8 majorVersion = Qtdf::fetch<quint8>(qtdfTableStart, Qtdf::majorVersion);
384 quint8 minorVersion = Qtdf::fetch<quint8>(qtdfTableStart, Qtdf::minorVersion);
385 if (majorVersion != 5 || minorVersion != 12) {
386 qWarning("Invalid version of qtdf table %d.%d in font '%s'",
387 majorVersion,
388 minorVersion,
389 qPrintable(font.familyName()));
390 return false;
391 }
392
393 qreal pixelSize = qreal(Qtdf::fetch<quint16>(qtdfTableStart, Qtdf::pixelSize));
394 m_maxTextureSize = Qtdf::fetch<quint32>(qtdfTableStart, Qtdf::textureSize);
395 m_doubleGlyphResolution = Qtdf::fetch<quint8>(qtdfTableStart, Qtdf::flags) == 1;
396 padding = Qtdf::fetch<quint8>(qtdfTableStart, Qtdf::headerPadding);
397
398 if (pixelSize <= 0.0) {
399 qWarning("Invalid pixel size in '%s'", qPrintable(font.familyName()));
400 return false;
401 }
402
403 if (m_maxTextureSize <= 0) {
404 qWarning("Invalid texture size in '%s'", qPrintable(font.familyName()));
405 return false;
406 }
407
408 int systemMaxTextureSize = m_rhi->resourceLimit(QRhi::TextureSizeMax);
409
410 if (m_maxTextureSize > systemMaxTextureSize) {
411 qWarning("System maximum texture size is %d. This is lower than the value in '%s', which is %d",
412 systemMaxTextureSize,
413 qPrintable(font.familyName()),
414 m_maxTextureSize);
415 }
416
417 if (padding != QSG_RHI_DISTANCEFIELD_GLYPH_CACHE_PADDING) {
418 qWarning("Padding mismatch in '%s'. Font requires %d, but Qt is compiled with %d.",
419 qPrintable(font.familyName()),
420 padding,
421 QSG_RHI_DISTANCEFIELD_GLYPH_CACHE_PADDING);
422 }
423
424 m_referenceFont.setPixelSize(pixelSize);
425
426 quint32 glyphCount = Qtdf::fetch<quint32>(qtdfTableStart, Qtdf::numGlyphs);
427 m_unusedGlyphs.reserve(glyphCount);
428
429 const char *allocatorData = qtdfTableStart + Qtdf::HeaderSize;
430 {
431 m_areaAllocator = new QSGAreaAllocator(QSize(0, 0));
432 allocatorData = m_areaAllocator->deserialize(allocatorData, qtdfTableEnd - allocatorData);
433 if (allocatorData == nullptr)
434 return false;
435 }
436
437 if (m_areaAllocator->size().height() % m_maxTextureSize != 0) {
438 qWarning("Area allocator size mismatch in '%s'", qPrintable(font.familyName()));
439 return false;
440 }
441
442 textureCount = m_areaAllocator->size().height() / m_maxTextureSize;
443 m_maxTextureCount = qMax(m_maxTextureCount, textureCount);
444
445 const char *textureRecord = allocatorData;
446 for (int i = 0; i < textureCount; ++i, textureRecord += Qtdf::TextureRecordSize) {
447 if (textureRecord + Qtdf::TextureRecordSize > qtdfTableEnd) {
448 qWarning("qtdf table too small in font '%s'.",
449 qPrintable(font.familyName()));
450 return false;
451 }
452
453 TextureInfo *tex = textureInfo(i);
454 tex->allocatedArea.setX(Qtdf::fetch<quint32>(textureRecord, Qtdf::allocatedX));
455 tex->allocatedArea.setY(Qtdf::fetch<quint32>(textureRecord, Qtdf::allocatedY));
456 tex->allocatedArea.setWidth(Qtdf::fetch<quint32>(textureRecord, Qtdf::allocatedWidth));
457 tex->allocatedArea.setHeight(Qtdf::fetch<quint32>(textureRecord, Qtdf::allocatedHeight));
458 tex->padding = Qtdf::fetch<quint8>(textureRecord, Qtdf::texturePadding);
459 }
460
461 const char *glyphRecord = textureRecord;
462 for (quint32 i = 0; i < glyphCount; ++i, glyphRecord += Qtdf::GlyphRecordSize) {
463 if (glyphRecord + Qtdf::GlyphRecordSize > qtdfTableEnd) {
464 qWarning("qtdf table too small in font '%s'.",
465 qPrintable(font.familyName()));
466 return false;
467 }
468
469 glyph_t glyph = Qtdf::fetch<quint32>(glyphRecord, Qtdf::glyphIndex);
470 m_unusedGlyphs.insert(glyph);
471
472 GlyphData &glyphData = emptyData(glyph);
473
474#define FROM_FIXED_POINT(value) \
475(((qreal)value)/(qreal)65536)
476
477 glyphData.texCoord.x = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::textureOffsetX));
478 glyphData.texCoord.y = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::textureOffsetY));
479 glyphData.texCoord.width = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::textureWidth));
480 glyphData.texCoord.height = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::textureHeight));
481 glyphData.texCoord.xMargin = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::xMargin));
482 glyphData.texCoord.yMargin = FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::yMargin));
483 glyphData.boundingRect.setX(FROM_FIXED_POINT(Qtdf::fetch<qint32>(glyphRecord, Qtdf::boundingRectX)));
484 glyphData.boundingRect.setY(FROM_FIXED_POINT(Qtdf::fetch<qint32>(glyphRecord, Qtdf::boundingRectY)));
485 glyphData.boundingRect.setWidth(FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::boundingRectWidth)));
486 glyphData.boundingRect.setHeight(FROM_FIXED_POINT(Qtdf::fetch<quint32>(glyphRecord, Qtdf::boundingRectHeight)));
487
488#undef FROM_FIXED_POINT
489
490 int textureIndex = Qtdf::fetch<quint16>(glyphRecord, Qtdf::textureIndex);
491 if (textureIndex < 0 || textureIndex >= textureCount) {
492 qWarning("Invalid texture index %d (texture count == %d) in '%s'",
493 textureIndex,
494 textureCount,
495 qPrintable(font.familyName()));
496 return false;
497 }
498
499
500 TextureInfo *texInfo = textureInfo(textureIndex);
501 m_glyphsTexture.insert(glyph, texInfo);
502
503 glyphTextures[texInfo].append(glyph);
504 }
505
506 const uchar *textureData = reinterpret_cast<const uchar *>(glyphRecord);
507 for (int i = 0; i < textureCount; ++i) {
508
509 TextureInfo *texInfo = textureInfo(i);
510
511 int width = texInfo->allocatedArea.width();
512 int height = texInfo->allocatedArea.height();
513 qint64 size = width * height;
514 if (reinterpret_cast<const char *>(textureData + size) > qtdfTableEnd) {
515 qWarning("qtdf table too small in font '%s'.",
516 qPrintable(font.familyName()));
517 return false;
518 }
519
520 createTexture(texInfo, width, height, textureData);
521
522 QVector<glyph_t> glyphs = glyphTextures.value(texInfo);
523
524 Texture t;
525 t.texture = texInfo->texture;
526 t.size = texInfo->size;
527 t.rhiBased = true;
528
529 setGlyphsTexture(glyphs, t);
530
531 textureData += size;
532 }
533 }
534
535 if (profile) {
536 quint64 now = timer.elapsed();
537 qCDebug(QSG_LOG_TIME_GLYPH,
538 "distancefield: %d pre-generated glyphs loaded in %dms",
539 m_unusedGlyphs.size(),
540 (int) now);
541 }
542
543 return true;
544}
545
546void QSGRhiDistanceFieldGlyphCache::commitResourceUpdates(QRhiResourceUpdateBatch *mergeInto)
547{
548 if (m_resourceUpdates) {
549 mergeInto->merge(m_resourceUpdates);
550 m_resourceUpdates->release();
551 m_resourceUpdates = nullptr;
552 }
553
554 // now let's assume the resource updates will be committed in this frame
555 for (QRhiTexture *t : m_pendingDispose)
556 t->releaseAndDestroyLater(); // will be releaseAndDestroyed after the frame is submitted -> safe
557
558 m_pendingDispose.clear();
559}
560
561bool QSGRhiDistanceFieldGlyphCache::eightBitFormatIsAlphaSwizzled() const
562{
563 // return true when the shaders for 8-bit formats need .a instead of .r
564 // when sampling the texture
565 return !m_rhi->isFeatureSupported(QRhi::RedOrAlpha8IsRed);
566}
567
568QT_END_NAMESPACE
569