summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Rex <julian.rex@mapbox.com>2019-03-25 01:21:57 -0400
committerJulian Rex <julian.rex@mapbox.com>2019-04-16 11:38:41 -0400
commitfdaf5b313dcec7d5badf0dc192135135cb18dd14 (patch)
treee70f014d81a0bc6546413b05249f34186a398a5e
parente3d544ee1a34532681e746b90ff15a66fa17f535 (diff)
downloadqtlocation-mapboxgl-fdaf5b313dcec7d5badf0dc192135135cb18dd14.tar.gz
[ios] Call Renderer::flush on resign active/enter background
-rw-r--r--platform/darwin/src/MGLRendererFrontend.h8
-rw-r--r--platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m99
-rw-r--r--platform/ios/src/MGLMapView.mm117
3 files changed, 189 insertions, 35 deletions
diff --git a/platform/darwin/src/MGLRendererFrontend.h b/platform/darwin/src/MGLRendererFrontend.h
index ead6ad2660..f5ec53a49b 100644
--- a/platform/darwin/src/MGLRendererFrontend.h
+++ b/platform/darwin/src/MGLRendererFrontend.h
@@ -57,6 +57,14 @@ public:
renderer->render(*updateParameters_);
}
+ void flush() {
+ mbgl::BackendScope guard { mbglBackend, mbgl::BackendScope::ScopeType::Implicit };
+
+ if (renderer) {
+ renderer->flush();
+ }
+ }
+
mbgl::Renderer* getRenderer() {
return renderer.get();
}
diff --git a/platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m b/platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m
index b004c05998..cea52740d2 100644
--- a/platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m
+++ b/platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m
@@ -3,7 +3,6 @@
@interface MGLMapView (BackgroundTests)
@property (nonatomic) id<MGLApplication> application;
-@property (nonatomic) BOOL rendererWasFlushed;
@property (nonatomic, getter=isDormant) BOOL dormant;
@property (nonatomic) CADisplayLink *displayLink;
- (void)updateFromDisplayLink:(CADisplayLink *)displayLink;
@@ -104,7 +103,6 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
XCTAssertFalse(self.mapView.isDormant);
XCTAssertFalse(self.mapView.displayLink.isPaused);
XCTAssert(self.mapView.application.applicationState == UIApplicationStateActive);
- XCTAssertFalse(self.mapView.rendererWasFlushed);
__weak typeof(self) weakSelf = self;
@@ -116,6 +114,75 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
didEnterBackgroundExpectation.expectedFulfillmentCount = 1;
didEnterBackgroundExpectation.assertForOverFulfill = YES;
+ self.didEnterBackground = ^(__unused NSNotification *notification){
+ typeof(self) strongSelf = weakSelf;
+ MGLMapView *mapView = strongSelf.mapView;
+
+ // In general, because order of notifications is not guaranteed
+ // the following asserts are somewhat meaningless (don't do this in
+ // production) - however, because we're mocking their delivery (and
+ // we're tracking a bug)...
+
+ // MGLMapView responds to UIApplicationDidEnterBackgroundNotification and
+ // marks the map view as dormant. However, depending on the order of
+ // creation it's totally possible for client code also responding to
+ // this notification to be called first - and then trigger a scenario where
+ // GL can be rendering in the background - which can cause crashes.
+
+ MGLTestAssert(strongSelf, !mapView.isDormant);
+
+ // However, the display should be paused (because this has now moved
+ // to ...WillResignActive...
+ MGLTestAssert(strongSelf, mapView.displayLink.isPaused);
+
+ [didEnterBackgroundExpectation fulfill];
+ };
+
+ [self.mockApplication enterBackground];
+ [self waitForExpectations:@[didEnterBackgroundExpectation] timeout:1.0];
+
+ XCTAssert(self.mapView.isDormant);
+
+ // TODO: What do we want here?
+ XCTAssert(!self.mapView.displayLink || self.mapView.displayLink.isPaused);
+ XCTAssert(self.mapView.application.applicationState == UIApplicationStateBackground);
+
+ //
+ // Enter foreground
+ //
+
+ XCTestExpectation *willEnterForegroundExpectation = [self expectationWithDescription:@"willEnterForeground"];
+ willEnterForegroundExpectation.expectedFulfillmentCount = 1;
+ willEnterForegroundExpectation.assertForOverFulfill = YES;
+
+ self.willEnterForeground = ^(NSNotification *notification) {
+ [willEnterForegroundExpectation fulfill];
+ };
+
+ [self.mockApplication enterForeground];
+ [self waitForExpectations:@[willEnterForegroundExpectation] timeout:1.0];
+
+ XCTAssertFalse(self.mapView.isDormant);
+ XCTAssert(self.mapView.displayLink && !self.mapView.displayLink.isPaused);
+ XCTAssert(self.mapView.application.applicationState == UIApplicationStateActive);
+}
+
+- (void)testRendererAdjustingViewsWhenGoingIntoBackground {
+
+ XCTAssertFalse(self.mapView.isDormant);
+ XCTAssertFalse(self.mapView.displayLink.isPaused);
+ XCTAssert(self.mapView.application.applicationState == UIApplicationStateActive);
+
+ __weak typeof(self) weakSelf = self;
+
+ //
+ // Enter background
+ //
+
+ XCTestExpectation *didEnterBackgroundExpectation = [self expectationWithDescription:@"didEnterBackground"];
+ didEnterBackgroundExpectation.expectedFulfillmentCount = 1;
+ didEnterBackgroundExpectation.assertForOverFulfill = YES;
+
__block NSInteger displayLinkCount = 0;
self.displayLinkDidUpdate = ^{
@@ -130,7 +197,7 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
// the following asserts are somewhat meaningless (don't do this in
// production) - however, because we're mocking their delivery (and
// we're tracking a bug)...
-
+
// MGLMapView responds to UIApplicationDidEnterBackgroundNotification and
// marks the map view as dormant. However, depending on the order of
// creation it's totally possible for client code also responding to
@@ -138,11 +205,15 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
// GL can be rendering in the background - which can cause crashes.
MGLTestAssert(strongSelf, !mapView.isDormant);
- MGLTestAssert(strongSelf, !mapView.displayLink.isPaused);
+
+ // However, the display should be paused (because this has now moved
+ // to ...WillResignActive...
+ MGLTestAssert(strongSelf, mapView.displayLink.isPaused);
displayLinkCount = 0;
// Remove the map view, and re-add to try and force a bad situation
+ // This will delete/re-create the display link
UIView *parentView = mapView.superview;
NSLog(@"Removing MGLMapView from super view");
@@ -158,24 +229,27 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
[mapView.leftAnchor constraintEqualToAnchor:parentView.leftAnchor].active = YES;
[mapView.rightAnchor constraintEqualToAnchor:parentView.rightAnchor].active = YES;
[mapView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor].active = YES;
+
+ [didEnterBackgroundExpectation fulfill];
};
[self.mockApplication enterBackground];
- [self waitForExpectations:@[didEnterBackgroundExpectation] timeout:0.5];
+ [self waitForExpectations:@[didEnterBackgroundExpectation] timeout:1.0];
XCTAssert(self.mapView.isDormant);
- XCTAssert(self.mapView.displayLink.isPaused);
+
+ // TODO: What do we want here?
+ XCTAssert(!self.mapView.displayLink || self.mapView.displayLink.isPaused);
XCTAssert(self.mapView.application.applicationState == UIApplicationStateBackground);
- XCTAssert(self.mapView.rendererWasFlushed);
-
+
//
// Enter foreground
//
-
+
XCTestExpectation *willEnterForegroundExpectation = [self expectationWithDescription:@"willEnterForeground"];
willEnterForegroundExpectation.expectedFulfillmentCount = 1;
willEnterForegroundExpectation.assertForOverFulfill = YES;
-
+
self.willEnterForeground = ^(NSNotification *notification) {
displayLinkCount = 0;
[willEnterForegroundExpectation fulfill];
@@ -183,12 +257,11 @@ typedef void (^MGLNotificationBlock)(NSNotification*);
[self.mockApplication enterForeground];
XCTAssert(displayLinkCount == 0, @"updateDisplayLink was called %ld times", displayLinkCount);
- [self waitForExpectations:@[willEnterForegroundExpectation] timeout:0.5];
+ [self waitForExpectations:@[willEnterForegroundExpectation] timeout:1.0];
XCTAssertFalse(self.mapView.isDormant);
- XCTAssertFalse(self.mapView.displayLink.isPaused);
+ XCTAssert(self.mapView.displayLink && !self.mapView.displayLink.isPaused);
XCTAssert(self.mapView.application.applicationState == UIApplicationStateActive);
- XCTAssert(self.mapView.rendererWasFlushed);
}
@end
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index c0b9810678..21c90f5975 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -192,7 +192,7 @@ public:
NSString *viewReuseIdentifier;
};
-@interface UIApplication (MGLApplicationConforming) <MGLApplication>
+@interface UIApplication (MGLApplicationConformation) <MGLApplication>
@end
@@ -276,8 +276,8 @@ public:
// Application properties
@property (nonatomic) id<MGLApplication> application;
-@property (nonatomic) BOOL rendererWasFlushed;
@property (nonatomic) CADisplayLink *displayLink;
+@property (nonatomic) UIImage *lastSnapshotImage;
- (mbgl::Map &)mbglMap;
@@ -469,12 +469,11 @@ public:
if (application) {
[center addObserver:self selector:@selector(willResignActive:) name:UIApplicationWillResignActiveNotification object:application];
- [center addObserver:self selector:@selector(sleepGL:) name:UIApplicationDidEnterBackgroundNotification object:application];
- [center addObserver:self selector:@selector(wakeGL:) name:UIApplicationWillEnterForegroundNotification object:application];
- [center addObserver:self selector:@selector(wakeGL:) name:UIApplicationDidBecomeActiveNotification object:application];
+ [center addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:application];
+ [center addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:application];
+ [center addObserver:self selector:@selector(didBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:application];
[center addObserver:self selector:@selector(willTerminate) name:UIApplicationWillTerminateNotification object:application];
[center addObserver:self selector:@selector(didReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:application];
-
}
}
}
@@ -859,8 +858,9 @@ public:
- (void)didReceiveMemoryWarning
{
MGLAssertIsMainThread();
+ self.lastSnapshotImage = nil;
- if ( ! self.dormant)
+ if (!self.dormant && _rendererFrontend)
{
_rendererFrontend->reduceMemoryUse();
}
@@ -1013,7 +1013,7 @@ public:
// This is the delegate of the GLKView object's display call.
- (void)glkView:(__unused GLKView *)view drawInRect:(__unused CGRect)rect
{
- if ( ! self.dormant || ! _rendererFrontend)
+ if (!self.dormant && _rendererFrontend)
{
_rendererFrontend->render();
}
@@ -1235,18 +1235,28 @@ public:
- (void)validateDisplayLink
{
BOOL isVisible = self.superview && self.window;
+
if (isVisible && ! _displayLink)
{
+ BOOL active = (self.application.applicationState == UIApplicationStateActive);
+
if (_mbglMap && self.mbglMap.getMapOptions().constrainMode() == mbgl::ConstrainMode::None)
{
self.mbglMap.setConstrainMode(mbgl::ConstrainMode::HeightOnly);
}
-
+
_displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(updateFromDisplayLink:)];
+
+ _displayLink.paused = !active;
+
[self updateDisplayLinkPreferredFramesPerSecond];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- _needsDisplayRefresh = YES;
- [self updateFromDisplayLink:_displayLink];
+
+ if (active)
+ {
+ _needsDisplayRefresh = YES;
+ [self updateFromDisplayLink:_displayLink];
+ }
}
else if ( ! isVisible && _displayLink)
{
@@ -1496,16 +1506,59 @@ public:
- (void)willResignActive:(__unused NSNotification *)notification
{
+ [self resignGL];
}
-- (void)sleepGL:(__unused NSNotification *)notification
+- (void)didEnterBackground:(__unused NSNotification *)notification
+{
+ [self sleepGL:notification];
+}
+
+- (void)willEnterForeground:(__unused NSNotification *)notification
+{
+ [self wakeGL:notification];
+}
+
+- (void)didBecomeActive:(__unused NSNotification *)notification
+{
+ [self resumeGL];
+}
+
+- (BOOL)supportBackgroundRendering
{
// If this view targets an external display, such as AirPlay or CarPlay, we
// can safely continue to render OpenGL content without tripping
// gpus_ReturnNotPermittedKillClient in libGPUSupportMercury, because the
// external connection keeps the application from truly receding to the
// background.
- if (self.window.screen != [UIScreen mainScreen])
+ return (self.window.screen != [UIScreen mainScreen]);
+}
+
+- (void)resignGL
+{
+ if ([self supportBackgroundRendering])
+ {
+ return;
+ }
+
+ // Pausing on resign active is a change in behaviour.
+ self.displayLink.paused = YES;
+
+ // Take the GL snapshot before entering the background, since this triggers
+ // a render
+ self.lastSnapshotImage = self.glView.snapshot;
+
+ // For OpenGL this calls glFinish as recommended in
+ // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/ImplementingaMultitasking-awareOpenGLESApplication/ImplementingaMultitasking-awareOpenGLESApplication.html#//apple_ref/doc/uid/TP40008793-CH5-SW1
+ if (_rendererFrontend)
+ {
+ _rendererFrontend->flush();
+ }
+}
+
+- (void)sleepGL:(__unused NSNotification *)notification
+{
+ if ([self supportBackgroundRendering])
{
return;
}
@@ -1518,8 +1571,11 @@ public:
// Compromise position: release everything but currently rendering tiles
// A possible improvement would be to store a copy of the GL buffers that we could use to rapidly
// restart, but that we could also discard in response to a memory warning.
- _rendererFrontend->reduceMemoryUse();
-
+ if (_rendererFrontend)
+ {
+ _rendererFrontend->reduceMemoryUse();
+ }
+
if ( ! self.dormant)
{
self.dormant = YES;
@@ -1528,8 +1584,6 @@ public:
[MGLMapboxEvents flush];
- _displayLink.paused = YES;
-
if ( ! self.glSnapshotView)
{
self.glSnapshotView = [[UIImageView alloc] initWithFrame:self.glView.frame];
@@ -1538,7 +1592,7 @@ public:
[self insertSubview:self.glSnapshotView aboveSubview:self.glView];
}
- self.glSnapshotView.image = self.glView.snapshot;
+ self.glSnapshotView.image = self.lastSnapshotImage;
self.glSnapshotView.hidden = NO;
if (self.debugMask && [self.glSnapshotView.subviews count] == 0)
@@ -1551,6 +1605,11 @@ public:
[self.glView deleteDrawable];
}
+
+ if (_rendererFrontend)
+ {
+ _rendererFrontend->flush();
+ }
}
- (void)wakeGL:(__unused NSNotification *)notification
@@ -1558,7 +1617,9 @@ public:
MGLLogInfo(@"Entering foreground.");
MGLAssertIsMainThread();
- if (self.dormant && self.application.applicationState != UIApplicationStateBackground)
+ NSAssert(self.application.applicationState != UIApplicationStateActive, @"Should transition from background");
+
+ if (self.dormant)
{
self.dormant = NO;
@@ -1570,8 +1631,8 @@ public:
[self.glView bindDrawable];
- _displayLink.paused = NO;
-
+ [self validateDisplayLink];
+
[self validateLocationServices];
[MGLMapboxEvents pushTurnstileEvent];
@@ -1579,10 +1640,22 @@ public:
}
}
+- (void)resumeGL
+{
+ self.lastSnapshotImage = nil;
+
+ // Only restart the display link if we're not hidden
+ self.displayLink.paused = self.hidden;
+}
+
+
- (void)setHidden:(BOOL)hidden
{
super.hidden = hidden;
- _displayLink.paused = hidden;
+
+ BOOL inactive = (self.application.applicationState != UIApplicationStateActive);
+
+ self.displayLink.paused = hidden || inactive;
}
- (void)tintColorDidChange