QPainterPath: Add option to cache calculations

Speeds up repeated calls to length and percentage functions.

Change-Id: I357732e35de648e7da07ad492e58d5b94c2c0870
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
This commit is contained in:
Eirik Aavitsland 2024-09-01 20:18:45 +02:00
parent 76b1424048
commit 90247966a7
5 changed files with 214 additions and 13 deletions

View File

@ -2821,6 +2821,44 @@ QPolygonF QPainterPath::toFillPolygon(const QTransform &matrix) const
return polygon;
}
/*!
Returns true if caching is enabled; otherwise returns false.
\since 6.10
\sa setCachingEnabled()
*/
bool QPainterPath::isCachingEnabled() const
{
Q_D(QPainterPath);
return d && d->cacheEnabled;
}
/*!
Enables or disables length caching according to the value of \a enabled.
Enabling caching speeds up repeated calls to the member functions involving path length
and percentage values, such as length(), percentAtLength(), pointAtPercent() etc., at the cost
of some extra memory usage for storage of intermediate calculations. By default it is disabled.
Disabling caching will release any allocated cache memory.
\since 6.10
\sa isCachingEnabled(), length(), percentAtLength(), pointAtPercent()
*/
void QPainterPath::setCachingEnabled(bool enabled)
{
ensureData();
if (d_func()->cacheEnabled == enabled)
return;
detach();
QPainterPathPrivate *d = d_func();
d->cacheEnabled = enabled;
if (!enabled) {
d->m_runLengths.clear();
d->m_runLengths.squeeze();
}
}
//derivative of the equation
static inline qreal slopeAt(qreal t, qreal a, qreal b, qreal c, qreal d)
{
@ -2835,6 +2873,11 @@ qreal QPainterPath::length() const
Q_D(QPainterPath);
if (isEmpty())
return 0;
if (d->cacheEnabled) {
if (d->dirtyRunLengths)
d->computeRunLengths();
return d->m_runLengths.last();
}
qreal len = 0;
for (int i=1; i<d->elements.size(); ++i) {
@ -2883,6 +2926,32 @@ qreal QPainterPath::percentAtLength(qreal len) const
if (len > totalLength)
return 1;
if (d->cacheEnabled) {
const int ei = qMax(d->elementAtT(len / totalLength), 1); // Skip initial MoveTo
qreal res = 0;
const QPainterPath::Element &e = d->elements[ei];
switch (e.type) {
case QPainterPath::LineToElement:
res = len / totalLength;
break;
case CurveToElement:
{
QBezier b = QBezier::fromPoints(d->elements.at(ei-1),
e,
d->elements.at(ei+1),
d->elements.at(ei+2));
qreal prevLen = d->m_runLengths[ei - 1];
qreal blen = d->m_runLengths[ei] - prevLen;
qreal elemRes = b.tAtLength(len - prevLen);
res = (elemRes * blen + prevLen) / totalLength;
break;
}
default:
Q_UNREACHABLE();
}
return res;
}
qreal curLen = 0;
for (int i=1; i<d->elements.size(); ++i) {
const Element &e = d->elements.at(i);
@ -2927,7 +2996,8 @@ qreal QPainterPath::percentAtLength(qreal len) const
return 0;
}
static inline QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, qreal *bezierLength)
static inline QBezier uncached_bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength,
qreal *bezierLength)
{
*startingLength = 0;
if (t > 1)
@ -2981,6 +3051,35 @@ static inline QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *starti
return QBezier();
}
QBezier QPainterPathPrivate::bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength,
qreal *bezierLength) const
{
Q_ASSERT(t >= 0 && t <= 1);
QPainterPathPrivate *d = path.d_func();
if (!path.isEmpty() && d->cacheEnabled) {
const int ei = qMax(d->elementAtT(t), 1); // Avoid the initial MoveTo element
const qreal prevRunLength = d->m_runLengths[ei - 1];
*startingLength = prevRunLength;
*bezierLength = d->m_runLengths[ei] - prevRunLength;
const QPointF prev = d->elements[ei - 1];
const QPainterPath::Element &e = d->elements[ei];
switch (e.type) {
case QPainterPath::LineToElement:
{
QPointF delta = (e - prev) / 3;
return QBezier::fromPoints(prev, prev + delta, prev + 2 * delta, e);
}
case QPainterPath::CurveToElement:
return QBezier::fromPoints(prev, e, elements[ei + 1], elements[ei + 2]);
break;
default:
Q_UNREACHABLE();
}
}
return uncached_bezierAtT(path, t, startingLength, bezierLength);
}
/*!
Returns the point at at the percentage \a t of the current path.
The argument \a t has to be between 0 and 1.
@ -3006,7 +3105,7 @@ QPointF QPainterPath::pointAtPercent(qreal t) const
qreal totalLength = length();
qreal curLen = 0;
qreal bezierLen = 0;
QBezier b = bezierAtT(*this, t, &curLen, &bezierLen);
QBezier b = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen);
qreal realT = (totalLength * t - curLen) / bezierLen;
return b.pointAt(qBound(qreal(0), realT, qreal(1)));
@ -3034,7 +3133,7 @@ qreal QPainterPath::angleAtPercent(qreal t) const
qreal totalLength = length();
qreal curLen = 0;
qreal bezierLen = 0;
QBezier bez = bezierAtT(*this, t, &curLen, &bezierLen);
QBezier bez = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen);
qreal realT = (totalLength * t - curLen) / bezierLen;
qreal m1 = slopeAt(realT, bez.x1, bez.x2, bez.x3, bez.x4);
@ -3063,7 +3162,7 @@ qreal QPainterPath::slopeAtPercent(qreal t) const
qreal totalLength = length();
qreal curLen = 0;
qreal bezierLen = 0;
QBezier bez = bezierAtT(*this, t, &curLen, &bezierLen);
QBezier bez = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen);
qreal realT = (totalLength * t - curLen) / bezierLen;
qreal m1 = slopeAt(realT, bez.x1, bez.x2, bez.x3, bez.x4);
@ -3283,9 +3382,10 @@ bool QPainterPath::contains(const QPainterPath &p) const
void QPainterPath::setDirty(bool dirty)
{
d_func()->pathConverter.reset();
d_func()->dirtyBounds = dirty;
d_func()->dirtyControlBounds = dirty;
d_func()->pathConverter.reset();
d_func()->dirtyRunLengths = dirty;
d_func()->convex = false;
}
@ -3358,6 +3458,43 @@ void QPainterPath::computeControlPointRect() const
d->controlBounds = QRectF(minx, miny, maxx - minx, maxy - miny);
}
void QPainterPathPrivate::computeRunLengths()
{
Q_ASSERT(!elements.isEmpty());
m_runLengths.clear();
const int numElems = elements.size();
m_runLengths.reserve(numElems);
QPointF runPt = elements[0];
qreal runLen = 0.0;
for (int i = 0; i < numElems; i++) {
QPainterPath::Element e = elements[i];
switch (e.type) {
case QPainterPath::LineToElement:
runLen += QLineF(runPt, e).length();
runPt = e;
break;
case QPainterPath::CurveToElement: {
Q_ASSERT(i < numElems - 2);
QPainterPath::Element ee = elements[i + 2];
runLen += QBezier::fromPoints(runPt, e, elements[i + 1], ee).length();
runPt = ee;
break;
}
case QPainterPath::MoveToElement:
runPt = e;
break;
case QPainterPath::CurveToDataElement:
break;
}
m_runLengths.append(runLen);
}
Q_ASSERT(m_runLengths.size() == elements.size());
dirtyRunLengths = false;
}
#ifndef QT_NO_DEBUG_STREAM
QDebug operator<<(QDebug s, const QPainterPath &p)
{

View File

@ -134,8 +134,10 @@ public:
QPainterPath::Element elementAt(int i) const;
void setElementPositionAt(int i, qreal x, qreal y);
bool isCachingEnabled() const;
void setCachingEnabled(bool enabled);
qreal length() const;
qreal percentAtLength(qreal t) const;
qreal percentAtLength(qreal len) const;
QPointF pointAtPercent(qreal t) const;
qreal angleAtPercent(qreal t) const;
qreal slopeAtPercent(qreal t) const;
@ -174,6 +176,7 @@ private:
friend class QPainterPathStroker;
friend class QPainterPathStrokerPrivate;
friend class QPainterPathPrivate;
friend class QTransform;
friend class QVectorPath;
friend Q_GUI_EXPORT const QVectorPath &qtVectorPathForPath(const QPainterPath &);

View File

@ -24,6 +24,7 @@
#include <private/qvectorpath_p.h>
#include <private/qstroker_p.h>
#include <private/qbezier_p.h>
#include <memory>
@ -111,7 +112,8 @@ public:
dirtyBounds(false),
dirtyControlBounds(false),
convex(false),
hasWindingFill(false)
hasWindingFill(false),
cacheEnabled(false)
{
}
@ -124,21 +126,25 @@ public:
dirtyBounds(false),
dirtyControlBounds(false),
convex(false),
hasWindingFill(false)
hasWindingFill(false),
cacheEnabled(false)
{
}
QPainterPathPrivate(const QPainterPathPrivate &other) noexcept
: QSharedData(other),
elements(other.elements),
m_runLengths(other.m_runLengths),
bounds(other.bounds),
controlBounds(other.controlBounds),
cStart(other.cStart),
require_moveTo(false),
dirtyBounds(other.dirtyBounds),
dirtyControlBounds(other.dirtyControlBounds),
dirtyRunLengths(other.dirtyRunLengths),
convex(other.convex),
hasWindingFill(other.hasWindingFill)
hasWindingFill(other.hasWindingFill),
cacheEnabled(other.cacheEnabled)
{
}
@ -149,6 +155,10 @@ public:
inline void close();
inline void maybeMoveTo();
inline void clear();
void computeRunLengths();
int elementAtT(qreal t);
QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength,
qreal *bezierLength) const;
const QVectorPath &vectorPath() {
if (!pathConverter)
@ -159,6 +169,7 @@ public:
private:
QList<QPainterPath::Element> elements;
std::unique_ptr<QVectorPathConverter> pathConverter;
QList<qreal> m_runLengths;
QRectF bounds;
QRectF controlBounds;
@ -167,8 +178,10 @@ private:
bool require_moveTo : 1;
bool dirtyBounds : 1;
bool dirtyControlBounds : 1;
bool dirtyRunLengths : 1;
bool convex : 1;
bool hasWindingFill : 1;
bool cacheEnabled : 1;
};
class QPainterPathStrokerPrivate
@ -257,6 +270,7 @@ inline void QPainterPathPrivate::clear()
Q_ASSERT(ref.loadRelaxed() == 1);
elements.clear();
m_runLengths.clear();
cStart = 0;
bounds = {};
@ -265,12 +279,23 @@ inline void QPainterPathPrivate::clear()
require_moveTo = false;
dirtyBounds = false;
dirtyControlBounds = false;
dirtyRunLengths = false;
convex = false;
pathConverter.reset();
}
#define KAPPA qreal(0.5522847498)
inline int QPainterPathPrivate::elementAtT(qreal t)
{
Q_ASSERT(cacheEnabled);
if (dirtyRunLengths)
computeRunLengths();
qreal len = t * m_runLengths.constLast();
const auto it = std::lower_bound(m_runLengths.constBegin(), m_runLengths.constEnd(), len);
return (it == m_runLengths.constEnd()) ? m_runLengths.size() - 1 : int(it - m_runLengths.constBegin());
}
#define KAPPA qreal(0.5522847498)
QT_END_NAMESPACE

View File

@ -1145,10 +1145,16 @@ void tst_QPainterPath::pointAtPercent()
QFETCH(qreal, percent);
QFETCH(QPointF, point);
QVERIFY(!path.isCachingEnabled());
QPointF result = path.pointAtPercent(percent);
QVERIFY(pathFuzzyCompare(point.x() , result.x()));
QVERIFY(pathFuzzyCompare(point.y() , result.y()));
path.setCachingEnabled(true);
QVERIFY(path.isCachingEnabled());
result = path.pointAtPercent(percent);
QVERIFY2(pathFuzzyCompare(point.x() , result.x()), "caching");
QVERIFY2(pathFuzzyCompare(point.y() , result.y()), "caching");
}
void tst_QPainterPath::lengths_data()
@ -1190,11 +1196,20 @@ void tst_QPainterPath::lengths()
QFETCH(qreal, lenAt50);
QFETCH(qreal, lenAt75);
QVERIFY(!path.isCachingEnabled());
QVERIFY(pathFuzzyCompare(path.length() / 1000, length / 1000));
QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt25), qreal(0.25)));
QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt50), qreal(0.50)));
QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt75), qreal(0.75)));
QVERIFY(pathFuzzyCompare(path.percentAtLength(length), qreal(1)));
path.setCachingEnabled(true);
QVERIFY(path.isCachingEnabled());
QVERIFY2(pathFuzzyCompare(path.length() / 1000, length / 1000), "caching");
QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt25), qreal(0.25)), "caching");
QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt50), qreal(0.50)), "caching");
QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt75), qreal(0.75)), "caching");
QVERIFY2(pathFuzzyCompare(path.percentAtLength(length), qreal(1)), "caching");
}
void tst_QPainterPath::setElementPositionAt()
@ -1226,6 +1241,11 @@ void tst_QPainterPath::angleAtPercent()
path.moveTo(line.p1());
path.lineTo(line.p2());
QVERIFY(!path.isCachingEnabled());
QCOMPARE(path.angleAtPercent(0.5), line.angle());
path.setCachingEnabled(true);
QVERIFY(path.isCachingEnabled());
QCOMPARE(path.angleAtPercent(0.5), line.angle());
}
}

View File

@ -15,8 +15,12 @@ public:
private slots:
void initTestCase_data();
void general_data();
void length_data() { general_data(); }
void length();
void percentAtLength_data() { general_data(); }
void percentAtLength();
void pointAtPercent_data() { general_data(); }
void pointAtPercent();
};
@ -62,11 +66,19 @@ void tst_QPainterPath::initTestCase_data()
QTest::newRow("2k_text") << p;
}
void tst_QPainterPath::general_data()
{
QTest::addColumn<bool>("caching");
QTest::newRow("Uncached") << false;
QTest::newRow("Cached") << true;
}
void tst_QPainterPath::length()
{
QFETCH_GLOBAL(QPainterPath, path);
//const qreal len = path.length() * 0.72;
QFETCH(bool, caching);
path.setCachingEnabled(caching);
QBENCHMARK {
path.length();
@ -76,6 +88,8 @@ void tst_QPainterPath::length()
void tst_QPainterPath::percentAtLength()
{
QFETCH_GLOBAL(QPainterPath, path);
QFETCH(bool, caching);
path.setCachingEnabled(caching);
const qreal len = path.length() * 0.72;
@ -87,6 +101,8 @@ void tst_QPainterPath::percentAtLength()
void tst_QPainterPath::pointAtPercent()
{
QFETCH_GLOBAL(QPainterPath, path);
QFETCH(bool, caching);
path.setCachingEnabled(caching);
const qreal t = 0.72;