diff --git a/platform/ios/platform/ios/src/MGLMapView.mm b/platform/ios/platform/ios/src/MGLMapView.mm index 07d9e2300..351ab0449 100644 --- a/platform/ios/platform/ios/src/MGLMapView.mm +++ b/platform/ios/platform/ios/src/MGLMapView.mm @@ -248,7 +248,9 @@ public: @property (nonatomic) CGFloat scale; @property (nonatomic) CGFloat angle; @property (nonatomic) CGFloat quickZoomStart; +/// Dormant means there is no underlying GL view (typically in the background) @property (nonatomic, getter=isDormant) BOOL dormant; +@property (nonatomic, readonly, getter=isDisplayLinkActive) BOOL displayLinkActive; @property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed; @property (nonatomic) CGFloat rotationThresholdWhileZooming; @property (nonatomic) CGFloat rotationBeforeThresholdMet; @@ -283,6 +285,11 @@ public: @property (nonatomic, assign) UIEdgeInsets safeMapViewContentInsets; @property (nonatomic, strong) NSNumber *automaticallyAdjustContentInsetHolder; +// Display Link +@property (nonatomic, weak) UIScreen *displayLinkScreen; +@property (nonatomic) CADisplayLink *displayLink; +@property (nonatomic, assign) BOOL needsDisplayRefresh; + @end @implementation MGLMapView @@ -313,9 +320,6 @@ public: CLLocationDegrees _pendingLatitude; CLLocationDegrees _pendingLongitude; - CADisplayLink *_displayLink; - BOOL _needsDisplayRefresh; - NSInteger _changeDelimiterSuppressionDepth; /// Center of the pinch gesture on the previous iteration of the gesture. @@ -495,7 +499,7 @@ public: NSAssert(!_mbglMap, @"_mbglMap should be NULL"); _mbglMap = std::make_unique(*_rendererFrontend, *_mbglView, mapOptions, resourceOptions); - // start paused if in IB + // start paused if launch into the background if (background) { self.dormant = YES; } @@ -730,7 +734,7 @@ public: [self removeAnnotations:annotations]; } - [self validateDisplayLink]; + [self destroyDisplayLink]; [self destroyCoreObjects]; @@ -1179,7 +1183,7 @@ public: { // Only add a block if the display link (that calls processPendingBlocks) is // running, otherwise fall back to calling immediately. - if (_displayLink && !_displayLink.isPaused) + if (self.isDisplayLinkActive) { [self willChangeValueForKey:@"pendingCompletionBlocks"]; [self.pendingCompletionBlocks addObject:block]; @@ -1245,9 +1249,9 @@ public: return; } - if (_needsDisplayRefresh || (self.pendingCompletionBlocks.count > 0)) + if (self.needsDisplayRefresh || (self.pendingCompletionBlocks.count > 0)) { - _needsDisplayRefresh = NO; + self.needsDisplayRefresh = NO; // UIView update logic has moved into `renderSync` above, which now gets // triggered by a call to setNeedsDisplay. @@ -1282,7 +1286,7 @@ public: { MGLAssertIsMainThread(); - _needsDisplayRefresh = YES; + self.needsDisplayRefresh = YES; } - (void)willTerminate @@ -1291,14 +1295,50 @@ public: if ( ! self.dormant) { - [self validateDisplayLink]; + [self.displayLink invalidate]; + self.displayLink = nil; + self.dormant = YES; + + if (_rendererFrontend) { + _rendererFrontend->reduceMemoryUse(); + } + _mbglView->deleteView(); } [self destroyCoreObjects]; } +- (UIScreen *)windowScreen { + UIScreen *screen; + +#ifdef SUPPORT_UIWINDOWSCENE + if (@available(iOS 13.0, *)) { + if (self.window.windowScene) { + screen = self.window.windowScene.screen; + } + } +#endif + + // Fallback if there's no windowScene + if (!screen) { + screen = self.window.screen; + } + + return screen; +} + +- (BOOL)isVisible +{ + // "Visible" is not strictly true here - for example, the view hierarchy is not + // currently observed (e.g. looking at a parent's or the window's hidden + // status. + // This does NOT take application state into account + UIScreen *screen = [self windowScreen]; + return (!self.isHidden && screen); +} + - (void)validateDisplayLink { BOOL isVisible = self.superview && self.window; @@ -1312,7 +1352,7 @@ public: _displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(updateFromDisplayLink:)]; [self updateDisplayLinkPreferredFramesPerSecond]; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - _needsDisplayRefresh = YES; + self.needsDisplayRefresh = YES; [self updateFromDisplayLink:_displayLink]; } else if ( ! isVisible && _displayLink) @@ -1395,22 +1435,47 @@ public: // slow down. The exact cause of this is unknown, but this work around // appears to lessen the effects. _mbglView->setPresentsWithTransaction(NO); + } + + // Changing windows regardless of whether it's a new one, or the map is being + // removed from the hierarchy + [self destroyDisplayLink]; - // Moved from didMoveToWindow - [self validateDisplayLink]; + if (self.window) { +#ifdef SUPPORT_UIWINDOWSCENE + if (@available(iOS 13.0, *)) + { + [self.window removeObserver:self forKeyPath:@"windowScene" context:windowScreenContext]; + } + else +#endif + { + [self.window removeObserver:self forKeyPath:@"screen" context:windowScreenContext]; + } } } - (void)didMoveToWindow { [super didMoveToWindow]; - + MGLLogDebug(@"[%p] window=%p", self, self.window); + if (self.window) { // See above comment - _mbglView->setPresentsWithTransaction(self.enablePresentsWithTransaction); + [self resumeRenderingIfNecessary]; + [self updatePresentsWithTransaction]; - [self validateDisplayLink]; +#ifdef SUPPORT_UIWINDOWSCENE + if (@available(iOS 13.0, *)) + { + [self.window addObserver:self forKeyPath:@"windowScene" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:windowScreenContext]; + } + else +#endif + { + [self.window addObserver:self forKeyPath:@"screen" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:windowScreenContext]; + } } } @@ -1518,16 +1583,75 @@ public: [self setNeedsLayout]; } +- (void)stopDisplayLink +{ + MGLLogDebug(@"[%p]", self); + self.displayLink.paused = YES; + self.needsDisplayRefresh = NO; + [self processPendingBlocks]; +} + +- (void)createDisplayLink +{ + MGLLogDebug(@"[%p]", self); + + // Create and start the display link in a *paused* state + MGLAssert(!self.displayLinkScreen, @""); + MGLAssert(!self.displayLink, @""); + MGLAssert(self.window, @""); + MGLAssert(self.window.screen, @""); + + self.displayLinkScreen = self.window.screen; + self.displayLink = [self.window.screen displayLinkWithTarget:self selector:@selector(updateFromDisplayLink:)]; + self.displayLink.paused = YES; + + [self updateDisplayLinkPreferredFramesPerSecond]; + + [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + + if (_mbglMap && self.mbglMap.getMapOptions().constrainMode() == mbgl::ConstrainMode::None) + { + self.mbglMap.setConstrainMode(mbgl::ConstrainMode::HeightOnly); + } +} + +- (void)destroyDisplayLink +{ + MGLLogDebug(@"[%p]", self); + [self.displayLink invalidate]; + self.displayLink = nil; + self.displayLinkScreen = nil; + self.needsDisplayRefresh = NO; + [self processPendingBlocks]; +} + +- (void)startDisplayLink +{ + MGLLogDebug(@"[%p]", self); + MGLAssert(self.displayLink, @""); + MGLAssert([self isVisible], @"Display link should only be started when allowed"); + + self.displayLink.paused = NO; + [self setNeedsRerender]; + [self updateFromDisplayLink:self.displayLink]; +} + #pragma mark - Application lifecycle - (void)willResignActive:(NSNotification *)notification { - if ([self supportsBackgroundRendering]) - { + MGLAssertIsMainThread(); + MGLLogDebug(@"[%p]", self); + + // Going from active to inactive states. This could be because a system dialog + // has been displayed, control center, or the app is headed into the background + + if (self.supportsBackgroundRendering) { return; } - - self.lastSnapshotImage = _mbglView->snapshot(); - + + // We want to pause the rendering + [self stopDisplayLink]; + // 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 // reduceMemoryUse(), calls performCleanup(), which calls glFinish @@ -1539,18 +1663,83 @@ public: - (void)didEnterBackground:(NSNotification *)notification { - [self pauseRendering:notification]; + MGLAssertIsMainThread(); + MGLAssert(!self.dormant, @"Should not be dormant heading into background"); + MGLLogDebug(@"[%p] dormant=%d", self, self.dormant); + + // See comment in `supportsBackgroundRendering` above. + if (self.supportsBackgroundRendering) { + return; + } + + // We now want to stop rendering. + // We want to pause the rendering + [self stopDisplayLink]; + + // 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 + // reduceMemoryUse(), calls performCleanup(), which calls glFinish + if (_rendererFrontend) + { + _rendererFrontend->reduceMemoryUse(); + } + + // We now completely remove the display link, and the renderable resource. + // Although the method below is called `deleteView` this does NOT delete the + // GLKView, instead releasing the memory hungry resources. + [self destroyDisplayLink]; + [self processPendingBlocks]; + _mbglView->deleteView(); + + self.dormant = YES; + + // We want to add a snapshot image over the top of the map view, so that + // there are no glitches when the application comes back into the foreground + + [self enableSnapshotView]; + + // Handle non-rendering issues. + [self validateLocationServices]; } - (void)willEnterForeground:(NSNotification *)notification { - // Do nothing, currently if resumeRendering is called here it's a no-op. + MGLLogDebug(@"[%p] dormant=%d", self, self.dormant); + + // We're transitioning from Background to Inactive states + + if (self.supportsBackgroundRendering) { + return; + } + + // Reverse the process of going into the background + _mbglView->createView(); + + // A display link needs the window's screen, so create it if we can + UIScreen *screen = [self windowScreen]; + + if (screen) { + [self createDisplayLink]; + + // If we can render during the inactive state, start the display link now + if (self.isVisible) { + [self startDisplayLink]; + } + } + self.dormant = NO; + // Note: We do not remove the snapshot view (if there is one) until we have become + // active. + [self validateLocationServices]; } - (void)didBecomeActive:(NSNotification *)notification { - [self resumeRendering:notification]; - self.lastSnapshotImage = nil; + MGLLogDebug(@"[%p] DL.paused=<%p>.paused=%d", self, self.displayLink, self.displayLink.paused); + + // Most times, we should already have a display link created at this point, + // which may or may not be running. However, at the start of the application, + // it's possible to have a situation where the display link hasn't been created. + [self resumeRenderingIfNecessary]; } #pragma mark - GL / display link wake/sleep @@ -1561,106 +1750,108 @@ public: - (BOOL)supportsBackgroundRendering { + // Note: The following comment may be out of date with Metal enabled builds where + // OpenGL is being emulated with Metal. + // // 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. - return (self.window.screen != [UIScreen mainScreen]); + UIScreen *screen = [self windowScreen]; + + BOOL supportsBackgroundRendering = (screen && (screen != [UIScreen mainScreen])); + MGLLogDebug(@"supportsBackgroundRendering=%d",supportsBackgroundRendering); + return supportsBackgroundRendering; } -- (void)pauseRendering:(__unused NSNotification *)notification -{ - // 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 supportsBackgroundRendering]) +- (void)enableSnapshotView { + if (self.lastSnapshotImage) { - return; - } - - MGLLogInfo(@"Entering background."); - MGLAssertIsMainThread(); - - // Ideally we would wait until we actually received a memory warning but the bulk of the memory - // we have to release is tied up in GL buffers that we can't touch once we're in the background. - // 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. - if (_rendererFrontend) - { - _rendererFrontend->reduceMemoryUse(); - } - - if ( ! self.dormant) - { - self.dormant = YES; - - [self validateLocationServices]; - - _displayLink.paused = YES; - [self processPendingBlocks]; - - if (self.lastSnapshotImage) + if ( ! self.glSnapshotView) { - if ( ! self.glSnapshotView) - { - self.glSnapshotView = [[UIImageView alloc] initWithFrame: _mbglView->getView().frame]; - self.glSnapshotView.autoresizingMask = _mbglView->getView().autoresizingMask; - self.glSnapshotView.contentMode = UIViewContentModeCenter; - [self insertSubview:self.glSnapshotView aboveSubview:_mbglView->getView()]; - } - - self.glSnapshotView.image = self.lastSnapshotImage; - self.glSnapshotView.hidden = NO; - self.glSnapshotView.alpha = 1; - - if (self.debugMask && [self.glSnapshotView.subviews count] == 0) - { - UIView *snapshotTint = [[UIView alloc] initWithFrame:self.glSnapshotView.bounds]; - snapshotTint.autoresizingMask = self.glSnapshotView.autoresizingMask; - snapshotTint.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.25]; - [self.glSnapshotView addSubview:snapshotTint]; - } + self.glSnapshotView = [[UIImageView alloc] initWithFrame: _mbglView->getView().frame]; + self.glSnapshotView.autoresizingMask = _mbglView->getView().autoresizingMask; + self.glSnapshotView.contentMode = UIViewContentModeCenter; + [self insertSubview:self.glSnapshotView aboveSubview:_mbglView->getView()]; } - _mbglView->deleteView(); + self.glSnapshotView.image = self.lastSnapshotImage; + self.glSnapshotView.hidden = NO; + self.glSnapshotView.opaque = NO; + self.glSnapshotView.alpha = 1; + + if (self.debugMask && [self.glSnapshotView.subviews count] == 0) + { + UIView *snapshotTint = [[UIView alloc] initWithFrame:self.glSnapshotView.bounds]; + snapshotTint.autoresizingMask = self.glSnapshotView.autoresizingMask; + snapshotTint.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.25]; + [self.glSnapshotView addSubview:snapshotTint]; + } } } -- (void)resumeRendering:(__unused NSNotification *)notification -{ - MGLLogInfo(@"Entering foreground."); - MGLAssertIsMainThread(); +- (void)resumeRenderingIfNecessary { + MGLLogDebug(@"[%p] DL.paused=<%p>.paused=%d", self, self.displayLink, self.displayLink.paused); - if (self.dormant && [UIApplication sharedApplication].applicationState != UIApplicationStateBackground) - { - self.dormant = NO; + // Most times, we should already have a display link created at this point, + // which may or may not be running. However, at the start of the application, + // it's possible to have a situation where the display link hasn't been created. - _mbglView->createView(); + // Reverse the process of going into the background + if ([UIApplication sharedApplication].applicationState != UIApplicationStateBackground) { + if (self.dormant) { + _mbglView->createView(); + self.dormant = NO; + } - [UIView transitionWithView:self - duration:0.25 - options:UIViewAnimationOptionTransitionCrossDissolve - animations:^{ - self.glSnapshotView.hidden = YES; - } - completion:^(BOOL finished) { - [self.glSnapshotView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; - }]; - - _displayLink.paused = NO; - - [self validateLocationServices]; + // Check display link, if necessary + if (!self.displayLink) { + if ([self windowScreen]) { + [self createDisplayLink]; + } + } } + + // Start the display link if we need to + if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive || [UIApplication sharedApplication].applicationState == UIApplicationStateInactive) { + + BOOL mapViewVisible = self.isVisible; + if (self.displayLink) { + if (mapViewVisible && self.displayLink.isPaused) { + [self startDisplayLink]; + } + else if (!mapViewVisible && !self.displayLink.isPaused) { + // Unlikely scenario + [self stopDisplayLink]; + } + } + } + + // Reveal the snapshot view + + if (self.glSnapshotView && !self.glSnapshotView.isHidden) { + [UIView transitionWithView:self + duration:0.25 + options:UIViewAnimationOptionTransitionCrossDissolve + animations:^{ + self.glSnapshotView.hidden = YES; + } + completion:^(BOOL finished) { + [self.glSnapshotView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + }]; + } +} + +- (BOOL)isDisplayLinkActive { + MGLLogDebug(@"[%p]", self); + return (self.displayLink && !self.displayLink.isPaused); } - (void)setHidden:(BOOL)hidden { super.hidden = hidden; - _displayLink.paused = hidden; + _displayLink.paused = ![self isVisible]; if (hidden) { @@ -2585,6 +2776,8 @@ public: #pragma mark - Properties - +static void *windowScreenContext = &windowScreenContext; + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"coordinate"] && [object conformsToProtocol:@protocol(MGLAnnotation)] && ![object isKindOfClass:[MGLMultiPoint class]]) @@ -2635,6 +2828,14 @@ public: [self updateCalloutView]; } } + else if (context == windowScreenContext) + { + if ([keyPath isEqualToString:@"screen"] || + [keyPath isEqualToString:@"windowScene"]) { + [self destroyDisplayLink]; + [self didBecomeActive:nil]; + } + } } + (NSSet *)keyPathsForValuesAffectingZoomEnabled @@ -5089,7 +5290,7 @@ public: // of the `-[MGLCalloutView marginInsetsHintForPresentationFromRect:] // protocol method. bounds = CGRectInset(bounds, -calloutPositioningRect.size.width, -calloutPositioningRect.size.height); - } + } if (moveIntoView) { @@ -6990,6 +7191,7 @@ public: NSTimeInterval now = CACurrentMediaTime(); if (lastSnapshotTime == 0.0 || (now - lastSnapshotTime > MGLBackgroundSnapshotImageInterval)) { + MGLLogDebug(@"Taking snapshot"); self.lastSnapshotImage = _mbglView->snapshot(); lastSnapshotTime = now; } diff --git a/platform/ios/platform/ios/src/MGLMapView_Private.h b/platform/ios/platform/ios/src/MGLMapView_Private.h index bb34644f2..785585ba7 100644 --- a/platform/ios/platform/ios/src/MGLMapView_Private.h +++ b/platform/ios/platform/ios/src/MGLMapView_Private.h @@ -70,6 +70,7 @@ FOUNDATION_EXTERN MGL_EXPORT MGLExceptionName const _Nonnull MGLUnderlyingMapUna @property (nonatomic, nonnull) MGLUserLocationAnnotationView *userLocationAnnotationView; @property (nonatomic, nonnull) MGLAnnotationContainerView *annotationContainerView; @property (nonatomic, readonly) BOOL enablePresentsWithTransaction; +@property (nonatomic, assign) BOOL needsDisplayRefresh; - (BOOL) _opaque;