Skip to content

Replace QPainter with Blend2D in CMapIMG#1131

Draft
jmlzr wants to merge 9 commits into
Maproom:devfrom
jmlzr:QMS-1130
Draft

Replace QPainter with Blend2D in CMapIMG#1131
jmlzr wants to merge 9 commits into
Maproom:devfrom
jmlzr:QMS-1130

Conversation

@jmlzr

@jmlzr jmlzr commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What is the linked issue for this pull request:

QMS-#1130

What you have done:

Added Tracy as a profiler and instrumented the IMG rendering to look for where to focus my performance optimization efforts. That showed that Qt's rasterizer is the bottleneck that will dwarf any gains that could be made in the code. With the biggest bottlenecks being drawPolylines by and followed by drawPolygons.
So I decided to let Claude replace Qt's very slow rasterizer with Blend2D which is probably the fastest software rasterizer/vector graphics library in existence.

The new code seems to result in the same output but runs ~3 times faster!

Performance comparison

To make a comparison I recorded traces of me panning and zooming around the same area before and after the commit that switched to Blend2D and then used Tracy's compare feature.

drawPolylines saw a massive 5x improvement, dropping from 569ms mean to 117ms:

image

drawPolygons saw a also impressive 2.5x improvement, from 170ms to 67ms:

image

Looking at draw is deceiving as the results of that are diluted by early exists from redraw requests.

Render output

I took comparison screenshots before of the same view before and after, map used is a Freizeitkarte IMG.

Before:

qms-1130-kufstein-qt

After:

qms-1130-kufstein-blend2d

As you can see there is virtually no visible difference! The only thing I can notice on this view is the POIs shifting a little but they seem to shift by a similar amount with the previous renderer as well when panning minimally so I'm not sure if that is even a regression.

Next steps

Since this is a rather large change and I haven't checked if Claude did stupid things I made this just a draft for now. I do encourage people to try it out though!

I think now that the biggest bottleneck is gone I could also squeeze out probably at least another ~20% from optimizing the rest of the code.

Btw the massive amount of ISO C++ prohibits anonymous structs warnings comes from the fact that our CMakeLists.txt unfortunately sets global compile options with add_compile_options that then affects subprojects as well. The better thing would be to set them only for our own targets with target_compile_options. But we can fix that if we get close to merging this, for now I didn't want to bother with that.

Steps to perform a simple smoke test:

Open a IMG.

Tracy

If anyone wants to play with Tracy:

  1. Reconfigure QMS with -DTRACY_ENABLE=1 -DTRACY_ON_DEMAND=1
  2. Build Tracy, in the QMS build dir do:
cd _deps/tracy-src
cmake -B profiler/build -S profiler -DCMAKE_BUILD_TYPE=Release
cmake --build profiler/build --config Release -j$(nproc)
  1. Start Tracy from the same dir with ./profiler/build/tracy-profiler and connect to the running QMS instance.

Does the code comply to the coding rules and naming conventions Coding Guidelines:

  • yes

Is every user facing string in a tr() macro?

  • yes

Did you add the ticket number and title into the changelog? Keep the numeric order in each release block.

  • yes, I didn't forget to change changelog.txt

No as only a draft for now.

@kiozen

kiozen commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

First quick impression:

  • I am naturally a bit uneasy about another huge dependency. But let's see what the package maintainers say.
  • On my fairly weak private laptop I hardly notice any big difference. Maybe because reading the data takes the most of the time. Or belnd2d can't get much out of the Intel graphics adapter. Can't tell

@jmlzr

jmlzr commented Jun 24, 2026 via email

Copy link
Copy Markdown
Contributor Author

@wthaem

wthaem commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

My attempt to build the PR jmlzr:QMS-1130 with my default CMake parameters failed on Windows 11:

FAILED: [code=2] src/qmaptool/CMakeFiles/qmaptool.dir/canvas/CCanvas.cpp.obj
C:\PROGRA~1\MICROS~1\18\COMMUN~1\VC\Tools\MSVC\1451~1.362\bin\Hostx64\x64\cl.exe  /nologo /TP -DAPPLICATION_NAME=qmaptool -DHELPPATH=doc/HTML -DQT_CORE_LIB -DQT_GUI_LIB -DQT_HELP_LIB -DQT_NETWORK_LIB -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_WIDGETS_LIB -DUNICODE -DVER_MAJOR=1 -DVER_MINOR=20 -DVER_STEP=3 -DVER_TWEAK="" -DWIN32 -DWIN64 -D_CRT_SECURE_NO_WARNINGS -D_ENABLE_EXTENDED_ALIGNED_STORAGE -D_UNICODE -D_WIN64 -ID:\QtProjects\QMS\QMS4Qt6\build-ninja\src\qmaptool -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool -ID:\QtProjects\QMS\QMS4Qt6\build-ninja\src\qmaptool\qmaptool_autogen\include -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool\. -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool\..\common -external:ID:\QtProjects\QMS\QMS4Qt6\build-ninja -external:Id:\QtProjects\QMS\GISInternals\1944_260614\release-1944-x64\include -external:Id:\QtProjects\QMS\GISInternals\1944_260614\release-1944-x64\include\proj9 -external:IC:\Qt\6.11.0\msvc2022_64\include\QtWidgets -external:IC:\Qt\6.11.0\msvc2022_64\include -external:IC:\Qt\6.11.0\msvc2022_64\include\QtCore -external:IC:\Qt\6.11.0\msvc2022_64\mkspecs\win32-msvc -external:IC:\Qt\6.11.0\msvc2022_64\include\QtGui -external:IC:\Qt\6.11.0\msvc2022_64\include\QtNetwork -external:IC:\Qt\6.11.0\msvc2022_64\include\QtHelp -external:IC:\Qt\6.11.0\msvc2022_64\include\QtSql -external:W0 /wd4996 /wd4101 /wd4005 /wd4003 /EHsc /O2 /Ob2 /DNDEBUG /Zi /GL -std:c++20 -MD /MP -Zc:__cplusplus -permissive- -utf-8 /showIncludes /Fosrc\qmaptool\CMakeFiles\qmaptool.dir\canvas\CCanvas.cpp.obj /Fdsrc\qmaptool\CMakeFiles\qmaptool.dir\ /FS -c D:\QtProjects\QMS\QMS4Qt6\src\qmaptool\canvas\CCanvas.cpp
D:\QtProjects\QMS\QMS4Qt6\src\qmaptool\canvas\CCanvas.cpp(22): fatal error C1083: Datei (Include) kann nicht geöffnet werden: "tracy/Tracy.hpp": No such file or directory
[234/606] Building CXX object src\qmaptool\CMakeFiles\qmaptool.dir\overlay\gridtool\CGridSelArea.cpp.obj
ninja: build stopped: subcommand failed.

There are lots of new CMAKE variables. I used their default settings. More info is certainly needed to build successfully on Windows.

Must there any additional packages be downloaded before building?

Sorry for the message layout - I copied it from the output log.

@jmlzr

jmlzr commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@wthaem Yes cmake will need to download a few packages via FetchContent. Can you check at the start of cmake output if that failed or something? If you run cmake directly it should normally already fail at that point though... If you had the build directory already before switching to this branch you could maybe try cmake with --fresh to force a reconfigure.

@kiozen

kiozen commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

same problem on a Linux machine. Even with cmake from scratch. The tracy header path is not set properly for the target.

@wthaem

wthaem commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

I'm starting the build always with cmake --fresh. Now, I deleted in a first step everything from the build directory, too. This changed the situation a bit: Some dependencies were obviously found (asmjit, blend2d, tracy). Now there appears -- Could NOT find rocprofiler-sdk (missing: rocprofiler-sdk_DIR) as new message. Configuring and generating is ok. But building fails again with a message similar to the one communicated yesterday:

FAILED: [code=2] src/qmaptool/CMakeFiles/qmaptool.dir/canvas/CCanvas.cpp.obj
C:\PROGRA~1\MICROS~1\18\COMMUN~1\VC\Tools\MSVC\1451~1.362\bin\Hostx64\x64\cl.exe  /nologo /TP -DAPPLICATION_NAME=qmaptool -DHELPPATH=doc/HTML -DQT_CORE_LIB -DQT_GUI_LIB -DQT_HELP_LIB -DQT_NETWORK_LIB -DQT_NO_DEBUG -DQT_SQL_LIB -DQT_WIDGETS_LIB -DUNICODE -DVER_MAJOR=1 -DVER_MINOR=20 -DVER_STEP=3 -DVER_TWEAK="" -DWIN32 -DWIN64 -D_CRT_SECURE_NO_WARNINGS -D_ENABLE_EXTENDED_ALIGNED_STORAGE -D_UNICODE -D_WIN64 -ID:\QtProjects\QMS\QMS4Qt6\build-ninja\src\qmaptool -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool -ID:\QtProjects\QMS\QMS4Qt6\build-ninja\src\qmaptool\qmaptool_autogen\include -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool\. -ID:\QtProjects\QMS\QMS4Qt6\src\qmaptool\..\common -external:ID:\QtProjects\QMS\QMS4Qt6\build-ninja -external:Id:\QtProjects\QMS\GISInternals\1944_260614\release-1944-x64\include -external:Id:\QtProjects\QMS\GISInternals\1944_260614\release-1944-x64\include\proj9 -external:IC:\Qt\6.11.0\msvc2022_64\include\QtWidgets -external:IC:\Qt\6.11.0\msvc2022_64\include -external:IC:\Qt\6.11.0\msvc2022_64\include\QtCore -external:IC:\Qt\6.11.0\msvc2022_64\mkspecs\win32-msvc -external:IC:\Qt\6.11.0\msvc2022_64\include\QtGui -external:IC:\Qt\6.11.0\msvc2022_64\include\QtNetwork -external:IC:\Qt\6.11.0\msvc2022_64\include\QtHelp -external:IC:\Qt\6.11.0\msvc2022_64\include\QtSql -external:W0 /wd4996 /wd4101 /wd4005 /wd4003 /EHsc /O2 /Ob2 /DNDEBUG /Zi /GL -std:c++20 -MD /MP -Zc:__cplusplus -permissive- -utf-8 /showIncludes /Fosrc\qmaptool\CMakeFiles\qmaptool.dir\canvas\CCanvas.cpp.obj /Fdsrc\qmaptool\CMakeFiles\qmaptool.dir\ /FS -c D:\QtProjects\QMS\QMS4Qt6\src\qmaptool\canvas\CCanvas.cpp
D:\QtProjects\QMS\QMS4Qt6\src\qmaptool\canvas\CCanvas.cpp(22): fatal error C1083: Datei (Include) kann nicht geöffnet werden: "tracy/Tracy.hpp": No such file or directory
[320/831] Building CXX object src\qmaptool\CMakeFiles\qmaptool.dir\overlay\gridtool\CGridSelArea.cpp.obj
ninja: build stopped: subcommand failed.

The file Tracy.hpp can be found in the directory ...\build-ninja\_deps\tracy-src\public\tracy\Tracy.hpp.

Edit: ChatGPT info: "Does ROCprofiler-SDK exist on Windows?

Officially: no, not as a supported Windows package."

How to switch off the use of tracy?

My settings:

TRACY_ENABLE:BOOL=OFF

TRACY_ON_DEMAND:BOOL=OFF

@jmlzr

jmlzr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

I'm sorry I should have read your error message more carefully! Turns out I accidentally added an include to a qmaptool file (and qmaptool is not linked with Tracy). Since I always just built the qmapshack target I did not notice.

I fixed that by removing the include and confirmed that now all our targets build successfully.

You can ignore that missing rocprofiler. That's just an optional dependency of Tracy we don't need.

@wthaem

wthaem commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Now building is ok and nothing special noticed with a quick first test (KMz now again without blurry).

@jmlzr

jmlzr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@wthaem did you maybe comment on the wrong PR? Because the blurry KMZ maps was something you noted in #1129 so it won't affect this branch yeah...

Did you try IMG maps? Noticed a performance improvement with those? Or visual issues?

@wthaem

wthaem commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Sure: the KMZ topic belongs to the other PR. But I noticed again the difference in quality with this PR.

I tried IMG maps and many types of raster maps. Loading, zooming, panning of IMGs was pretty quick. Image quality on my notebook screen quite good. My notebook has a good CPU!

@JFritzle

Copy link
Copy Markdown
Contributor

Currently, I am trying to build on macOS. I get tons of warnings like these:

/Users/user/qms1130/build_QMapShack/_deps/blend2d-src/blend2d/compression/deflatedecoder_p.h:281:5: warning: anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
  281 |     struct {
      |     ^
/Users/user/qms1130/build_QMapShack/_deps/blend2d-src/blend2d/compression/deflatedecoder_p.h:285:5: warning: anonymous structs are a GNU extension [-Wgnu-anonymous-struct]
  285 |     struct {
      |     ^
/Users/user/qms1130/build_QMapShack/_deps/blend2d-src/blend2d/compression/deflatedecoder_p.h:281:5: warning: anonymous types declared in an anonymous union are an extension [-Wnested-anon-types]
  281 |     struct {
      |     ^
/Users/user/qms1130/build_QMapShack/_deps/blend2d-src/blend2d/compression/deflatedecoder_p.h:285:5: warning: anonymous types declared in an anonymous union are an extension [-Wnested-anon-types]
  285 |     struct {
      |     ^

Might it be possible to expand cmake configuration not to throw these warnings?

@jmlzr

jmlzr commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

@JFritzle Pretty sure that's the same issue I already described in the PR description and Apple's clang just uses a different wording for that warning then GCC:

Btw the massive amount of ISO C++ prohibits anonymous structs warnings comes from the fact that our CMakeLists.txt unfortunately sets global compile options with add_compile_options that then affects subprojects as well. The better thing would be to set them only for our own targets with target_compile_options. But we can fix that if we get close to merging this, for now I didn't want to bother with that.

@JFritzle

Copy link
Copy Markdown
Contributor

Did not notice a performance improvement with map Reit_Wanderkarte_Italia_11_25.img on my Windows 11 PC:
Intel Core i5-2400 CPU, 4 logical processors, 22 GB RAM.

@kiozen

kiozen commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Haven't had the guts to deal with Tracy yet. But my hands-on experience is similar. On my high performance work laptop I can notice a small difference in performance. On my old private laptop no noticeable gain.

But on both machines I have to increase the details level quite a lot to get a noticeable lag. More than I usually would do. So the benefit is on the lower side

I am a bit undecided. Pulling in new dependencies is always a risk and makes me feel uneasy. On the other hand side I highly appreciate and acknowledge every work donated to QMapShack. This places me in between chairs. It's always a hard decision based on the estimated future of the feature. Will it cause more benefit than future trouble?

There is already quite some stuff I would like to get rid of (alglib, quazip) either by kicking out the feature or replacing it with native code. Dropping features is always trouble because of this one user popping up actually using it. And replacing code is another problem on it's own. For example what happens if I have to decide that blend2d can't be used anymore for whatever reason. I have to revert to Qt. Users might notice the performance loss and complain. Restoring the former code is possible. But might be more trouble than expected.

So there must be a benefit worth it. For example replacing the QMapShack native code with the Garmin SDK was a tough decision, too. Apart from a not 100% clarified license situation, it's now depending on Garmin's lib. On the other hand side, the native code was done ages ago by someone not reachable any more. The newer devices implement new features that require to actually notice the change (reported by users), understanding the rather complex code and fixing the stuff without breaking something. To get rid of this maintenance burden picking the Gamrin Lib was a choice.

So long story short: I am still undecided. Even after writing all of this.

@jmlzr

jmlzr commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

That's very strange. I guess you guys must be either using maps only in areas with very little features or have screens with low resolution if you don't have noticeable lag with the previous renderer.
On a 3840x2160 screen and with a AMD 5600G with details on 0 already the map feels sluggish on areas without that much going on. On areas with much going on - worst case a big city - it get very annoying (>=1s per update). If I turn details up it gets pretty much unusable.
I checked a Wanderreitkarte and that has even worse performance than a Freizeitkarte.

@kiozen I don't get your point tbh. If you ever wanna get rid of Blend2D you can just revert. It will only be used in the IMG renderer and that one is unmaintained since a while anyways, so not even like there is much of a chances of there being changes that then might need back porting. Most importantly: Why would a user complaining about performance in the future after a hypothetically possible revert matter more than one complaining now about current performance?

@kiozen

kiozen commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Tested on a 2560x1440 screen. With Freizeitkarte and a big City like Rome. I am not really that much of a fan of very large screens. I appreciate multiple (cheap) screens. But that's just personal taste.

I passed the problem of a speed up vs dependency to Claude, asking if a performance gain could be achieved with Qt, too.

This is the first answer. Of course this has to be taken with a grain of salt. This is not a proven enhancement with measured performance gain. But it seems to be interesting enough to give it a try. Let's see how much development time I can spare on that the next week.

PR #1131 — Replace QPainter with Blend2D in CMapIMG

The PR substitutes Blend2D for Qt's software rasterizer in CMapIMG's geometry pipeline (drawPolygons, drawPolylines, drawPoints, drawPois) while keeping text on QPainter. The benchmark numbers are real (5× polyline, 2.5× polygon), but the implementation has several correctness gaps and the core architectural question — is Blend2D the right fix — deserves scrutiny.

Findings:

  1. applyPen silently drops standard Qt dash styles → solid-line regression
    applyPen handles only Qt::SolidLine and Qt::CustomDashLine. Qt::DashLine, Qt::DotLine, Qt::DashDotLine, Qt::DashDotDotLine fall through and Blend2D gets no dash array, so all pre-defined dashed road/area-border types render as solid lines. Garmin TYP files use these heavily. This is a visible rendering regression on any map tile that uses preset pen styles.
  2. Format_ARGB32 premultiply round-trip is lossy
    draw() calls buf.image.convertTo(QImage::Format_ARGB32_Premultiplied) at entry and convertTo(QImage::Format_ARGB32) at exit. If any previous layer already painted semi-transparent content into buf.image, the multiply-then-divide round-trip loses low bits of the RGB channels. The correct fix is to allocate buf.image as Format_ARGB32_Premultiplied from the start (IDrawContext.cpp:109,113) so no in-place conversion is needed — this also speeds up all other layers' QPainter work.
  3. brushToTile creates a new QImage per polygon per frame for textured fills
    Hatch/texture brushes call brushToTile() on every drawPolygon() invocation. For a lake or forest area, this allocates and renders a tile image on every frame. A per-type cache keyed on the brush would avoid all but the first allocation.
  4. polyToPath has undefined behaviour on empty polygons
    poly[0] is accessed unconditionally before the loop that iterates segments. A zero-point polygon (degenerate data in some Garmin tiles) causes UB.

Can Qt alone close most of this gap?

Yes — significantly. The Blend2D speedup is real, but it's partly chasing the symptom (slow rasterizer) while some of the root causes are algorithmic:

  1. The pointAtPercent O(n²) problem — biggest single win
    In drawPolylines (line 1617–1676), the pixmap-along-line code builds a QPainterPath from the polygon just to call path.pointAtPercent(t) for each segment. Each pointAtPercent() call walks the full path from the start to measure arc length — O(n) per call, O(n²) total. The PR itself fixes this by using poly[i]/poly[i+1] directly with std::atan2. This fix requires zero new dependencies and is likely where most of the "5× drawPolylines" comes from for longer roads. It can be cherry-picked without Blend2D.

  2. Commented-out simplifyPolyline() — potentially even bigger
    Lines 1546, 1611, 1743, 1772 all have // simplifyPolyline(...) commented out. If that function implements Douglas-Peucker with a ~0.5 px threshold, enabling it would reduce vertex counts by 70–90% at typical zoom levels, which speeds up both convertRad2Px() and everything QPainter does downstream. Do you know why it was disabled?

  3. Buffer format: Format_ARGB32 → Format_ARGB32_Premultiplied
    IDrawContext.cpp:109,113 allocates the draw buffer as Format_ARGB32. QPainter compositing onto premultiplied surfaces skips a per-pixel multiply step — the Qt docs explicitly note this as the fast path. Switching these two lines benefits every map layer, not just CMapIMG, at zero cost. (This also fixes the PR's lossy round-trip if Blend2D is ever merged.)

  4. p.save()/translate/rotate/drawImage/p.restore() per pixmap segment
    The pixmap-along-line loop does 5 QPainter calls per road segment. Computing the rotation transform directly (QTransform::fromTranslate + rotate) and calling p.setTransform() once, or better, pre-rotating the pixmap tile and using drawImage(QPointF, ...) with no transform, removes the save/restore overhead.

  5. Batching drawPolygon per type
    drawPolygons calls p.drawPolygon(poly) individually for every polygon of the same type (same pen+brush). Accumulating them into a single QPainterPath with addPolygon() and calling p.fillPath() / p.drawPath() once per type reduces QPainter state machine overhead.

Bottom line: Items 1–3 alone could realistically recover 50–70% of Blend2D's measured gain with no new dependency, no premultiply round-trip hazard, and no hybrid-API surface to maintain. I'd suggest doing those first (especially re-enabling simplification if it's safe), profiling again, and then deciding whether the remaining gap justifies pulling in Blend2D.


@jmlzr

jmlzr commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

A grain of salt is underselling it. You have to be really careful with letting Claude think where it can make performance gains. It's wrong a lot of time cause it misjudges the costs of things. For example I can already tell you that this pixel format conversion is way too cheap to matter. Also I wanted to look into if changing the format of the double buffers in IDrawContext would be possible without regressions, as even the Qt docs say that for composition work the premultiplied is preferred.

I'm very doubtful on the other points as well as I spent quite some time before the port to Blend2D with letting it optimize the Qt renderer and I got to only around 20%. Plus I could see in Tracy that almost all time was spent in Qt's rasterizer. If you look at benchmarks you will also see that Qt is just very very slow: https://blend2d.com/performance.html

Maybe seeing the Blend2D port gave it ideas that indeed would make a difference with Qt so please try it if you'd like but I'd be very surprised if it got anywhere close.

I found that Claude gets way better at performance optimization if you let it instrument all it's suspicions and run the program with Tracy recording it. Then you ether tell it what the expensive zones are or even just feed it tracy-csvexport output and let it implement what it things makes most sense from that. Then you do another Tracy run to check if you actually gained anything and guide the next optimization step. Loop as long as you wish.

I have a few more commits for this branch local that I still need to review and clean up before I push. Got some more performance out of it, made some refactoring plus I fixed that bug (that was in the original Qt version and been bugging me since forever) of patterned fills shifting around.

@kiozen

kiozen commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Ok, agreed. Fixing it on the Qt side does not hit the same performance by quite a gap compared to Blend2d. So the only option to speed up rendering is to rely on non Qt code. Go ahead.

jmlzr added 9 commits June 30, 2026 13:48
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.
Dramatically speed up IMG rendering by replacing the very slow Qt vector
engine with the much faster Blend2D for all but text rendering.
Both classes declared `virtual ~...() = default;` but are never used
polymorphically. That user-declared destructor added a vtable pointer to
every object and suppressed the implicit (noexcept) move constructor /
assignment, so the QVector<CGarminPolygon>/<CGarminPoint> buffers used during
RGN decode could only grow by copying their QPolygonF/QStringList members.

Removing the destructor (rule of zero) restores move relocation and drops the
vtable. No behaviour change.
- Replace C-style casts with reinterpret_cast/static_cast in readFile,
  readBasics, readSubfileBasics and loadSubDiv.
- Make the raw RGN data pointers (pRawData, pOffset) const-correct.
- Hoist the repeated subfile.parts["TRE"]/["RGN"]/["LBL"]/["NET"] map lookups
  into single bound references instead of re-querying the QMap per offset.
- Header: typedef -> using for polytype_t/pointtype_t; collapse
  map_level_t::operator== to a single boolean expression.
- getToolTip: fold the two identical "join dict values, skip the - placeholder"
  loops into a single appendValues lambda.
- Replace the STREETNAME_THRESHOLD macro with a typed, documented
  constexpr qreal kStreetNameThreshold.
- drawPolygons: bucket polygon indices by type once and walk polygonDrawOrder
  hitting only buckets that exist, replacing the O(types x N) scan that
  re-iterated the whole polygon list once per draw-order entry.
- drawPolygons/drawPoints/drawPois: look up the property maps with constFind
  and a static-default fallback instead of non-const operator[], which silently
  inserted a default entry for every missing type on every frame (mutating
  shared state and polluting tooltip lookups). drawPolylines skips absent types
  via constFind too.
- Submit geometry to Blend2D in bulk: QPointF is layout-compatible with BLPoint
  (static_assert), so toBLPoints() feeds a QPolygonF straight to fill_polygon /
  stroke_polygon / stroke_polyline. Drops the per-line BLPath allocation and the
  vertex-by-vertex polyToPath helper.
- Modernize the (disabled) debug border blocks to the current Blend2D API.
Profiling showed CMapIMG::draw spends ~44% in loadVisibleData and ~20% in
drawPolylines. Add named Tracy zones to localize the cost inside those:

- loadSubDiv, CGarminPolygon::decode/decode2, CGarminPoint::decode/decode2
  -> isolate geometry decode.
- strtbl.get (CGarminStrTbl6/8/Utf8::get) -> isolate label lookup (an mmap +
  codepage decode per labelled object, much of it wasted when zoomed out).
- readFile.RGN -> separate the per-frame RGN read/XOR from decode.
- isCompletelyOutside, img2line, polyline.pixmap -> isolate culling and the
  per-segment pixmap-along-line work.
- IDrawContext::convertRad2Px(QPolygonF&) -> PROJ conversion share; the
  QPointF overload already had a zone, the polygon overload did not.
Tracy showed label decoding was the largest slice of loadVisibleData: every
labelled object triggered an mmap (via CFileExt::data) + a codepage decode in
IGarminStrTbl, redone every frame although label content is immutable.

Turn IGarminStrTbl::get() into a non-virtual wrapper over a bounded LRU
QCache<quint64, QStringList> (key (type << 32) | offset); on a miss it delegates
to a new protected virtual decode() - the renamed per-coding get() of
CGarminStrTbl6/8/Utf8. A hit needs no mmap or decode, so it also shrinks the
per-subfile CFileExt::free() munmap loop. QCache evicts least-recently-used
entries, so memory stays bounded while panning very large gmapsupp files. The
cache is only touched from the (serialized) draw thread, so it needs no lock.

Tracy: strtbl.get now measures the wrapper (hits+misses), strtbl.decode the
miss path only - their call-count ratio reads out the hit rate.
convertRad2Px/convertPx2Rad map rad<->px as (proj(p) - focusInMetres) /
(scale*zoomFactor) + center, and reprojected the focus on every call - 2.5M+
times per draw on a dense vector map (Tracy: ~13% of a draw, spread across
drawPolygons + drawPolylines).

- Cache focus-in-metres in IDrawContext::focusInM, computed once in draw() right
  after focus is set (its only write site) and reused by all three conversions.
  Recomputed every draw, so setProjection is handled for free.
- Drop the per-call QVector<p_t> allocation in the polygon overload: the rare
  antimeridian wrap-fixes now go into a stack QVarLengthArray, and scale*zoomFactor
  is hoisted out of the per-point loop.

Behaviour-preserving for every caller (GIS items, DEM, overlays, ...): focusInM
equals the old per-call convertRad2M(focus), and the antimeridian check is kept
verbatim (it tests y, never fires, left as-is rather than "fixed").
Anchor the pattern origin in global coordinates so the patterns are
independent of the viewport and it's position.
@jmlzr

jmlzr commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on current dev and added most of the commits I mentioned that I had by now reviewed and cleaned up.

I don't really like the cache focus-in-metres in IDrawContext for convertRad2Px commit, might remove that again. Also gave only 1-2% performance. convertRad2Px really needs a refactor though. It most likely has a bug too: It checks the y axis for wrap-around but then applies the fix to the x axis. (that bug exists in current dev and this commit preserves that behavior)

@kiozen

kiozen commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

If you change anything in those central methods like convertRad2Px please do it in it's own PR. I want this completely isolated from anything else to revert it easily if anything breaks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants