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 db1524b41..baff8e0bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,50 @@ 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) + +############################################################################################### +# 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 1fc263b84..4af37c76c 100644 --- a/src/qmapshack/CMakeLists.txt +++ b/src/qmapshack/CMakeLists.txt @@ -1035,6 +1035,8 @@ target_link_libraries(${APPLICATION_NAME} ${ALGLIB_LIBRARIES} QuaZip::QuaZip GarminFit + Tracy::TracyClient + blend2d::blend2d ) 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..b4153ee69 100644 --- a/src/qmapshack/map/CMapIMG.cpp +++ b/src/qmapshack/map/CMapIMG.cpp @@ -18,8 +18,12 @@ #include "map/CMapIMG.h" +#include + #include #include +#include +#include #include "CMainWindow.h" #include "canvas/CCanvas.h" @@ -115,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), @@ -1098,6 +1244,7 @@ quint8 CMapIMG::scale2bits(const QPointF& scale) { void CMapIMG::draw(IDrawContext::buffer_t& buf) /* override */ { + ZoneScoped; if (map->needsRedraw()) { return; } @@ -1108,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(); @@ -1150,63 +1287,82 @@ 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); if (!file.open(QIODevice::ReadOnly)) { @@ -1264,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 } @@ -1283,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 @@ -1526,13 +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) { @@ -1540,22 +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); @@ -1563,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) { @@ -1582,157 +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; @@ -1740,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) { @@ -1758,20 +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, @@ -1830,32 +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); @@ -1863,31 +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 @@ -1897,13 +2043,14 @@ 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); } } } } 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 +2063,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)) { 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;