-
Notifications
You must be signed in to change notification settings - Fork 399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Parallel Tile Generation #35
Comments
I don't think there is, given the way |
It is possible to do this with CATiledLayer, but it requires usage of private methods of the CATiledLayer class. The built-in MapKit MKMapView uses a sub-classed UIScrollView+CATiledLayer combination (called MKScrollView+MKTiledLayer). I posted more info about this a while back on the devforums: https://devforums.apple.com/message/523140 I have filed a radar about this last year requesting to expose the private methods of CATiledLayer that are required to implement truly asynchronous tile fetching like MKMapView can do (CATiledLayer drawRect, while being called on multiple threads, is synchronous with respect to the actual drawing, and any network I/O must block drawRect). MKOverlayView also has the usage pattern that's needed to implement efficient network fetching of tiles, and it's also internally backed by CATiledLayer private methods. |
@incanus Thanks for that.. I ran across that late last night after seeing some odd behavior. I still don't understand why I can't retain the graphics context and then draw on it at a later time. |
@mtoon I think the problem is that in order to retain the graphics context, CATiledLayer would have have know about that, since it takes the context passed to drawRect and immediately draws it on the screen after drawRect returns. The context only lives within the call to drawRect. The solution is to only have drawRect called when you have the data ready to draw, which is done with the canDrawRect/setNeedsDisplayInRect pattern. If anyone finds a solution to this, I'd love to hear about it! |
FYI.. Good response here on the forums for my question.. https://devforums.apple.com/message/626588 |
Oops, just realized your post was almost exactly the same answer... Hoepfully my follow up in the forums about how to increase the performance of drawrect for CATiledLayer is answered. Thanks! |
"As for the CATiledLayer, yes it will generate an extra thread on devices with 2 cores." So, it looks like CATiledLayer is already optimized for multiple threads. So, now I just need to optimize my rendering routines! Oh well, so much for the easy route! Just a note here... I am SHOCKED (all caps in a good way!) at how memory efficient this implementation of route-me is. I had used the original one and spent a LOT of time optimizing it and my own code and I still was plagued with constant memory warnings. Roughly a direct port over to this new library (same functionality, just had to accommodate for the structural differences) and I haven't had a single memory warning even on one of our test iPad 1s... So, thank you for all of your hard work! |
@zhm, good info. I hadn't gotten that deep into it, as my That is, completely rewriting |
Something I used to do in the previous RM library (because it was passing back an object rather than a UIImage, so it was easier) was to return the base tile (so that something the user could reference came back immediately) and then spawn off a background block to render the actual composite tile and then update the tile on screen once that was done. So.. not to keep this going forever, but do you guys have any ideas about how that might be possible with the CATiledLayer method? Could it be as easy as a notification with an attached CGRect for invalidating and redrawing with the newly cached tile? Or would that not work? |
Thinking about this, I think it would work. You track the passed |
It all comes down to your |
It seems to me that there could be some neat potential here with composited tiles. If you have one of the (say) three composited layers handy for a tile, you pass that back in |
Yup, that's exactly how my previous implementation worked.... We use the mapbox mbtiles for the base layer and then have lots of other possible layers that can be composited on top of those, so I basically pulled the correct tile out of the base layer and spawned off a block (with a reference to the tile) and returned the base layer (very fast) and then when the block finished it called the updateTileWithData method which would replace the tile with the final version. However, that worked well because the RMMapView held a reference to all pending RMTileImages and would call cancelLoading if they went out of scope which I checked each layer and broke out of the composting loop if needed. I don't see that in this implementation in the same style, so it might such down a lot of CPU processing tiles that quickly go out of focus as the user scrolls around fast. However, I'm new at this library so maybe that does exist. |
That might work... it's definitely worth a shot. It depends on how smart CATiledLayer is with setNeedsDisplayInRect. I know there is a private method on CATiledLayer called setNeedsDisplayInRect:levelOfDetail which allows you to invalidate a single level of detail, rather than the rect at all levels of detail (zoom scales). It's possible that it's smart enough to determine the level of detail based on the CATiledLayer parameters and the given rect, since tiles are well-known and not arbitrary. If it's not smart, it could invalidate any tile containing that rectangle, which would include a lot of other tiles at at lower zoom levels (i.e. invalidating a tile at zoom 4 could invalidate tiles at 3, 2, and 1 even though they they've already been drawn). The additional level of detail parameter seems critical to make it work like MKMapView because it's the only way to tell it exactly which tile needs to be drawn. |
I am not using the alpstein branch, but I have written my own map engine that uses CATiledLayer and works just about the same. A big difference is that I broke tile loading completely out of the drawRect:. That is, when the scrollview hosting the tiledlayer finishes moving (there is a scrollview delegate call for this), I kick off a tile loading class that gets the current projected rect of my view and does all the ctile loading and compositing in a NSOperationQueue. If the view starts moving again I pause the queue, and when the view stops moving, kill the operations that are no longer in view and add the new ones that are needed. When the queue finishes a tile, the tile gets sent back to my map view and added to an in memory cache of tiles currently on screen. Finally, I invalidate the the tiled layers rect for the tile I just cached. I only feed the tiled layer drawRect: tiles from that cache. To not risk losing work done to composite tiles for other zoom levels I have a secondary cache on disk which I check in the tile loader class I mentioned above. This plus a few other tweaks I have made makes it feel pretty close to MKMapView speeds, even when loading tiles over the network. |
This is an interesting approach... Any thoughts about creating a queue that holds all of the pending requests that is updated when the view changes? It actually sounds fairly straightforward, but that's coming from the newbie, so the more veteran users of this library, what do you think of:
As Aaron pointed out, an operation queue might be good so that the level of parallelism is configurable. What i like about this is you could very simply implement a tile source like: RMBlockTileSource *ts = [[RMBlockTileSource alloc] initWithRenderingBlock:^(RMTileRequest *req){ Please excuse the syntax errors, just off the top of my head. |
@afarnham, thanks for the comment. I think a version of that combined with @incanus and @mtoon's suggestions should produce a good result. I'd be interested to test out a proof-of-concept using the pattern. Even though it might not use the internal caching of CATiledLayer to its fullest extent since drawRect is called potentially more than necessary, I bet it would produce a result that's perceptually nearly identical. Great stuff! |
Do you know if I scroll to say, pos x,y the various tiles are queued up internally in the tiled layer.. Then (while the tiles are being rendered), I scroll to an entirely new area... will the tiled layer stop requesting the tile not needed now? |
When I rewrote the map view to use Therefore, I'd prefer a solution that uses the current approach for "traditional" tile sources and something else for tile sources that need more background processing, but if we find a solution that works well in both cases, is easy to maintain and has good performance, I'll be happy with that as well. |
Has anyone started on this? About to wrap the latest version and I could dive in, but if anyone is already started, I didn't want to duplicate effort. Thanks! |
I'm back looking into these issues. I am seeing bottlenecking for network-based tiles, and I think it's based on the underlying Core Foundation URL loading not being able to simultaneously request tiles from the same hostname. With multicore @afarnham, have you run into this in your implementation? Even with a queue to move network ops outside of the rendering routine, it seems like you'd have to have a single serial queue for these ops. My current areas of investigation are:
/cc @russellquinn |
@incanus I had not encountered this issue, but had not really looked for it before. I'll keep an eye out once I am back to working on this project (another week or two I think). |
@zhm, doing the background fetching method and then calling |
In doing some poking around with the Maps.app, I have noticed that the client does HTTP POST requests to the server, roughly on the order of 1/5th the number of requests as this library does. Then, it gets back larger responses of concatenated PNG images in a single file, which presumably it then splits and renders to the tiled layer. However, it seems that being able to access |
@zhm You can keep the background from appearing while loading tiles by borrowing a caching trick from the original, non-catiledlayer route me. The idea is to keep tiles from previous zoom levels of the current map area in a cache that your drawrect function can access. So when a tile is needed, but not yet downloaded you can instead find a cached tile from a previous zoom level that covers the area and slice it up into an image that fits the requested tile perfectly. It will be blurry tile you get the actual tile you need, but users are used to that as it happens in Maps.app too. In my implementation I fetch the tiles for the 2 previous zoom levels that cover my current map area and put them in "on screen" memory cache that my catiledlayer drawrect function can access. |
I think this feature could be feasible now, with the new feature of adding multiple tile sources to the map view.
Any ideas? This would be mainly a feature of the tile source, but I think some enhacements in route-me might be necessary to support such tile sources. |
It would be great to have a way to notify the map of updated tiles and have just that tile re-rendered. I think the older Routeme used to actually have a class representing the tile and that tile could be passed back to the map for updating. An example of this would be both this base and then updated content load as well as dynamically updating tiles (for things like WX overlays, etc). It would be nice not to reload the whole view simply because it seems to take a little time to do that. |
@mtoon I don't see any feasible possibility to only reload specific tiles since |
No worries, thanks for the info! |
Calling [CATiledLayer setNeedsDisplayInRect:] with a CGRect that represents the pixel locations of the tile you want to invalidate will cause it to redraw just that tile on the screen. It should not cause a whole screen redraw. The main side effect is that it will affect the cache of tiles at other zoom levels that intersect that rect. One other possible side effect is that if your redraw pushes over some memory limit on the CATiledLayer it can cause a full screen draw, but I have found that is pretty rare on iPad 2 and 3. |
Guys, I've been focusing on this in a somewhat-sloppy-but-works-for-testing branch of our SDK called develop-bench. Feel free to check it out. I've also been working on a testbed app that uses this branch and makes it easy to really hammer the testing. It's called MapBox Bench. I've been working on two async methods thus far. More details in the app's README.
I'm not entirely happy with either method yet, but they are my most recent thinking on this and it's something I want to keep focusing on, so I thought it was good to get them out there. |
We are seeing pretty good performance with the above-referenced commit. I'm curious what others think. Here is the rationale/method:
This effectively does what @zhm mentioned above and requests redraws. However, combined with Any thoughts? I'm liking this so far. |
I don't have much time at the moment to test it in depth, but it looks quite good so far. I will try to get some spare time next week. |
Good evening! I hate to keep asking questions, but I've been banging my head against the wall on this one... Is there a provision to generate the tiles in parallel? Basically looking for a way for the map to request all of the tiles at once and use some kind of background processing mechanism to generate the tiles and then notify the mapview that they are ready and have the mapview draw the tiles...
Thoughts?
Thank you!
The text was updated successfully, but these errors were encountered: