summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEirik Aavitsland <[email protected]>2024-09-01 20:18:45 +0200
committerEirik Aavitsland <[email protected]>2025-02-19 15:50:26 +0100
commit90247966a799a11fd17d5e55d00379a34b1cdf1f (patch)
tree724401472775a03fa1801bf06f0147ba57471e61
parent76b1424048bf7645a8a24425be14b8b679fec206 (diff)
QPainterPath: Add option to cache calculations
Speeds up repeated calls to length and percentage functions. Change-Id: I357732e35de648e7da07ad492e58d5b94c2c0870 Reviewed-by: Eskil Abrahamsen Blomfeldt <[email protected]>
-rw-r--r--src/gui/painting/qpainterpath.cpp147
-rw-r--r--src/gui/painting/qpainterpath.h5
-rw-r--r--src/gui/painting/qpainterpath_p.h33
-rw-r--r--tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp22
-rw-r--r--tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp20
5 files changed, 214 insertions, 13 deletions
diff --git a/src/gui/painting/qpainterpath.cpp b/src/gui/painting/qpainterpath.cpp
index 21a0d39855c..daea8d7d0f0 100644
--- a/src/gui/painting/qpainterpath.cpp
+++ b/src/gui/painting/qpainterpath.cpp
@@ -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)
{
diff --git a/src/gui/painting/qpainterpath.h b/src/gui/painting/qpainterpath.h
index 4caf1aa5c01..59faf4a53fd 100644
--- a/src/gui/painting/qpainterpath.h
+++ b/src/gui/painting/qpainterpath.h
@@ -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 &);
diff --git a/src/gui/painting/qpainterpath_p.h b/src/gui/painting/qpainterpath_p.h
index eb6878ce712..6de7db4ed3b 100644
--- a/src/gui/painting/qpainterpath_p.h
+++ b/src/gui/painting/qpainterpath_p.h
@@ -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
diff --git a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp
index cd343445490..71c689114cb 100644
--- a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp
+++ b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp
@@ -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());
}
}
diff --git a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp
index ab861d63585..a4854e3fdb2 100644
--- a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp
+++ b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp
@@ -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;