Skip to content

Feature/mvt integration#46

Draft
rkreienbuehl wants to merge 73 commits into
p-lr:feature/mvt-integrationfrom
rkreienbuehl:feature/mvt-integration
Draft

Feature/mvt integration#46
rkreienbuehl wants to merge 73 commits into
p-lr:feature/mvt-integrationfrom
rkreienbuehl:feature/mvt-integration

Conversation

@rkreienbuehl

Copy link
Copy Markdown
Contributor

@p-lr here the new MR draft of my work.

Rasterizer is still very slow on all devices (desktop seemed fast at first, but depends on the hardware), performance needs to be drastically improved, maybe you have some ideas what could bring better performance? One idea is to draw the symbols instead of using markers, but rendering itself also needs to be improved. I will investigate this further. Maybe you also have some inputs on how to improve performance?
I could fix the crashes on iOS and Android, so the demo works on iOS, Android and desktop
Collision detection is not integrated right now
I am in holidays from next week, but will work on it as I find the time.

@rkreienbuehl

Copy link
Copy Markdown
Contributor Author

I made some research. What I found out so far:

To come closer to how maplibre does symbol placement, symbols should be preproduced so I will implement a placement engine. This would improve performance, because symbols aren't produced every time the viewport changes. Also with the current implementation of symbols only the priority is used for determining the order of placing them, maplibre also uses the layer as info for this.
When the viewport changes, I need only to place symbols (and do collision detection) instead of rendering them every time.

Will take some time to implement this properly. Will get back as soon as there is an update on it.

@p-lr

p-lr commented Mar 2, 2026

Copy link
Copy Markdown
Owner

Having a SymbolsEngine feels necessary indeed. It might be interesting to discuss the design before starting the implementation.
On top of my head, there are two different things that needs to be clarified. Currently, our "viewport" is a combination of two very different data:

  • the tile set
  • the visible area

On top of my head, the engine should make this distinction. Like, when the tile set changes, that means new tiles are visible (or a completely different list of tiles). This is different from when the visible area changes (alone), as in this case it means the user has zoomed in or out but the tiles set is still the same. In that case, the internal list of symbols remains the same, only the scale changes and the engine should probably run collision detection on all the markers in cache to determine the new list of visible symbols.

To summarize, the engine would maintain an in-memory cache of produced symbols, and that cache would be updated whenever the tile set changes. Maybe that cache would look like a Map<Tile, List<Symbol>>, to that symbols are cached per tile (e.g. some sort of 2D cache). That cache would contain several thousand symbols in total, as we've seen. However, thanks to collision detection and the logic you mentioned, only a limited list would be sent to the ui for rendering.

What do you think?

@rkreienbuehl

Copy link
Copy Markdown
Contributor Author

I totally agree. This approach would also be very close to what maplibre does with symbols. I didn't had the time to come up with a full concept, but will work on it this week as much as I find time, so we can discuss this in detail.

@p-lr

p-lr commented Mar 3, 2026

Copy link
Copy Markdown
Owner

Sounds good, no hurry.

@rkreienbuehl

Copy link
Copy Markdown
Contributor Author

@p-lr I finally found some time after holidays.

So for working on this I analyzed how it work right now vs how MapLibre does it:

Current pipeline vs target pipeline

  • Current: viewport change -> produceSymbols(viewport) -> collision -> draw
  • Target: tile/style change -> produceCandidates(tile) (cached) ; viewport change -> place(candidates) -> draw

This would need the following key components and their responsibilities:

  • SymbolLayerPainter / SymbolsProducer: candidate factory (slow). Decode geometry, measure text, resolve sprites, generate stable IDs and anchors.
  • SymbolPlacementEngine: placement (fast). Gather visible candidates, sort by priority (layer first), rebuild screen-space bounds/OBB, run collision, return draw list.
  • CollisionDetector: screen-space collision index. Uses Rtree coarse search plus OBB intersects for exact tests.

Symbols

  • Symbols should get a layerId for layer first priority.

  • There should be SymbolCandidates and PlacedSymbols.

    • SymbolCandidates: are stored/cached by tile in SymbolPlacementEngine ;
      Something like this:
      data class SymbolCandidate(
        val symbol: Symbol,        // existing type: Sprite / Text / SpriteWithText
        val layerIndex: Int,       // primary ordering: style layer order
        val inLayerPriority: Int   // secondary ordering within layer (existing symbolZOrder)
      )
    • PlacedSymbols: computed when viewport is changed ;
      Something like this:
      data class PlacedSymbol(
        val candidate: SymbolCandidate,
        val canvasX: Float,
        val canvasY: Float,
        val angle: Float
      )
  • LabelPlacement also needs val layerIndex: Int and val inLayerPriority: Int (rename from priority).

SymbolPlacementEngine

API sketch

interface SymbolPlacementEngine {
    fun putTileCandidates(tileKey: String, candidates: List<SymbolCandidate>)
    fun removeTile(tileKey: String)
    fun clear()

    fun place(
        viewportInfo: ViewportInfo,
        visibleTiles: VisibleTiles,
        zoomPRState: ZoomPanRotateState // or equivalent info needed to project
    ): List<PlacedSymbol>
}

Placement algorithm (run on every viewport change)

  1. Gather candidates from visible tiles (cache lookup only).
  2. Filter by zoom/style if needed (cheap checks only).
  3. Sort by (layerIndex desc, inLayerPriority desc, stableId asc).
  4. Clear CollisionDetector (new frame).
  5. For each candidate in order: compute screen anchor + size; rebuild screen-space LabelPlacement with OBB; tryPlaceLabel(); if placed, add to draw list.
  6. Return placed Symbols to SymbolState for rendering.

VectorLayer

Split flows: production vs placement

  • Production trigger: tile visibility changes and tile data load/update. Produce candidates per tile once, then cache them in SymbolPlacementEngine.
  • Placement trigger: viewport changes (pan/zoom/rotate). Call placementEngine.place(...) and update SymbolState with the placed symbols only.

This is a rough plan. What do you thingk?

@p-lr

p-lr commented Mar 28, 2026

Copy link
Copy Markdown
Owner

I would rather focus on one thing at a time, because the task is fairly complex. There are two main challenges :

  1. Have a working algorithm for collision detection
  2. Optimize performances

We can start with the collision detection challenge. No cache, everything is re-computed when we scroll or zoom (i.e on every single viewport changes).
Doing this way, we can focus on the algorithm itself and fix all issues we'll find along the way. When the algorithm proves to be correct, then we can focus on architecture changes to optimize performances (caching, etc.).

This approach would make me more confident.

@rkreienbuehl

rkreienbuehl commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

Sounds like a reasonable plan. What I totally forgot was that I actually didn't add the collision detection at all when porting into the library. I added it now, and collision detection seems to work ok, but layer priority needs definitely to be added, as described in my last message.

What do you think?

@p-lr

p-lr commented Apr 1, 2026

Copy link
Copy Markdown
Owner

That makes sense. With collision detection, I'm expecting the number of symbols in the rendering pipeline to drop dramatically. I'll take the time to test this and let you know.
If you feel that you don't need my feedback to start working on layer priority, please go ahead.

P.S: I will most probably have to resume my work on the master branch. As the library isn't 100% on par with the android native version, and I'll need a complete kmp version in the upcoming months.

Thanks for your commitment.

@rkreienbuehl

Copy link
Copy Markdown
Contributor Author

Thats ok, I will work on it step by step towards introducing a new SymbolPlacementEngine. I'll post updates here 👍🏻

@rkreienbuehl

rkreienbuehl commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

@p-lr After a lot of research and learnings on how MapLibre does Collision detection, I found a key error that lead to overlapping symbols that shouldn't overlap. CollisionDetector compared the priority and placed if priority was higher, but never removed the lower priority symbol. I changed this so that CollisionDetector does not compare priority, but only detects collisions. Prior to collision detection, the symbols will now be sorted by priority.
Simulator Screenshot - iPhone 17 - 2026-05-06 at 11 08 32

There are still some issues with placed symbols that need to be addressed. I most likely work on it on the weekend. I'll keep you updated.

@p-lr

p-lr commented May 7, 2026

Copy link
Copy Markdown
Owner

I appreciate your work on this — it's a complex subject. I haven't had a chance to look it over yet, but I definitely intend to do so.

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.

3 participants