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

Switch to earcut #2444

Merged
merged 7 commits into from
May 31, 2016
Merged

Switch to earcut #2444

merged 7 commits into from
May 31, 2016

Conversation

jfirebaugh
Copy link
Contributor

@jfirebaugh jfirebaugh commented Oct 13, 2015

We're currently using libtess2, but the caveat is that it crashes hard on geometries that aren't completely valid. Therefore, we also use ClipperLib to cleanup geometries before passing them on. This adds quite a bit of code, as well as a huge performance hit for processing tiles.

Instead, we should switch to earcut.hpp. Earcut also requires valid geometries, but doesn't crash hard when they aren't. It's also a 2⨉ to 15⨉ speed improvement in tessellation alone, in particular for polygons with low vertex counts, such as buildings. It also allows us to drop ClipperLib.

Alternatively, we could drop libtess2 in a first step, but still clean up geometries with ClipperLib.

@mourner
Copy link
Member

mourner commented Sep 29, 2015

You could remove clipper and integrate earcut without waiting for validness fixes in the data sources (which can take a while), and test whether it's really a problem.

@kkaefer
Copy link
Member Author

kkaefer commented Sep 29, 2015

It's not a problem in that the renderer crashes, but some of the polygons will be invalid. It'll probably be hard to spot that because you have no direct comparison, though.

@mourner
Copy link
Member

mourner commented Sep 29, 2015

@kkaefer yeah, I mean, if the problems are hard to spot, we might get by without fully valid polygons for some time.

@jfirebaugh jfirebaugh added the performance Speed, stability, CPU usage, memory usage, or power usage label Sep 29, 2015
@jfirebaugh
Copy link
Contributor

@kkaefer Is the total_vertex_count > 65536 check only to avoid a libtess limit, or is it something we need to enforce with earcut too?

@kkaefer
Copy link
Member Author

kkaefer commented Oct 2, 2015

That check is because we're using vertex buffers with elements buffers referencing those elements. Since elements buffers are 2 bytes per item, we can address a max of 65536 unique vertices. This check is a bit overly aggressive since there may be duplicate points.

@kkaefer kkaefer mentioned this pull request Oct 2, 2015
8 tasks
@jfirebaugh
Copy link
Contributor

First renders:

image

@kkaefer @mourner @ansis Any tips on how to debug stuff like this?

@mourner
Copy link
Member

mourner commented Oct 6, 2015

@jfirebaugh do you try to infer ring structure somehow? When I tested on GL JS, artifacts like this appeared because multipolygons are flattened into a set of rings (e.g. outer, hole, hole, outer, hole), and earcut requires polygons as outer ring + its holes.

@jfirebaugh
Copy link
Contributor

Much progress in the last 4 hours:

image

I ported the ring classification code from JS, and then discovered that earcut.hpp has a bug somewhere if you use short as the coordinate type. Above screenshot was generated using int instead.

Still some rendering errors in the test suite, possibly from an outdated vector tile? Not seeing this issue in the test app at zoom 0:

image

@springmeyer
Copy link
Contributor

Great progress @jfirebaugh. Outdated tile (rendered prior to the "simplification" push which also landed winding order fix - https://github.com/mapbox/mapnik-internal/issues/10) is likely. But lowzoom tiles may still have self-intersections in the latest tiles and that is the case @flippmoke is focused on in mapnik/node-mapnik#533.

@mourner
Copy link
Member

mourner commented Oct 7, 2015

@jfirebaugh Sorry John, was sleepy yesterday and pointed you to the wrong branch. The ring classification routine you ported depends on naive point in polygon tests (all rings tested against each other) which can sometimes be unreliable.

There's a different, much simpler ring clasification routine in mapbox/mapbox-gl-js@77d2061 (earcut-alt branch) that infers ring structure from their winding order (opposite winding order means different types, outer vs hole) and order they appear in VT (holes always follow their outer ring), which should perform much better and be more reliable provided there are no more winding order bugs left in the current VT.

Still great progress though! Did you remove the clipper processing or is it clipper + earcut? Would be cool to measure real world performance of earcut vs clipper + libtess.

@jfirebaugh
Copy link
Contributor

@mourner Okay, I'll port the winding order-based classification routine. Can we clean up the various earcut* gl-js branches into one canonical branch?

@mourner
Copy link
Member

mourner commented Oct 7, 2015

@jfirebaugh yes, will do tomorrow.

@jfirebaugh
Copy link
Contributor

After updating the 0 tile test fixture pbf, down to the following tesselation error:

image

This is visible in real-world styles too:

image

@mourner
Copy link
Member

mourner commented Oct 7, 2015

@jfirebaugh is this the old ring classification or the new one?

@jfirebaugh
Copy link
Contributor

New one, winding order based.

@mourner
Copy link
Member

mourner commented Oct 7, 2015

Can you find out if it's a VT winding order bug or Earcut failure?

@mourner
Copy link
Member

mourner commented Oct 7, 2015

If it's not easy to determine the exact cause, just send me the gist with the exact input to earcut (isolated single polygon that causes the problem) and I'll figure it out.

@jfirebaugh
Copy link
Contributor

Conclusion from testing is that we will need to roll out improved polygon validity in our datasources before this can land.

@jfirebaugh
Copy link
Contributor

Instruments Time Profiler results from two scenes, rough average of 3 loads for each, all times are sums over all tiles loaded for the scene. I had to look at slightly different functions due to oddities in how the profiler nested functions (likely due to inlining).

Mapbox SF @ z15, 6 tiles

  • With libtess, FillBucket::tesselate takes 70-80 ms, of which 35-45 ms is libtess and 25 ms is clipper
  • With earcut, FillBucket::addGeometry takes 12-20 ms, of which 6-7 ms is earcut, 1 ms is ring classification, and the rest varies between runs

SF @ z7, 9 tiles

  • With libtess, FillBucket::tesselate takes 310 ms, of which 190 ms is libtess and 80 ms is clipper
  • With earcut, FillBucket::addGeometry takes 135 ms, of which 115 is earcut

So as a rough guideline, earcut is at least 2x faster and in the best case (lots of simple polygons like building footprints), 6-7x faster.

@mourner
Copy link
Member

mourner commented Oct 13, 2015

@lucaswoj
Copy link
Contributor

improved polygon validity in our datasources

Is this ticketed somewhere?

@mourner
Copy link
Member

mourner commented Oct 14, 2015

@jfirebaugh @yhahn a middle option to make the transition easier and get at least half of the gain while we try to tackle the challenge of VT compatibility is to run Clipper + Earcut. In theory this should work for all legacy VT while still shaving off some good ms.

@jfirebaugh
Copy link
Contributor

That's an option, although from the numbers above, it would reduce the gain on fill tessellation to about 1.5-2x. Let's see how the source validity work pans out first.

@mourner
Copy link
Member

mourner commented Oct 14, 2015

@jfirebaugh yeah, I just thought it would be easy enough to break down into multiple steps:

  1. libtess -> earcut
  2. VT validity
  3. remove clipper

Another advantage of trying clipper+earcut is that for shape annotations to be 100% stable, we'll probably have to run clipper on it since GeoJSON-VT simplification routine can make valid polygons invalid. Although artifacts would be extremely rare for user annotations (unlike crazy water polys in Streets), they're still a possibility we need to take care of.

@jfirebaugh
Copy link
Contributor

jfirebaugh commented May 19, 2016

I'm seeing some performance hot spots on the same complex European landcover polygons that cause issues with libtess2 (#4777):

image

@mourner
Copy link
Member

mourner commented May 19, 2016

@jfirebaugh try to isolate a particularly nasty polygon, and I'll see whether there's something in particular that makes the hole bridging routine get stuck for so long.

@jfirebaugh
Copy link
Contributor

The problematic polygons are almost certainly the same ones that are causing the problem for libtess in #4777.

I added some debug output for tiles taking longer than 1 second to process with earcut (in debug mode, so worst case):

2272ms layer=wood sourceLayer=landuse tileID=6/32/22
3399ms layer=wood sourceLayer=landuse tileID=6/32/23
8310ms layer=wood sourceLayer=landuse tileID=6/34/22
10681ms layer=wood sourceLayer=landuse tileID=6/33/22
5038ms layer=wood sourceLayer=landuse tileID=6/33/21
12985ms layer=wood sourceLayer=landuse tileID=6/34/21

https://gist.github.com/jfirebaugh/14c6d1b1aceb1c6f880baf1610e06817 contains the data for the fourth polygon above. It looks like this when tesselated with earcut:

image

@jfirebaugh
Copy link
Contributor

gl-js branch that logs slow earcut times: https://github.com/mapbox/mapbox-gl-js/compare/earcut-timing

@jfirebaugh jfirebaugh force-pushed the earcut branch 2 times, most recently from 1fb0bbd to 362f34c Compare May 23, 2016 22:24
@jfirebaugh jfirebaugh force-pushed the earcut branch 3 times, most recently from 46c3380 to 777d3f2 Compare May 27, 2016 19:14
@jfirebaugh
Copy link
Contributor

@mourner 👀 on the latest change?

I think this is good to go -- with the combined Mapbox Streets VT update and ring limiting code, earcut time is down to a reasonable fraction of overall ticks in profiles. (In Europe at z6-8, it's still the dominant processing cost on worker threads, so any further optimizations are welcome.)

[] (const auto& a, const auto& b) {
return signedArea(a) > signedArea(b);
});
polygon.resize(1 + maxRings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! The only minor thing is that maxRings name implies a limit to a total number of rings, including the outer ring, so you would need to remove a few +1s. Or rename to maxHoles.

@jfirebaugh jfirebaugh force-pushed the earcut branch 2 times, most recently from 524b549 to 354789f Compare May 28, 2016 01:15
@jfirebaugh jfirebaugh merged commit a800d33 into master May 31, 2016
@jfirebaugh jfirebaugh deleted the earcut branch May 31, 2016 17:15
1ec5 added a commit that referenced this pull request Jun 3, 2016
Added entries to the iOS SDK changelog for #5124, #2444, #5141, #5164. Removed entries for changes made since the last release; they go in the GitHub prerelease notes but not here in this document.
1ec5 added a commit that referenced this pull request Jun 14, 2016
@tobrun tobrun mentioned this pull request Aug 5, 2016
9 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
performance Speed, stability, CPU usage, memory usage, or power usage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants