Add caching for the QTextLayout objects we use

The QTextLayout handling is terribly slow on Qt 4.8.7, but some
caching has been added in Qt5 that makes it much faster. For some
reason, it is not that slow with Qt 4.8.1.

Caches are introduced for the three following methods

* width(doctring), controlled by CACHE_METRICS_WIDTH. This cache already
  existed, but the code has been cleaned up

* getTextLayout, controlled by CACHE_METRICS_QTEXTLAYOUT (disabled by
  default on Qt5, which does its own caching). This is used for pos2x
  and x2pos and now for drawing of text too. The previous code used a
  trivial caching scheme of the last used QTextLayout, but now they
  are properly kept in a QCache. Moreover, the cacheEnabled() property
  is enabled for these QTextLayout object (not sure what this does).

* breakAt, controlled by CACHE_METRICS_BREAKAT. This is the only user
  of QTextLayout which did not have some kind of caching already.

For some weird reasons related to Argument-dependent look-up, the
qHash(docstring) function has to be defined in std namespace, since
lyx::docstring is actually std::basic_string<wchar_t>.

(cherry picked from c5119c97fc)
This commit is contained in:
Jean-Marc Lasgouttes 2016-07-05 14:06:22 +02:00
parent 54fa0d51da
commit d76309913f
5 changed files with 172 additions and 60 deletions

View File

@ -21,11 +21,33 @@
#include "insets/Inset.h" #include "insets/Inset.h"
#include "support/convert.h"
#include "support/lassert.h" #include "support/lassert.h"
#ifdef CACHE_SOME_METRICS
#include <QByteArray>
#endif
using namespace std; using namespace std;
using namespace lyx::support; using namespace lyx::support;
#ifdef CACHE_SOME_METRICS
namespace std {
/*
* Argument-dependent lookup implies that this function shall be
* declared in the namespace of its argument. But this is std
* namespace, since lyx::docstring is just std::basic_string<wchar_t>.
*/
uint qHash(lyx::docstring const & s)
{
return qHash(QByteArray(reinterpret_cast<char const *>(s.data()),
s.size() * sizeof(lyx::docstring::value_type)));
}
}
#endif
namespace lyx { namespace lyx {
namespace frontend { namespace frontend {
@ -51,11 +73,23 @@ inline QChar const ucs4_to_qchar(char_type const ucs4)
} // anon namespace } // anon namespace
// Limit strwidth_cache_ size to 512kB of string data /*
* Limit (strwidth|breakat)_cache_ size to 512kB of string data.
* Limit qtextlayout_cache_ size to 500 elements (we do not know the
* size of the QTextLayout objects anyway).
* Note that all these numbers are arbitrary.
*/
GuiFontMetrics::GuiFontMetrics(QFont const & font) GuiFontMetrics::GuiFontMetrics(QFont const & font)
: font_(font), metrics_(font, 0), : font_(font), metrics_(font, 0)
strwidth_cache_(1 << 19), #ifdef CACHE_METRICS_WIDTH
tl_cache_rtl_(false), tl_cache_wordspacing_(-1.0) , strwidth_cache_(1 << 19)
#endif
#ifdef CACHE_METRICS_BREAKAT
, breakat_cache_(1 << 19)
#endif
#ifdef CACHE_METRICS_QTEXTLAYOUT
, qtextlayout_cache_(500)
#endif
{ {
} }
@ -140,14 +174,13 @@ int GuiFontMetrics::rbearing(char_type c) const
int GuiFontMetrics::width(docstring const & s) const int GuiFontMetrics::width(docstring const & s) const
{ {
QByteArray qba = #ifdef CACHE_METRICS_WIDTH
QByteArray(reinterpret_cast<char const *>(s.data()), int * pw = strwidth_cache_[s];
s.size() * sizeof(docstring::value_type));
int * pw = strwidth_cache_[qba];
if (pw) if (pw)
return *pw; return *pw;
// For some reason QMetrics::width returns a wrong value with Qt5 // For some reason QMetrics::width returns a wrong value with Qt5
// int w = metrics_.width(toqstr(s)); // int w = metrics_.width(toqstr(s));
#endif
QTextLayout tl; QTextLayout tl;
tl.setText(toqstr(s)); tl.setText(toqstr(s));
tl.setFont(font_); tl.setFont(font_);
@ -155,8 +188,9 @@ int GuiFontMetrics::width(docstring const & s) const
QTextLine line = tl.createLine(); QTextLine line = tl.createLine();
tl.endLayout(); tl.endLayout();
int w = int(line.naturalTextWidth()); int w = int(line.naturalTextWidth());
#ifdef CACHE_METRICS_WIDTH
strwidth_cache_.insert(qba, new int(w), qba.size()); strwidth_cache_.insert(s, new int(w), s.size() * sizeof(char_type));
#endif
return w; return w;
} }
@ -179,58 +213,66 @@ int GuiFontMetrics::signedWidth(docstring const & s) const
} }
QTextLayout const & QTextLayout const *
GuiFontMetrics::getTextLayout(docstring const & s, QFont font, GuiFontMetrics::getTextLayout(docstring const & s, bool const rtl,
bool const rtl, double const wordspacing) const double const wordspacing) const
{ {
if (s != tl_cache_s_ || font != tl_cache_font_ || rtl != tl_cache_rtl_ QTextLayout * ptl;
|| wordspacing != tl_cache_wordspacing_) { #ifdef CACHE_METRICS_QTEXTLAYOUT
tl_cache_.setText(toqstr(s)); docstring const s_cache = s + (rtl ? "r" : "l") + convert<docstring>(wordspacing);
font.setWordSpacing(wordspacing); ptl = qtextlayout_cache_[s_cache];
tl_cache_.setFont(font); if (!ptl) {
#endif
ptl = new QTextLayout();
ptl->setCacheEnabled(true);
ptl->setText(toqstr(s));
QFont copy = font_;
copy.setWordSpacing(wordspacing);
ptl->setFont(copy);
// Note that both setFlags and the enums are undocumented // Note that both setFlags and the enums are undocumented
tl_cache_.setFlags(rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight); ptl->setFlags(rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight);
tl_cache_.beginLayout(); ptl->beginLayout();
tl_cache_.createLine(); ptl->createLine();
tl_cache_.endLayout(); ptl->endLayout();
tl_cache_s_ = s; #ifdef CACHE_METRICS_QTEXTLAYOUT
tl_cache_font_ = font; qtextlayout_cache_.insert(s_cache, ptl);
tl_cache_rtl_ = rtl;
tl_cache_wordspacing_ = wordspacing;
} }
return tl_cache_; #endif
return ptl;
} }
int GuiFontMetrics::pos2x(docstring const & s, int const pos, bool const rtl, int GuiFontMetrics::pos2x(docstring const & s, int pos, bool const rtl,
double const wordspacing) const double const wordspacing) const
{ {
QTextLayout const & tl = getTextLayout(s, font_, rtl, wordspacing); if (pos <= 0)
pos = 0;
QTextLayout const * tl = getTextLayout(s, rtl, wordspacing);
/* Since QString is UTF-16 and docstring is UCS-4, the offsets may /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
* not be the same when there are high-plan unicode characters * not be the same when there are high-plan unicode characters
* (bug #10443). * (bug #10443).
*/ */
int const qpos = toqstr(s.substr(0, pos)).length(); int const qpos = toqstr(s.substr(0, pos)).length();
return static_cast<int>(tl.lineForTextPosition(qpos).cursorToX(qpos)); return static_cast<int>(tl->lineForTextPosition(qpos).cursorToX(qpos));
} }
int GuiFontMetrics::x2pos(docstring const & s, int & x, bool const rtl, int GuiFontMetrics::x2pos(docstring const & s, int & x, bool const rtl,
double const wordspacing) const double const wordspacing) const
{ {
QTextLayout const & tl = getTextLayout(s, font_, rtl, wordspacing); QTextLayout const * tl = getTextLayout(s, rtl, wordspacing);
int const qpos = tl.lineForTextPosition(0).xToCursor(x); int const qpos = tl->lineForTextPosition(0).xToCursor(x);
// correct x value to the actual cursor position. // correct x value to the actual cursor position.
x = static_cast<int>(tl.lineForTextPosition(0).cursorToX(qpos)); x = static_cast<int>(tl->lineForTextPosition(0).cursorToX(qpos));
/* Since QString is UTF-16 and docstring is UCS-4, the offsets may /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
* not be the same when there are high-plan unicode characters * not be the same when there are high-plan unicode characters
* (bug #10443). * (bug #10443).
*/ */
#if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
return qstring_to_ucs4(tl.text().left(qpos)).length(); return qstring_to_ucs4(tl->text().left(qpos)).length();
#else #else
/* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned /* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
* by QString::toUcs4 (used by qstring_to_ucs4)may have wrong * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
* length. We work around the problem by trying all docstring * length. We work around the problem by trying all docstring
* positions until the right one is found. This is slow only if * positions until the right one is found. This is slow only if
* there are many high-plane Unicode characters. It might be * there are many high-plane Unicode characters. It might be
@ -246,10 +288,11 @@ int GuiFontMetrics::x2pos(docstring const & s, int & x, bool const rtl,
} }
bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const force) const
pair<int, int> *
GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
bool const rtl, bool const force) const
{ {
if (s.empty())
return false;
QTextLayout tl; QTextLayout tl;
/* Qt will not break at a leading or trailing space, and we need /* Qt will not break at a leading or trailing space, and we need
* that sometimes, see http://www.lyx.org/trac/ticket/9921. * that sometimes, see http://www.lyx.org/trac/ticket/9921.
@ -291,8 +334,7 @@ bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const
tl.createLine(); tl.createLine();
tl.endLayout(); tl.endLayout();
if ((force && line.textLength() == offset) || int(line.naturalTextWidth()) > x) if ((force && line.textLength() == offset) || int(line.naturalTextWidth()) > x)
return false; return new pair<int, int>(-1, -1);
x = int(line.naturalTextWidth());
/* Since QString is UTF-16 and docstring is UCS-4, the offsets may /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
* not be the same when there are high-plan unicode characters * not be the same when there are high-plan unicode characters
* (bug #10443). * (bug #10443).
@ -301,10 +343,10 @@ bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const
// The ending character zerow_nbsp has to be ignored if the line is complete. // The ending character zerow_nbsp has to be ignored if the line is complete.
int const qlen = line.textLength() - offset - (line.textLength() == qs.length()); int const qlen = line.textLength() - offset - (line.textLength() == qs.length());
#if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
s = qstring_to_ucs4(qs.mid(offset, qlen)); int len = qstring_to_ucs4(qs.mid(offset, qlen)).length();
#else #else
/* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned /* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
* by QString::toUcs4 (used by qstring_to_ucs4)may have wrong * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
* length. We work around the problem by trying all docstring * length. We work around the problem by trying all docstring
* positions until the right one is found. This is slow only if * positions until the right one is found. This is slow only if
* there are many high-plane Unicode characters. It might be * there are many high-plane Unicode characters. It might be
@ -315,7 +357,34 @@ bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const
while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen) while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen)
--len; --len;
LASSERT(len > 0 || qlen == 0, /**/); LASSERT(len > 0 || qlen == 0, /**/);
s = s.substr(0, len); #endif
// The -1 is here to account for the leading zerow_nbsp.
return new pair<int, int>(len, int(line.naturalTextWidth()));
}
bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const force) const
{
if (s.empty())
return false;
pair<int, int> * pp;
#ifdef CACHE_METRICS_BREAKAT
docstring const s_cache = s + convert<docstring>(x) + (rtl ? "r" : "l") + (force ? "f" : "w");
pp = breakat_cache_[s_cache];
if (!pp) {
#endif
pp = breakAt_helper(s, x, rtl, force);
#ifdef CACHE_METRICS_BREAKAT
breakat_cache_.insert(s_cache, pp, s_cache.size() * sizeof(char_type));
}
#endif
if (pp->first == -1)
return false;
s = s.substr(0, pp->first);
x = pp->second;
#ifndef CACHE_METRICS_BREAKAT
delete pp;
#endif #endif
return true; return true;
} }
@ -332,7 +401,6 @@ void GuiFontMetrics::rectText(docstring const & str,
} }
void GuiFontMetrics::buttonText(docstring const & str, void GuiFontMetrics::buttonText(docstring const & str,
int & w, int & ascent, int & descent) const int & w, int & ascent, int & descent) const
{ {

View File

@ -16,13 +16,29 @@
#include "support/docstring.h" #include "support/docstring.h"
#include <QByteArray>
#include <QCache>
#include <QFont> #include <QFont>
#include <QFontMetrics> #include <QFontMetrics>
#include <QHash> #include <QHash>
#include <QTextLayout> #include <QTextLayout>
// Declare which font metrics elements have to be cached
#define CACHE_METRICS_WIDTH
#define CACHE_METRICS_BREAKAT
// Qt 5.x already has its own caching of QTextLayout objects
#if (QT_VERSION < 0x050000)
#define CACHE_METRICS_QTEXTLAYOUT
#endif
#if defined(CACHE_METRICS_WIDTH) || defined(CACHE_METRICS_BREAKAT) \
|| defined(CACHE_METRICS_QTEXTLAYOUT)
#define CACHE_SOME_METRICS
#endif
#ifdef CACHE_SOME_METRICS
#include <QCache>
#endif
namespace lyx { namespace lyx {
namespace frontend { namespace frontend {
@ -63,11 +79,16 @@ public:
/// ///
int width(QString const & str) const; int width(QString const & str) const;
/// Return a pointer to a cached QTextLayout object
QTextLayout const *
getTextLayout(docstring const & s, bool const rtl,
double const wordspacing) const;
private: private:
QTextLayout const & std::pair<int, int> *
getTextLayout(docstring const & s, QFont font, breakAt_helper(docstring const & s, int const x,
bool const rtl, double const wordspacing) const; bool const rtl, bool const force) const;
/// The font /// The font
QFont font_; QFont font_;
@ -78,8 +99,20 @@ private:
/// Cache of char widths /// Cache of char widths
mutable QHash<char_type, int> width_cache_; mutable QHash<char_type, int> width_cache_;
#ifdef CACHE_METRICS_WIDTH
/// Cache of string widths /// Cache of string widths
mutable QCache<QByteArray, int> strwidth_cache_; mutable QCache<docstring, int> strwidth_cache_;
#endif
#ifdef CACHE_METRICS_BREAKAT
/// Cache for breakAt
mutable QCache<docstring, std::pair<int, int>> breakat_cache_;
#endif
#ifdef CACHE_METRICS_QTEXTLAYOUT
/// Cache for QTextLayout:s
mutable QCache<docstring, QTextLayout> qtextlayout_cache_;
#endif
struct AscendDescend { struct AscendDescend {
int ascent; int ascent;
@ -93,13 +126,6 @@ private:
/// Cache of char right bearings /// Cache of char right bearings
mutable QHash<char_type, int> rbearing_cache_; mutable QHash<char_type, int> rbearing_cache_;
// A trivial QTextLayout cache
mutable QTextLayout tl_cache_;
mutable docstring tl_cache_s_;
mutable QFont tl_cache_font_;
mutable bool tl_cache_rtl_;
mutable double tl_cache_wordspacing_;
}; };
} // namespace frontend } // namespace frontend

View File

@ -470,8 +470,17 @@ int GuiPainter::text(int x, int y, docstring const & s,
return textwidth; return textwidth;
} }
// don't use the pixmap cache, // don't use the pixmap cache
do_drawText(x, y, str, dir, f, ff); setQPainterPen(computeColor(f.realColor()));
if (dir != Auto) {
QTextLayout const * ptl = fm.getTextLayout(s, dir == RtL, wordspacing);
ptl->draw(this, QPointF(x, y - fm.maxAscent()));
}
else {
if (font() != ff)
setFont(ff);
drawText(x, y, str);
}
//LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8()) //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
// << " at " << x << "," << y); // << " at " << x << "," << y);
return textwidth; return textwidth;

View File

@ -122,6 +122,13 @@ string convert<string>(double d)
} }
template<>
docstring convert<docstring>(double d)
{
return from_ascii(convert<string>(d));
}
template<> template<>
int convert<int>(string const s) int convert<int>(string const s)
{ {

View File

@ -58,6 +58,8 @@ What's new
- Issue an error message if incompatible languages (in terms of language package - Issue an error message if incompatible languages (in terms of language package
requirements) are used (bug 10456). requirements) are used (bug 10456).
- Improve display speed, especially when compiled against Qt 4.x.
* DOCUMENTATION AND LOCALIZATION * DOCUMENTATION AND LOCALIZATION