Unverified Commit 75555658 authored by Elijah Semyonov's avatar Elijah Semyonov Committed by GitHub

Restrict redrawing when the application is background. (#790)

* Pause metal redrawer when app is background.

* Add comment

* Add logic for synchronous scheduling

* Add logic for synchronous scheduling

* Add correct initial state
parent 86d0d0a2
......@@ -3,12 +3,101 @@ package org.jetbrains.skiko.redrawer
import kotlinx.cinterop.*
import org.jetbrains.skia.*
import org.jetbrains.skiko.Logger
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSRunLoop
import platform.Foundation.NSSelectorFromString
import platform.Metal.MTLCommandBufferProtocol
import platform.QuartzCore.*
import platform.UIKit.UIApplication
import platform.UIKit.UIApplicationDidEnterBackgroundNotification
import platform.UIKit.UIApplicationState
import platform.UIKit.UIApplicationWillEnterForegroundNotification
import platform.darwin.*
import kotlin.math.roundToInt
import kotlin.native.ref.WeakReference
private class DisplayLinkConditions(
val setPausedCallback: (Boolean) -> Unit
) {
/**
* see [MetalRedrawer.needsProactiveDisplayLink]
*/
var needsToBeProactive: Boolean = false
set(value) {
field = value
update()
}
/**
* Indicates that scene is invalidated and next display link callback will draw
*/
var needsRedrawOnNextVsync: Boolean = false
set(value) {
field = value
update()
}
/**
* Indicates that application is running foreground now
*/
var isApplicationActive: Boolean = false
set(value) {
field = value
update()
}
private fun update() {
val isUnpaused = isApplicationActive && (needsToBeProactive || needsRedrawOnNextVsync)
setPausedCallback(!isUnpaused)
}
}
private class ApplicationStateListener(
/**
* Callback which will be called with `true` when the app becomes active, and `false` when the app goes background
*/
private val callback: (Boolean) -> Unit
) : NSObject() {
init {
val notificationCenter = NSNotificationCenter.defaultCenter
notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationWillEnterForeground.name),
UIApplicationWillEnterForegroundNotification,
null
)
notificationCenter.addObserver(
this,
NSSelectorFromString(::applicationDidEnterBackground.name),
UIApplicationDidEnterBackgroundNotification,
null
)
}
@ObjCAction
fun applicationWillEnterForeground() {
callback(true)
}
@ObjCAction
fun applicationDidEnterBackground() {
callback(false)
}
/**
* Deregister from [NSNotificationCenter]
*/
fun dispose() {
val notificationCenter = NSNotificationCenter.defaultCenter
notificationCenter.removeObserver(this, UIApplicationWillEnterForegroundNotification, null)
notificationCenter.removeObserver(this, UIApplicationDidEnterBackgroundNotification, null)
}
}
internal class MetalRedrawer(
private val metalLayer: CAMetalLayer,
......@@ -25,6 +114,7 @@ internal class MetalRedrawer(
?: throw IllegalStateException("CAMetalLayer.device can not be null")
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private val context = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
private val inflightCommandBuffers = mutableListOf<MTLCommandBufferProtocol>()
// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
private val inflightSemaphore = dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())
......@@ -35,22 +125,14 @@ internal class MetalRedrawer(
caDisplayLink?.preferredFramesPerSecond = value
}
/*
* Indicates that scene is invalidated and next display link callback will draw
*/
private var hasScheduledDrawOnNextVSync = false
/**
* Needs scheduling displayLink for forcing UITouch events to come at the fastest possible cadence.
* Otherwise, touch events can come at rate lower than actual display refresh rate.
*/
var needsProactiveDisplayLink = false
var needsProactiveDisplayLink: Boolean
get() = displayLinkConditions.needsToBeProactive
set(value) {
field = value
if (value) {
caDisplayLink?.setPaused(false)
}
displayLinkConditions.needsToBeProactive = value
}
internal var caDisplayLink: CADisplayLink? = CADisplayLink.displayLinkWithTarget(
......@@ -60,9 +142,31 @@ internal class MetalRedrawer(
selector = NSSelectorFromString(DisplayLinkProxy::handleDisplayLinkTick.name)
)
private val displayLinkConditions = DisplayLinkConditions { paused ->
caDisplayLink?.setPaused(paused)
}
private val applicationStateListener = ApplicationStateListener { isApplicationActive ->
displayLinkConditions.isApplicationActive = isApplicationActive
if (!isApplicationActive) {
// If application goes background, synchronously schedule all inflightCommandBuffers, as per
// https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/preparing_your_metal_app_to_run_in_the_background?language=objc
inflightCommandBuffers.forEach {
// Will immediately return for MTLCommandBuffer's which are not in `Commited` status
it.waitUntilScheduled()
}
}
}
init {
val caDisplayLink = caDisplayLink ?: throw IllegalStateException("caDisplayLink is null during redrawer init")
caDisplayLink.setPaused(true)
// UIApplication can be in UIApplicationStateInactive state (during app launch before it gives control back to run loop)
// and won't receive UIApplicationWillEnterForegroundNotification
// so we compare the state with UIApplicationStateBackground instead of UIApplicationStateActive
displayLinkConditions.isApplicationActive = UIApplication.sharedApplication.applicationState != UIApplicationState.UIApplicationStateBackground
if (addDisplayLinkToRunLoop == null) {
caDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop, NSRunLoop.mainRunLoop.currentMode)
} else {
......@@ -71,9 +175,12 @@ internal class MetalRedrawer(
}
internal fun dispose() {
check(caDisplayLink != null, { "MetalRedrawer.dispose() was called more than once" } )
check(caDisplayLink != null, { "MetalRedrawer.dispose() was called more than once" })
disposeCallback(this)
applicationStateListener.dispose()
caDisplayLink?.invalidate()
caDisplayLink = null
......@@ -82,27 +189,20 @@ internal class MetalRedrawer(
}
internal fun needRedraw() {
hasScheduledDrawOnNextVSync = true
// If caDisplayLink is proactive (touches tracking), this does nothing (already unpaused)
caDisplayLink?.setPaused(false)
displayLinkConditions.needsRedrawOnNextVsync = true
}
private fun handleDisplayLinkTick() {
if (hasScheduledDrawOnNextVSync) {
hasScheduledDrawOnNextVSync = false
if (displayLinkConditions.needsRedrawOnNextVsync) {
displayLinkConditions.needsRedrawOnNextVsync = false
draw()
}
if (!needsProactiveDisplayLink) {
caDisplayLink?.setPaused(true)
}
}
private fun draw() {
if (caDisplayLink == null) {
Logger.warn { "caDisplayLink callback called after it was invalidated "}
Logger.warn { "caDisplayLink callback called after it was invalidated " }
return
}
......@@ -160,6 +260,13 @@ internal class MetalRedrawer(
surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
if (inflightCommandBuffers.size == metalLayer.maximumDrawableCount.toInt()) {
inflightCommandBuffers.removeAt(0)
}
inflightCommandBuffers.add(commandBuffer)
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment