Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Flickering labels when panning #7026

Closed
kkaefer opened this issue Nov 11, 2016 · 12 comments
Closed

Flickering labels when panning #7026

kkaefer opened this issue Nov 11, 2016 · 12 comments
Assignees
Labels
bug Core The cross-platform C++ core, aka mbgl text rendering

Comments

@kkaefer
Copy link
Member

kkaefer commented Nov 11, 2016

Seeing this at a panning operation. What's happening is this:

  • Step 1: tiles are loaded and placed
  • Step 2: an adjacent tile becomes visible in the viewport, some (but not all!) of the labels in the previously loaded tiles vanish
  • Step 3: some of the new tiles complete loading, and some of the labels that disappeared are showing again (see lower left)
  • Step 4: all new tiles completed loading, and all labels are back to normal

untitled1
untitled2
untitled3
untitled4

@boundsj
Copy link
Contributor

boundsj commented Nov 11, 2016

This seems like it might be related #7003

@kkaefer
Copy link
Member Author

kkaefer commented Nov 11, 2016

given #7003 (comment), I don't think this is related

@1ec5
Copy link
Contributor

1ec5 commented Jan 4, 2017

I think this is the behavior @amyleew and I were seeing in #5946.

@jfirebaugh
Copy link
Contributor

This is reproducible on the iOS 3.4.0 release branch but not the 3.3.7 tag.

@kkaefer
Copy link
Member Author

kkaefer commented Jan 5, 2017

Here's another recording of consecutive frames: http://bl.ocks.org/kkaefer/raw/18a5bac434522ec92bd5113ce10fdb2c/

Based on the following analysis, I believe these are two distinct issues:

  • The left lower tile (with Potsdam on it), has an expiration date of 2017-01-04 23:36:18 in frame 2. In frame 5, it vanishes completely. In frame 8, the non-symbol layers reappear, but this time with an expiration date of 2017-01-05 22:11:54, and in frame 9 the symbol layers appear again. You can see a similar behavior with the right lower tile, and likely also with all other tiles. My suspicion is that we are deleting a loaded tile when we receive new data for the same tile (that is not yet parsed!). This explains why the tile completely vanished for a few frames, then gradually reappears as we're parsing the new tile.

    The issue here is that we're deleting existing tiles when new data arrives, but we should wait until the new data is parsed completely (as in placement has finished as well).

  • In the transition from frame 1 to frame 2, you can see that the lower left tile (Potsdam) is loaded, but only shows shapes/lines, not symbols, which is why the symbols from the parent tile (which covers the entire lower section) are still displayed. In frame 3, the lower right tile starts showing shapes/lines (but not symbols) as well. This prompts the parent tile (which still supplied the symbols in the previous frame) is deleted, and the symbols vanish. Symbols reappear in frame 4, but this time they're way more detailed, leading me to believe that symbol parsing for the two new tiles has concluded at this point. You can observe a similar behavior for the upper two tiles between frame 5 and 5.

    The issue here is that we currently can't clip labels, and therefore have to show all or nothing of a parent tile. There's code that accounts for this, but the issue we're seeing here is that the parent tile gets evicted by our update_renderables algorithm, even though it is still needed. This is because we treat a tile as "renderable" once the shapes/lines have been parsed, meaning that we evict the parent tile as soon as those are available. This leads to the label flicker we're seeing here. Instead, we should not treat tiles as a unit, but instead run this entire algorithm on a layer basis, and essentially remove the association of a bucket with a tile altogether. Right now, the ownership chain is:

    • MapMap::Impl:
      • std::unique_ptr<Style> style;
        Style:
        • std::vector<std::unique_ptr<Source>> sources;
          SourceSource::Impl:
          • std::map<OverscaledTileID, std::unique_ptr<Tile>> tiles;
            GeometryTile: (subclass of Tile)
            • std::unordered_map<std::string, std::shared_ptr<Bucket>> buckets;
              Bucket

    so in short

    MapStyleSourceGeometryTileBucket

@jfirebaugh
Copy link
Contributor

jfirebaugh commented Jan 5, 2017

From code comment linked above:

We're not clipping symbol layers, so when we have both parents and children of symbol layers, we drop all children in favor of their parent to avoid duplicate labels. See #2482

I doubt that adjustments to eviction timing will make this strategy viable. Consider what happens, under the current algorithm, when slowly panning at zN across a region for which you have a parent tile at zN-1 cached, but tiles at zN are getting loaded on demand as you pan. Whenever a new tile a zN is needed, the algorithm notices that it's not immediately available, but that the zN-1 parent tile is. It loads in the parent tile. This causes labels on up to three other zN tiles to disappear, to be replaced with the labels from the parent tile, which might be completely different due to different data selection at zN-1. Then the fourth tile loads, and zN labels are rendered again. Pan a little bit further, and the process repeats.

To me this suggests that a "drop all children" approach isn't viable. If there are renderable symbol layers for the children, we should be preferring those to the symbol layer of the parent.

@jfirebaugh
Copy link
Contributor

If there are renderable symbol layers for the children, we should be preferring those to the symbol layer of the parent.

After thinking about this for a while, this alone isn't a viable strategy either. There are at least three cases we need to consider:

  • Panning at a fixed zoom level, as described in the previous comment. In this case, we do not want to render parent symbols if there is at least one child tile that was already rendering its labels:
    +-----------------+
    |                 |
    |                 |
    |                 |
    |--------+        |
    |    * OAK        |
    | * SF   |        |
    |        |        |
    +--------+--------+
    
  • Zooming in. In this case, we should not render symbols for any of the children covering a particular parent that was already rendering its symbols until all necessary children are available.
    +-----------------+
    |        |        |
    |    Sacramento * |
    |        |        |
    |--------+--------|
    |        |        |
    | * San Francisco |
    |        |        |
    +--------+--------+
    
    Otherwise, you would see flickering: as soon symbols were rendered for the first child at zN, symbols from the zN-1 parent which cover the remaining zN siblings would disappear, and then reappear when the ideal zN symbols became available.
  • Zooming out. In this case, we should continue rendering all already-available child symbols until the ideal parent symbols are available. This likely already works as desired.

All these are based on the assumption that we are unable to relax the requirement that symbol layers cannot be clipped. If we can clip symbol layers, then other options may be feasible.

The general principle is: continue rendering previously-rendered symbol data for a tile until such time as the tile can be fully covered by ideal tiles with ready-to-render symbol data.

@jfirebaugh
Copy link
Contributor

Tile expiration-related flickering was fixed in #7616. This issue remains to track the flickering issue when panning, which is more difficult to fix.

@ChrisLoer
Copy link
Contributor

@ansis I think porting CrossTileSymbolIndex to gl-native (ongoing work at #10103) will fix this problem, right? Or at least exchange this problem for new problems.

@ChrisLoer ChrisLoer self-assigned this Oct 20, 2017
@ChrisLoer
Copy link
Contributor

We should fix this with #10103.

@ChrisLoer
Copy link
Contributor

ChrisLoer commented Nov 2, 2017

All these are based on the assumption that we are unable to relax the requirement that symbol layers cannot be clipped. If we can clip symbol layers, then other options may be feasible.

This is the assumption that the CrossTileSymbolIndex will change -- we won't actually clip symbols, but we'll detect duplicates between tiles and zero out the opacity of all but one version of the symbol.

The way I'm starting to think about this is that a GeometryTile can be "partially renderable" (everything but symbols loaded) or "fully renderable" (symbols also loaded). The general principle should be that update_renderables should never replace a "fully renderable" tile with a "partially renderable" one. The algorithm should be roughly:

  • If ideal tile is fully renderable, use it
  • If all children are fully renderable, use them
  • If any currently rendered parent is fully renderable, use it (but not a parent from the cache, because cached parents aren't currently displaying and we'd rather find something closer to the ideal zoom if possible)
  • If ideal tile is partially renderable, use it
  • Use as many children as are renderable
  • If the tile is still not fully covered, use the nearest renderable parent, including cached parents

The idea here is that while loading "new" areas of the map (e.g. a pan operation), we'll still be able to start showing tiles before symbols load, but when we're zooming in and out on tiles where symbols are already displaying, we'll wait for tiles to fully loaded before we swap them out.

The non-symbol layers should all be clipped based on the set of tiles that are chosen, and the symbol layers should be un-clipped but with duplicate detection + cross-tile collision detection handling symbols from overlapping/mixed-zoom tiles.

I'm playing around with an implementation that does this, but I haven't gotten the tile clipping logic right yet.

@ChrisLoer
Copy link
Contributor

I'm playing around with an implementation that does this, but I haven't gotten the tile clipping logic right yet.

As part of #10436, we decided to remove the "two-phase" logic that loads non-symbol buckets before symbol-buckets. This brings gl-native into line with gl-js, and significantly simplifies the tile-loading logic, at the cost of losing the "progressive load" effect when glyphs are taking longer to download than the rest of the map. Combined with the CrossTileSymbolIndex, I believe we've fixed this issue.

Rendering mixed-zoom tiles (i.e. a parent tile partially covered by children) is still a tricky case because the overlapping tiles are both fed into the collision index at the same time (meaning you can get cases where symbols from both a parent and a child tile are rendered next to each other for a short time, see #10480). However, we now have fade animations for everything, so the worst case should be symbols starting to fade in/out and then reversing course.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Core The cross-platform C++ core, aka mbgl text rendering
Projects
None yet
Development

No branches or pull requests

5 participants