diff options
author | Julian Rex <julian.rex@mapbox.com> | 2019-03-25 01:21:57 -0400 |
---|---|---|
committer | Julian Rex <julian.rex@mapbox.com> | 2019-04-16 11:38:41 -0400 |
commit | fdaf5b313dcec7d5badf0dc192135135cb18dd14 (patch) | |
tree | e70f014d81a0bc6546413b05249f34186a398a5e | |
parent | e3d544ee1a34532681e746b90ff15a66fa17f535 (diff) | |
download | qtlocation-mapboxgl-fdaf5b313dcec7d5badf0dc192135135cb18dd14.tar.gz |
[ios] Call Renderer::flush on resign active/enter background
-rw-r--r-- | platform/darwin/src/MGLRendererFrontend.h | 8 | ||||
-rw-r--r-- | platform/ios/Integration Tests/MGLBackgroundIntegrationTest.m | 99 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 117 |
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 |