From 8d80cd1078a7900fcdc8eee4b78e13f40663de6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20M=C3=B6lzer?= Date: Tue, 23 Jun 2026 14:50:02 +0200 Subject: [PATCH 1/2] [QMS-1130] Add Tracy Add the profiler Tracy to the project and start instrumenting some functions relevant to IMG rendering. With tracy turned off at compile time this won't add any overhead. Having a profiler available is essential to guide performance improvements. To use it turn on tracy with -DTRACY_ENABLE=1 then build the profiler application itself, launch QMS and the profiler and connect it. The tracy project will end in _deps/tracy-src inside the build dir. --- CMakeLists.txt | 15 +++++++++++++++ src/qmapshack/CMakeLists.txt | 1 + src/qmapshack/canvas/IDrawContext.cpp | 2 ++ src/qmapshack/map/CMapDraw.cpp | 3 +++ src/qmapshack/map/CMapIMG.cpp | 12 ++++++++++++ 5 files changed, 33 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index db1524b41..2c4bcf7f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,21 @@ endif(NOT ALGLIB_FOUND AND BUILD_QMAPSHACK) add_definitions(-DHELPPATH=${HTML_INSTALL_DIR}) +include(FetchContent) + +############################################################################################### +# Get Tracy +############################################################################################### +set(TRACY_GIT_TAG "master" CACHE STRING "Tracy git revision to fetch") +FetchContent_Declare ( + tracy + GIT_REPOSITORY https://github.com/wolfpld/tracy.git + GIT_TAG master + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) +FetchContent_MakeAvailable(tracy) + ############################################################################################### # Create library from Garmin's FIT SDK ############################################################################################### diff --git a/src/qmapshack/CMakeLists.txt b/src/qmapshack/CMakeLists.txt index 1fc263b84..7165465cd 100644 --- a/src/qmapshack/CMakeLists.txt +++ b/src/qmapshack/CMakeLists.txt @@ -1035,6 +1035,7 @@ target_link_libraries(${APPLICATION_NAME} ${ALGLIB_LIBRARIES} QuaZip::QuaZip GarminFit + Tracy::TracyClient ) if(APPLE) diff --git a/src/qmapshack/canvas/IDrawContext.cpp b/src/qmapshack/canvas/IDrawContext.cpp index 9d39be727..859e082fd 100644 --- a/src/qmapshack/canvas/IDrawContext.cpp +++ b/src/qmapshack/canvas/IDrawContext.cpp @@ -19,6 +19,7 @@ #include "canvas/IDrawContext.h" #include +#include #define BUFFER_BORDER 50 @@ -258,6 +259,7 @@ void IDrawContext::convertPx2Rad(QPointF& p) const { } void IDrawContext::convertRad2Px(QPointF& p) const { + ZoneScoped; mutex.lock(); // --------- start serialize with thread QPointF f = focus; diff --git a/src/qmapshack/map/CMapDraw.cpp b/src/qmapshack/map/CMapDraw.cpp index b01b79fdd..88dca7216 100644 --- a/src/qmapshack/map/CMapDraw.cpp +++ b/src/qmapshack/map/CMapDraw.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "CMainWindow.h" #include "canvas/CCanvas.h" @@ -405,9 +406,11 @@ void CMapDraw::drawt(IDrawContext::buffer_t& currentBuffer) /* override */ } item->setProcessing(true); + FrameMarkStart("imgDraw"); item->getMapfile()->draw(currentBuffer); item->setProcessing(false); seenActiveMap = true; + FrameMarkEnd("imgDraw"); } } CMapItem::mutexActiveMaps.unlock(); diff --git a/src/qmapshack/map/CMapIMG.cpp b/src/qmapshack/map/CMapIMG.cpp index 87efe621a..d2d42c083 100644 --- a/src/qmapshack/map/CMapIMG.cpp +++ b/src/qmapshack/map/CMapIMG.cpp @@ -18,6 +18,8 @@ #include "map/CMapIMG.h" +#include + #include #include @@ -1098,6 +1100,7 @@ quint8 CMapIMG::scale2bits(const QPointF& scale) { void CMapIMG::draw(IDrawContext::buffer_t& buf) /* override */ { + ZoneScoped; if (map->needsRedraw()) { return; } @@ -1207,6 +1210,7 @@ void CMapIMG::draw(IDrawContext::buffer_t& buf) /* override */ void CMapIMG::loadVisibleData(bool fast, polytype_t& polygons, polytype_t& polylines, pointtype_t& points, pointtype_t& pois, unsigned level, const QRectF& viewport, QPainter& p) { + ZoneScoped; #ifndef Q_OS_WIN32 CFileExt file(filename); if (!file.open(QIODevice::ReadOnly)) { @@ -1527,6 +1531,7 @@ void CMapIMG::loadSubDiv(CFileExt& file, const subdiv_desc_t& subdiv, IGarminStr } void CMapIMG::drawPolygons(QPainter& p, polytype_t& lines) { + ZoneScoped; const int N = polygonDrawOrder.size(); for (int n = 0; n < N; ++n) { quint32 type = polygonDrawOrder[(N - 1) - n]; @@ -1555,6 +1560,7 @@ void CMapIMG::drawPolygons(QPainter& p, polytype_t& lines) { } void CMapIMG::drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale) { + ZoneScoped; textpaths.clear(); QFont font = CMainWindow::self().getMapFont(); @@ -1730,6 +1736,7 @@ void CMapIMG::drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale void CMapIMG::drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polyline_property& property, const QFont& font, const QPointF& scale) { + ZoneScoped; QPolygonF& poly = l.pixel; const int size = poly.size(); const int lineWidth = p.pen().width(); @@ -1762,6 +1769,7 @@ void CMapIMG::drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polylin } void CMapIMG::drawLine(QPainter& p, const CGarminPolygon& l) { + ZoneScoped; const QPolygonF& poly = l.pixel; const int size = poly.size(); @@ -1831,6 +1839,7 @@ void CMapIMG::addLabel(const CGarminPoint& pt, const QRect& rect, const CGarminT } void CMapIMG::drawPoints(QPainter& p, pointtype_t& pts, QVector& rectPois) { + ZoneScoped; pointtype_t::iterator pt = pts.begin(); while (pt != pts.end()) { map->convertRad2Px(pt->pos); @@ -1871,6 +1880,7 @@ void CMapIMG::drawPoints(QPainter& p, pointtype_t& pts, QVector& rectPoi } void CMapIMG::drawPois(QPainter& p, pointtype_t& pts, QVector& rectPois) { + ZoneScoped; for (CGarminPoint& pt : pts) { map->convertRad2Px(pt.pos); @@ -1904,6 +1914,7 @@ void CMapIMG::drawPois(QPainter& p, pointtype_t& pts, QVector& rectPois) } void CMapIMG::drawLabels(QPainter& p, const QVector& lbls) { + ZoneScoped; QFont f = CMainWindow::self().getMapFont(); QVector fonts(8, f); fonts[CGarminTyp::eSmall].setPointSize(f.pointSize() - 2); @@ -1916,6 +1927,7 @@ void CMapIMG::drawLabels(QPainter& p, const QVector& lbls) { } void CMapIMG::drawText(QPainter& p) { + ZoneScoped; p.setPen(Qt::black); for (const textpath_t& textpath : std::as_const(textpaths)) { From 65ae3c8109cd188599903ba0806422a9a4935153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20M=C3=B6lzer?= Date: Tue, 23 Jun 2026 18:11:48 +0200 Subject: [PATCH 2/2] [QMS-1130] Replace QPainter with Blend2D in CMapIMG Dramatically speed up IMG rendering by replacing the very slow Qt vector engine with the much faster Blend2D for all but text rendering. --- CLAUDE.md | 38 +++ CMakeLists.txt | 29 ++ src/qmapshack/CMakeLists.txt | 1 + src/qmapshack/map/CMapIMG.cpp | 558 +++++++++++++++++++++------------- src/qmapshack/map/CMapIMG.h | 22 +- 5 files changed, 428 insertions(+), 220 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6fe801800..8c6601e2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,6 +150,44 @@ Only local BRouter supports fast (on-the-fly) routing. Online BRouter and Routin --- +## Architecture: CMapIMG Blend2D hybrid renderer + +`CMapIMG::draw()` (the Garmin IMG renderer) rasterises geometry with **Blend2D** and +keeps **text/labels on QPainter**. Dependency: `find_package(blend2d REQUIRED)` (root +CMakeLists), linked as `blend2d::blend2d`. + +**Phase split in `draw()`:** +1. *Geometry phase (Blend2D):* `drawPolygons`, `drawPolylines`, `drawPoints`, `drawPois`, + `loadVisibleData` — all take `BLContext&`. They also collect text into `textpaths`/`labels`. +2. *Text phase (QPainter):* `drawText`, `drawLabels` — still take `QPainter&`. Qt's font + matching/shaping has no Blend2D equivalent and text is a negligible share of draw time. + +**Key invariants (easy to break):** +- **Premultiplied round-trip.** The shared buffer is `Format_ARGB32` (non-premultiplied); + Blend2D only renders `BL_FORMAT_PRGB32`. `draw()` does `convertTo(Format_ARGB32_Premultiplied)` + at the start and `convertTo(Format_ARGB32)` at the end so the format change stays invisible + to the rest of the map stack. (Optional follow-up: make `IDrawContext` allocate premultiplied + buffers to drop both conversions.) +- **Synchronous context only.** `BLContext(blBuf)` defaults to synchronous rendering, so source + images for blits (`img2line` segments, icons) and pattern tiles may be temporaries — they are + read before the call returns. Do **not** switch to a multithreaded/async `BLContextCreateInfo` + without making those sources outlive `ctx.end()`. +- **`ctx.end()` before the QPainter phase** — flushes Blend2D pixels before QPainter touches the + same image. +- **Both phases re-apply `translate(-pp)`** (the buffer-vs-screen offset); the single shared + painter that used to do it once is gone. + +**Helpers (anonymous namespace in `CMapIMG.cpp`):** `applyPen` (QPen→stroke state incl. Qt dash +styles, returns false for `Qt::NoPen`), `polyToPath`, `toBLColor`, `blitQImage`/`blitBullet` +(convert to premultiplied on the fly), `brushToTile` (texture/hatch QBrush → repeating `BLPattern` +tile). `drawPolygons` sets `BL_FILL_RULE_EVEN_ODD` to match `QPainter::drawPolygon`. + +The pixmap-along-line loop indexes polyline vertices directly (arc-length breakpoints coincide +with vertices), replacing the old `QPainterPath::pointAtPercent` walk; angles use `std::atan2` +(radians) since `BLContext::rotate` takes radians, not degrees. + +--- + ## Architecture: tree item delegates The three `QStyledItemDelegate` subclasses used by the tree views: diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c4bcf7f9..baff8e0bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -244,6 +244,35 @@ FetchContent_Declare ( ) FetchContent_MakeAvailable(tracy) +############################################################################################### +# Get Blend2D and it's dependency AsmJit +############################################################################################### +set(ASMJIT_GIT_TAG "master" CACHE STRING "AsmJit git revision to fetch") +set(BLEND2D_GIT_TAG "master" CACHE STRING "Blend2D git revision to fetch") + +# asmjit is Blend2D's JIT backend. Fetch its sources only: SOURCE_SUBDIR points at a +# directory with no CMakeLists.txt so FetchContent_MakeAvailable populates without calling +# add_subdirectory(). Blend2D adds asmjit itself (via ASMJIT_DIR) and, because we build it +# statically, embeds it directly into the blend2d library. +FetchContent_Declare(asmjit + GIT_REPOSITORY https://github.com/asmjit/asmjit.git + GIT_TAG ${ASMJIT_GIT_TAG} + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + SOURCE_SUBDIR __populate_only__ +) +FetchContent_MakeAvailable(asmjit) +set(ASMJIT_DIR "${asmjit_SOURCE_DIR}" CACHE PATH "AsmJit source directory" FORCE) + +set(BLEND2D_STATIC TRUE) +FetchContent_Declare(blend2d + GIT_REPOSITORY https://github.com/blend2d/blend2d.git + GIT_TAG ${BLEND2D_GIT_TAG} + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE +) +FetchContent_MakeAvailable(blend2d) + ############################################################################################### # Create library from Garmin's FIT SDK ############################################################################################### diff --git a/src/qmapshack/CMakeLists.txt b/src/qmapshack/CMakeLists.txt index 7165465cd..4af37c76c 100644 --- a/src/qmapshack/CMakeLists.txt +++ b/src/qmapshack/CMakeLists.txt @@ -1036,6 +1036,7 @@ target_link_libraries(${APPLICATION_NAME} QuaZip::QuaZip GarminFit Tracy::TracyClient + blend2d::blend2d ) if(APPLE) diff --git a/src/qmapshack/map/CMapIMG.cpp b/src/qmapshack/map/CMapIMG.cpp index d2d42c083..b4153ee69 100644 --- a/src/qmapshack/map/CMapIMG.cpp +++ b/src/qmapshack/map/CMapIMG.cpp @@ -18,10 +18,12 @@ #include "map/CMapIMG.h" -#include +#include #include #include +#include +#include #include "CMainWindow.h" #include "canvas/CCanvas.h" @@ -117,6 +119,148 @@ static inline bool isCluttered(QVector& rectPois, const QRectF& rect) { return false; } +namespace { +/// Convert a QColor to a Blend2D non-premultiplied 0xAARRGGBB colour. +inline BLRgba32 toBLColor(const QColor& c) { return BLRgba32(c.rgba()); } + +/// Build a Blend2D path from a polyline. When @p close is set the path is closed +/// so it can be filled and its closing edge stroked (matching QPainter::drawPolygon). +BLPath polyToPath(const QPolygonF& poly, bool close) { + BLPath path; + const int n = poly.size(); + if (n == 0) { + return path; + } + path.move_to(poly[0].x(), poly[0].y()); + for (int i = 1; i < n; ++i) { + path.line_to(poly[i].x(), poly[i].y()); + } + if (close) { + path.close(); + } + return path; +} + +BLStrokeCap toBLCap(Qt::PenCapStyle cap) { + switch (cap) { + case Qt::SquareCap: + return BL_STROKE_CAP_SQUARE; + case Qt::RoundCap: + return BL_STROKE_CAP_ROUND; + case Qt::FlatCap: + default: + return BL_STROKE_CAP_BUTT; + } +} + +BLStrokeJoin toBLJoin(Qt::PenJoinStyle join) { + switch (join) { + case Qt::BevelJoin: + return BL_STROKE_JOIN_BEVEL; + case Qt::RoundJoin: + return BL_STROKE_JOIN_ROUND; + case Qt::MiterJoin: + case Qt::SvgMiterJoin: + default: + return BL_STROKE_JOIN_MITER_BEVEL; + } +} + +/** + @brief Configure a context's stroke state from a QPen. + + Maps width, cap, join, colour and the simple Qt dash styles to their Blend2D + counterparts. Qt dash patterns are expressed in pen-width units, so they are + scaled by the stroke width here. + + @return false for Qt::NoPen, telling the caller to skip stroking entirely. + */ +bool applyPen(BLContext& ctx, const QPen& pen) { + if (pen.style() == Qt::NoPen) { + return false; + } + + const double width = pen.widthF() > 0.0 ? pen.widthF() : 1.0; + ctx.set_stroke_width(width); + ctx.set_stroke_caps(toBLCap(pen.capStyle())); + ctx.set_stroke_join(toBLJoin(pen.joinStyle())); + + BLArray dashes; + switch (pen.style()) { + case Qt::DashLine: + dashes.append(4.0 * width); + dashes.append(2.0 * width); + break; + case Qt::DotLine: + dashes.append(1.0 * width); + dashes.append(2.0 * width); + break; + case Qt::DashDotLine: + dashes.append(4.0 * width); + dashes.append(2.0 * width); + dashes.append(1.0 * width); + dashes.append(2.0 * width); + break; + case Qt::DashDotDotLine: + dashes.append(4.0 * width); + dashes.append(2.0 * width); + dashes.append(1.0 * width); + dashes.append(2.0 * width); + dashes.append(1.0 * width); + dashes.append(2.0 * width); + break; + default: // SolidLine (and custom dashes, treated as solid) + break; + } + // An empty dash array clears any dash pattern left over from a previous type. + ctx.set_stroke_dash_array(dashes); + ctx.set_stroke_dash_offset(0.0); + ctx.set_stroke_style(toBLColor(pen.color())); + return true; +} + +/// Blit a QImage at (@p x, @p y). Blend2D only consumes premultiplied ARGB, so a +/// conversion is done on the fly for sources that are stored in another format. +void blitQImage(BLContext& ctx, double x, double y, const QImage& img) { + if (img.isNull()) { + return; + } + QImage storage; + const QImage& src = (img.format() == QImage::Format_ARGB32_Premultiplied) + ? img + : (storage = img.convertToFormat(QImage::Format_ARGB32_Premultiplied)); + BLImage bl; + if (bl.create_from_data(src.width(), src.height(), BL_FORMAT_PRGB32, const_cast(src.constBits()), + src.bytesPerLine(), BL_DATA_ACCESS_READ) != BL_SUCCESS) { + return; + } + ctx.blit_image(BLPoint(x, y), bl); +} + +/// Blit the small blue bullet used as a fallback marker for cluttered points. +void blitBullet(BLContext& ctx, double x, double y) { + static const QImage bullet = + QImage(":/icons/8x8/bullet_blue.png").convertToFormat(QImage::Format_ARGB32_Premultiplied); + blitQImage(ctx, x, y, bullet); +} + +/// Render a non-solid QBrush into a small premultiplied tile usable as a repeating +/// Blend2D pattern. Texture brushes return their image directly; hatch and dense +/// patterns are rasterised by Qt onto an 8x8 transparent tile (their period). +QImage brushToTile(const QBrush& brush) { + if (brush.style() == Qt::TexturePattern) { + return brush.textureImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + } + + QImage tile(8, 8, QImage::Format_ARGB32_Premultiplied); + tile.fill(Qt::transparent); + QPainter p(&tile); + p.fillRect(tile.rect(), brush); + p.end(); + return tile; +} +} // namespace + CMapIMG::CMapIMG(const QString& filename, CMapDraw* parent) : IMap(eFeatVisibility | eFeatVectorItems | eFeatTypFile, parent), filename(filename), @@ -1111,16 +1255,6 @@ void CMapIMG::draw(IDrawContext::buffer_t& buf) /* override */ return; } - QPainter p(&buf.image); - p.setOpacity(getOpacity() / 100.0); - USE_ANTI_ALIASING(p, true); - - QFont f = CMainWindow::self().getMapFont(); - - p.setFont(f); - p.setPen(Qt::black); - p.setBrush(Qt::NoBrush); - quint8 bits = scale2bits(bufferScale); QVector::const_iterator maplevel = maplevels.constEnd(); @@ -1153,63 +1287,81 @@ void CMapIMG::draw(IDrawContext::buffer_t& buf) /* override */ */ QPointF pp = buf.ref1; map->convertRad2Px(pp); - p.save(); - p.translate(-pp); - if (map->needsRedraw()) { - p.restore(); - return; - } + /* + The shared draw buffer is plain (non-premultiplied) ARGB32, but Blend2D only + renders into premultiplied ARGB. Convert the buffer in place for the duration + of this draw and restore the original format before returning, so the change + stays invisible to the rest of the map stack. + */ + buf.image.convertTo(QImage::Format_ARGB32_Premultiplied); - try { - loadVisibleData(false, polygons, polylines, points, pois, maplevel->level, viewport, p); - } catch (const std::bad_alloc&) { - qWarning() << "GarminIMG: Allocation error. Abort map rendering."; - p.restore(); - return; - } + bool ok = !map->needsRedraw(); + { + BLImage blBuf; + if (blBuf.create_from_data(buf.image.width(), buf.image.height(), BL_FORMAT_PRGB32, buf.image.bits(), + buf.image.bytesPerLine()) != BL_SUCCESS) { + buf.image.convertTo(QImage::Format_ARGB32); + return; + } - if (map->needsRedraw()) { - p.restore(); - return; - } - drawPolygons(p, polygons); + BLContext ctx(blBuf); + ctx.set_global_alpha(getOpacity() / 100.0); + /* + The buffer is allocated at device resolution (pixelRatio larger) and tagged with + QImage::setDevicePixelRatio(). QPainter applies that ratio implicitly, but Blend2D + renders into the raw pixels and knows nothing about it. Scale the context by the + pixel ratio so the logical coordinates produced by convertRad2Px() map to device + pixels exactly as the QPainter text phase (and the rest of the map stack) expect. + */ + ctx.scale(buf.pixelRatio, buf.pixelRatio); + ctx.translate(-pp.x(), -pp.y()); + + if (ok) { + try { + loadVisibleData(false, polygons, polylines, points, pois, maplevel->level, viewport, ctx); + } catch (const std::bad_alloc&) { + qWarning() << "GarminIMG: Allocation error. Abort map rendering."; + ok = false; + } + } - if (map->needsRedraw()) { - p.restore(); - return; - } - drawPolylines(p, polylines, bufferScale); + if (ok && !map->needsRedraw()) { + drawPolygons(ctx, polygons); + } + if (ok && !map->needsRedraw()) { + drawPolylines(ctx, polylines, bufferScale); + } + if (ok && !map->needsRedraw()) { + drawPoints(ctx, points, rectPois); + } + if (ok && !map->needsRedraw()) { + drawPois(ctx, pois, rectPois); + } - if (map->needsRedraw()) { - p.restore(); - return; + ctx.end(); } - drawPoints(p, points, rectPois); - if (map->needsRedraw()) { - p.restore(); - return; - } - drawPois(p, pois, rectPois); + // Text and labels are drawn on top with QPainter (see drawText()/drawLabels()). + if (ok && !map->needsRedraw()) { + QPainter p(&buf.image); + p.setOpacity(getOpacity() / 100.0); + USE_ANTI_ALIASING(p, true); + p.setFont(CMainWindow::self().getMapFont()); + p.translate(-pp); - if (map->needsRedraw()) { - p.restore(); - return; - } - drawText(p); + drawText(p); - if (map->needsRedraw()) { - p.restore(); - return; + if (!map->needsRedraw()) { + drawLabels(p, labels); + } } - drawLabels(p, labels); - p.restore(); + buf.image.convertTo(QImage::Format_ARGB32); } void CMapIMG::loadVisibleData(bool fast, polytype_t& polygons, polytype_t& polylines, pointtype_t& points, - pointtype_t& pois, unsigned level, const QRectF& viewport, QPainter& p) { + pointtype_t& pois, unsigned level, const QRectF& viewport, BLContext& ctx) { ZoneScoped; #ifndef Q_OS_WIN32 CFileExt file(filename); @@ -1268,9 +1420,9 @@ void CMapIMG::loadVisibleData(bool fast, polytype_t& polygons, polytype_t& polyl map->convertRad2Px(poly); - p.setPen(QPen(Qt::magenta, 2)); - p.setBrush(Qt::NoBrush); - p.drawPolygon(poly); + ctx.setStrokeWidth(2); + ctx.setStrokeStyle(BLRgba32(0xFFFF00FFu)); // magenta + ctx.strokePath(polyToPath(poly, true)); #endif // DEBUG_SHOW_SECTION_BORDERS } @@ -1287,8 +1439,9 @@ void CMapIMG::loadVisibleData(bool fast, polytype_t& polygons, polytype_t& polyl QPolygonF poly; poly << p1 << p2 << p3 << p4; - p.setPen(Qt::black); - p.drawPolygon(poly); + ctx.setStrokeWidth(1); + ctx.setStrokeStyle(BLRgba32(0xFF000000u)); // black + ctx.strokePath(polyToPath(poly, true)); #endif // DEBUG_SHOW_SUBDIV_BORDERS #ifdef Q_OS_WIN32 @@ -1530,14 +1683,41 @@ void CMapIMG::loadSubDiv(CFileExt& file, const subdiv_desc_t& subdiv, IGarminStr } } -void CMapIMG::drawPolygons(QPainter& p, polytype_t& lines) { +void CMapIMG::drawPolygons(BLContext& ctx, polytype_t& lines) { ZoneScoped; + const bool night = CMainWindow::self().isNight(); + + // QPainter::drawPolygon fills using the odd-even rule; Blend2D defaults to non-zero. + ctx.set_fill_rule(BL_FILL_RULE_EVEN_ODD); + const int N = polygonDrawOrder.size(); for (int n = 0; n < N; ++n) { quint32 type = polygonDrawOrder[(N - 1) - n]; + const CGarminTyp::polygon_property& property = polygonProperties[type]; + const QBrush& brush = night ? property.brushNight : property.brushDay; + + // Configure the fill style once per type. Solid colours map directly; texture + // images and hatch patterns are realised as a repeating BLPattern. The pattern's + // pixel data (tile) must outlive every fill that uses it, hence the outer scope. + QImage tile; + BLImage tileBL; + BLPattern pattern; + if (brush.style() == Qt::SolidPattern) { + ctx.set_fill_style(toBLColor(brush.color())); + } else { + tile = brushToTile(brush); + if (tileBL.create_from_data(tile.width(), tile.height(), BL_FORMAT_PRGB32, const_cast(tile.constBits()), + tile.bytesPerLine(), BL_DATA_ACCESS_READ) == BL_SUCCESS) { + pattern.set_image(tileBL); + pattern.set_extend_mode(BL_EXTEND_MODE_REPEAT); + ctx.set_fill_style(pattern); + } else { + ctx.set_fill_style(toBLColor(brush.color())); + } + } - p.setPen(polygonProperties[type].pen); - p.setBrush(CMainWindow::self().isNight() ? polygonProperties[type].brushNight : polygonProperties[type].brushDay); + const QPen& pen = property.pen; + const bool hasOutline = applyPen(ctx, pen); for (CGarminPolygon& line : lines) { if (line.type != type) { @@ -1545,23 +1725,25 @@ void CMapIMG::drawPolygons(QPainter& p, polytype_t& lines) { } QPolygonF& poly = line.pixel; - map->convertRad2Px(poly); - // simplifyPolyline(line); - - p.drawPolygon(poly); + const BLPath path = polyToPath(poly, true); + ctx.fill_path(path); + if (hasOutline) { + ctx.stroke_path(path); + } - if (!polygonProperties[type].known) { + if (!property.known) { qDebug() << "unknown polygon" << Qt::hex << type; } } } } -void CMapIMG::drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale) { +void CMapIMG::drawPolylines(BLContext& ctx, polytype_t& lines, const QPointF& scale) { ZoneScoped; textpaths.clear(); + const bool night = CMainWindow::self().isNight(); QFont font = CMainWindow::self().getMapFont(); font.setPointSize(9); @@ -1569,13 +1751,6 @@ void CMapIMG::drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale QVector lengths; lengths.reserve(100); - /* - int pixmapCount = 0; - int borderCount = 0; - int normalCount = 0; - int imageCount = 0; - int deletedCount = 0; - */ QHash > dict; for (int i = 0; i < lines.count(); ++i) { @@ -1588,158 +1763,124 @@ void CMapIMG::drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale const quint32& type = props.key(); const CGarminTyp::polyline_property& property = props.value(); - if (dict[type].isEmpty()) { + const QList& indices = dict[type]; + if (indices.isEmpty()) { continue; } if (property.hasPixmap) { - const QImage& pixmap = CMainWindow::self().isNight() ? property.imgNight : property.imgDay; + const QImage& pixmap = night ? property.imgNight : property.imgDay; const qreal h = pixmap.height(); - QList::const_iterator it = dict[type].constBegin(); - for (; it != dict[type].constEnd(); ++it) { - CGarminPolygon& item = lines[*it]; - { - // pixmapCount++; - - QPolygonF& poly = item.pixel; - int size = poly.size(); - - if (size < 2) { - continue; - } + for (quint32 idx : indices) { + CGarminPolygon& item = lines[idx]; + QPolygonF& poly = item.pixel; + const int size = poly.size(); - map->convertRad2Px(poly); - - lengths.resize(0); - - // deletedCount += line.size(); - // simplifyPolyline(line); - // deletedCount -= line.size(); - // size = line.size(); + if (size < 2) { + continue; + } - lengths.reserve(size); + map->convertRad2Px(poly); - QPainterPath path; - qreal totalLength = 0; + lengths.resize(0); + lengths.reserve(size); - qreal u1 = poly[0].x(); - qreal v1 = poly[0].y(); + qreal u1 = poly[0].x(); + qreal v1 = poly[0].y(); + for (int i = 1; i < size; ++i) { + qreal u2 = poly[i].x(); + qreal v2 = poly[i].y(); - for (int i = 1; i < size; ++i) { - qreal u2 = poly[i].x(); - qreal v2 = poly[i].y(); + qreal segLength = qSqrt((u2 - u1) * (u2 - u1) + (v2 - v1) * (v2 - v1)); + lengths << segLength; - qreal segLength = qSqrt((u2 - u1) * (u2 - u1) + (v2 - v1) * (v2 - v1)); - totalLength += segLength; - lengths << segLength; + u1 = u2; + v1 = v2; + } - u1 = u2; - v1 = v2; + if (scale.x() < STREETNAME_THRESHOLD && property.labelType != CGarminTyp::eNone) { + QFont f(font); + switch (property.labelType) { + case CGarminTyp::eSmall: + f.setPointSize(font.pointSize() - 2); + break; + case CGarminTyp::eLarge: + f.setPointSize(font.pointSize() + 2); + break; + default:; } - if (scale.x() < STREETNAME_THRESHOLD && property.labelType != CGarminTyp::eNone) { - QFont f(font); - switch (property.labelType) { - case CGarminTyp::eSmall: - f.setPointSize(font.pointSize() - 2); - break; - case CGarminTyp::eLarge: - f.setPointSize(font.pointSize() + 2); - break; - default:; - } + collectText(item, poly, f, h, night ? property.colorLabelNight : property.colorLabelDay); + } - collectText(item, poly, f, h, - CMainWindow::self().isNight() ? property.colorLabelNight : property.colorLabelDay); + // Lay the pixmap along each straight segment of the polyline. The arc-length + // breakpoints used to coincide with the polyline vertices, so we can index + // the vertices directly instead of re-walking a QPainterPath. + for (int i = 0; i + 1 < size; ++i) { + const qreal segLength = lengths.at(i); + if (segLength < 1.0) { + continue; } - path.addPolygon(poly); - const int nLength = lengths.count(); - - qreal curLength = 0; - QPointF p2 = path.pointAtPercent(curLength / totalLength); - for (int i = 0; i < nLength; ++i) { - qreal segLength = lengths.at(i); + const QPointF& p1 = poly[i]; + const QPointF& p2 = poly[i + 1]; + const double angle = std::atan2(p2.y() - p1.y(), p2.x() - p1.x()); - // qDebug() << curLength << totalLength << curLength / totalLength; - - QPointF p1 = p2; - p2 = path.pointAtPercent((curLength + segLength) / totalLength); - qreal angle = qAtan((p2.y() - p1.y()) / (p2.x() - p1.x())) * 180 / M_PI; - - if (p2.x() - p1.x() < 0) { - angle += 180; - } - - p.save(); - p.translate(p1); - p.rotate(angle); - p.drawImage(0, -h / 2, img2line(pixmap, segLength)); - // imageCount++; - - p.restore(); - curLength += segLength; + const QImage seg = img2line(pixmap, segLength); + BLImage segBL; + if (segBL.create_from_data(seg.width(), seg.height(), BL_FORMAT_PRGB32, const_cast(seg.constBits()), + seg.bytesPerLine(), BL_DATA_ACCESS_READ) != BL_SUCCESS) { + continue; } + + ctx.save(); + ctx.translate(p1.x(), p1.y()); + ctx.rotate(angle); + ctx.blit_image(BLPoint(0.0, -h / 2.0), segBL); + ctx.restore(); } } } else { - if (property.hasBorder) { - // draw background line 1st - p.setPen(CMainWindow::self().isNight() ? property.penBorderNight : property.penBorderDay); - - QList::const_iterator it = dict[type].constBegin(); - for (; it != dict[type].constEnd(); ++it) { - // borderCount++; - drawLine(p, lines[*it], property, font, scale); - } - // draw foreground line in a second run for nicer borders - } else { - p.setPen(CMainWindow::self().isNight() ? property.penLineNight : property.penLineDay); - - QList::const_iterator it = dict[type].constBegin(); - for (; it != dict[type].constEnd(); ++it) { - // normalCount++; - drawLine(p, lines[*it], property, font, scale); - } + // First run: the background (border) line for bordered types, otherwise the + // line itself. Either way the labels are collected here. + const QPen& pen = property.hasBorder ? (night ? property.penBorderNight : property.penBorderDay) + : (night ? property.penLineNight : property.penLineDay); + const bool stroke = applyPen(ctx, pen); + const int lineWidth = pen.width(); + + for (quint32 idx : indices) { + drawLine(ctx, lines[idx], stroke, lineWidth, property, font, scale); } } } - // 2nd run to draw foreground lines. - props = polylineProperties.begin(); - for (; props != end; ++props) { + // 2nd run to draw the foreground lines over their borders. + for (props = polylineProperties.begin(); props != end; ++props) { const quint32& type = props.key(); const CGarminTyp::polyline_property& property = props.value(); - if (dict[type].isEmpty()) { + const QList& indices = dict[type]; + if (indices.isEmpty()) { continue; } if (property.hasBorder && !property.hasPixmap) { - // draw foreground line 2nd - p.setPen(CMainWindow::self().isNight() ? property.penLineNight : property.penLineDay); - - QList::const_iterator it = dict[type].constBegin(); - for (; it != dict[type].constEnd(); ++it) { - drawLine(p, lines[*it]); + const QPen& pen = night ? property.penLineNight : property.penLineDay; + if (applyPen(ctx, pen)) { + for (quint32 idx : indices) { + drawLine(ctx, lines[idx]); + } } } } - - // qDebug() << "pixmapCount:" << pixmapCount - // << "borderCount:" << borderCount - // << "normalCount:" << normalCount - // << "imageCount:" << imageCount - // << "deletedCount:" << deletedCount; } -void CMapIMG::drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polyline_property& property, const QFont& font, - const QPointF& scale) { +void CMapIMG::drawLine(BLContext& ctx, CGarminPolygon& l, bool stroke, int lineWidth, + const CGarminTyp::polyline_property& property, const QFont& font, const QPointF& scale) { ZoneScoped; QPolygonF& poly = l.pixel; const int size = poly.size(); - const int lineWidth = p.pen().width(); if (size < 2) { return; @@ -1747,8 +1888,6 @@ void CMapIMG::drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polylin map->convertRad2Px(poly); - // simplifyPolyline(line); - if (scale.x() < STREETNAME_THRESHOLD && property.labelType != CGarminTyp::eNone) { QFont f(font); switch (property.labelType) { @@ -1765,21 +1904,19 @@ void CMapIMG::drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polylin CMainWindow::self().isNight() ? property.colorLabelNight : property.colorLabelDay); } - p.drawPolyline(poly); + if (stroke) { + ctx.stroke_path(polyToPath(poly, false)); + } } -void CMapIMG::drawLine(QPainter& p, const CGarminPolygon& l) { +void CMapIMG::drawLine(BLContext& ctx, const CGarminPolygon& l) { ZoneScoped; const QPolygonF& poly = l.pixel; - const int size = poly.size(); - - if (size < 2) { + if (poly.size() < 2) { return; } - // simplifyPolyline(poly); - - p.drawPolyline(poly); + ctx.stroke_path(polyToPath(poly, false)); } void CMapIMG::collectText(const CGarminPolygon& item, const QPolygonF& line, const QFont& font, qint32 lineWidth, @@ -1838,33 +1975,31 @@ void CMapIMG::addLabel(const CGarminPoint& pt, const QRect& rect, const CGarminT strlbl.isNight = isNight; } -void CMapIMG::drawPoints(QPainter& p, pointtype_t& pts, QVector& rectPois) { +void CMapIMG::drawPoints(BLContext& ctx, pointtype_t& pts, QVector& rectPois) { ZoneScoped; + const bool night = CMainWindow::self().isNight(); pointtype_t::iterator pt = pts.begin(); while (pt != pts.end()) { map->convertRad2Px(pt->pos); const CGarminTyp::point_property& property = pointProperties[pt->type]; - const QImage& icon = CMainWindow::self().isNight() ? property.imgNight : property.imgDay; + const QImage& icon = night ? property.imgNight : property.imgDay; const QSizeF& size = icon.size(); if (isCluttered(rectPois, QRectF(pt->pos, size))) { if (size.width() <= 8 && size.height() <= 8) { - p.drawImage(pt->pos.x() - (size.width() / 2), pt->pos.y() - (size.height() / 2), icon); + blitQImage(ctx, pt->pos.x() - (size.width() / 2), pt->pos.y() - (size.height() / 2), icon); } else { - p.drawPixmap(pt->pos.x() - 4, pt->pos.y() - 4, QPixmap(":/icons/8x8/bullet_blue.png")); + blitBullet(ctx, pt->pos.x() - 4, pt->pos.y() - 4); } ++pt; continue; } - bool showLabel = true; - - p.drawImage(pt->pos.x() - (size.width() / 2), pt->pos.y() - (size.height() / 2), icon); - showLabel = property.labelType != CGarminTyp::eNone; + blitQImage(ctx, pt->pos.x() - (size.width() / 2), pt->pos.y() - (size.height() / 2), icon); - if (CMainWindow::self().isPoiText() && showLabel) { + if (CMainWindow::self().isPoiText() && property.labelType != CGarminTyp::eNone) { // calculate bounding rectangle with a border of 2 px QRect rect = fm.boundingRect(pt->labels.join(" ")); rect.adjust(0, 0, 4, 4); @@ -1872,32 +2007,33 @@ void CMapIMG::drawPoints(QPainter& p, pointtype_t& pts, QVector& rectPoi // if no intersection was found, add label to list if (!intersectsWithExistingLabel(rect)) { - addLabel(*pt, rect, property, CMainWindow::self().isNight()); + addLabel(*pt, rect, property, night); } } ++pt; } } -void CMapIMG::drawPois(QPainter& p, pointtype_t& pts, QVector& rectPois) { +void CMapIMG::drawPois(BLContext& ctx, pointtype_t& pts, QVector& rectPois) { ZoneScoped; + const bool night = CMainWindow::self().isNight(); for (CGarminPoint& pt : pts) { map->convertRad2Px(pt.pos); const CGarminTyp::point_property& property = pointProperties[pt.type]; - const QImage& icon = CMainWindow::self().isNight() ? property.imgNight : property.imgDay; + const QImage& icon = night ? property.imgNight : property.imgDay; const QSizeF& size = icon.size(); if (isCluttered(rectPois, QRectF(pt.pos, size))) { if (size.width() <= 8 && size.height() <= 8) { - p.drawImage(pt.pos.x() - (size.width() / 2), pt.pos.y() - (size.height() / 2), icon); + blitQImage(ctx, pt.pos.x() - (size.width() / 2), pt.pos.y() - (size.height() / 2), icon); } else { - p.drawPixmap(pt.pos.x() - 4, pt.pos.y() - 4, QPixmap(":/icons/8x8/bullet_blue.png")); + blitBullet(ctx, pt.pos.x() - 4, pt.pos.y() - 4); } continue; } - p.drawImage(pt.pos.x() - (size.width() / 2), pt.pos.y() - (size.height() / 2), icon); + blitQImage(ctx, pt.pos.x() - (size.width() / 2), pt.pos.y() - (size.height() / 2), icon); if (CMainWindow::self().isPoiText()) { // calculate bounding rectangle with a border of 2 px @@ -1907,7 +2043,7 @@ void CMapIMG::drawPois(QPainter& p, pointtype_t& pts, QVector& rectPois) // if no intersection was found, add label to list if (!intersectsWithExistingLabel(rect)) { - addLabel(pt, rect, property, CMainWindow::self().isNight()); + addLabel(pt, rect, property, night); } } } diff --git a/src/qmapshack/map/CMapIMG.h b/src/qmapshack/map/CMapIMG.h index 4ae1766d8..38fc8eb33 100644 --- a/src/qmapshack/map/CMapIMG.h +++ b/src/qmapshack/map/CMapIMG.h @@ -30,6 +30,7 @@ class CMapDraw; class CFileExt; class IGarminStrTbl; +class BLContext; typedef QVector polytype_t; typedef QVector pointtype_t; @@ -161,24 +162,27 @@ class CMapIMG : public IMap { void processPrimaryMapData(); void readFile(CFileExt& file, quint32 offset, quint32 size, QByteArray& data); void loadVisibleData(bool fast, polytype_t& polygons, polytype_t& polylines, pointtype_t& points, pointtype_t& pois, - unsigned level, const QRectF& viewport, QPainter& p); + unsigned level, const QRectF& viewport, BLContext& ctx); void loadSubDiv(CFileExt& file, const subdiv_desc_t& subdiv, IGarminStrTbl* strtbl, const QByteArray& rgndata, bool fast, const QRectF& viewport, polytype_t& polylines, polytype_t& polygons, pointtype_t& points, pointtype_t& pois); bool intersectsWithExistingLabel(const QRect& rect) const; void addLabel(const CGarminPoint& pt, const QRect& rect, const CGarminTyp::point_property& property, bool isDay); - void drawPolygons(QPainter& p, polytype_t& lines); - void drawPolylines(QPainter& p, polytype_t& lines, const QPointF& scale); - void drawPoints(QPainter& p, pointtype_t& pts, QVector& rectPois); - void drawPois(QPainter& p, pointtype_t& pts, QVector& rectPois); + void drawPolygons(BLContext& ctx, polytype_t& lines); + void drawPolylines(BLContext& ctx, polytype_t& lines, const QPointF& scale); + void drawPoints(BLContext& ctx, pointtype_t& pts, QVector& rectPois); + void drawPois(BLContext& ctx, pointtype_t& pts, QVector& rectPois); + // Text and labels stay on QPainter; Qt's font handling has no Blend2D equivalent + // and they are a negligible fraction of the total rendering time. void drawLabels(QPainter& p, const QVector& lbls); void drawText(QPainter& p); - void drawLine(QPainter& p, CGarminPolygon& l, const CGarminTyp::polyline_property& property, const QFont& font, const QPointF& scale); - void drawLine(QPainter& p, const CGarminPolygon& l); + void drawLine(BLContext& ctx, CGarminPolygon& l, bool stroke, int lineWidth, + const CGarminTyp::polyline_property& property, const QFont& font, const QPointF& scale); + void drawLine(BLContext& ctx, const CGarminPolygon& l); - void collectText(const CGarminPolygon& item, const QPolygonF& line, const QFont& font, - qint32 lineWidth, const QColor& color); + void collectText(const CGarminPolygon& item, const QPolygonF& line, const QFont& font, qint32 lineWidth, + const QColor& color); void getInfoPoints(const pointtype_t& points, const QPoint& pt, QMultiMap& dict) const; void getInfoPolylines(const QPoint& pt, QMultiMap& dict) const;