Unverified Commit 81f1a7ab authored by Elijah Semyonov's avatar Elijah Semyonov Committed by GitHub

iOS frames scheduling fix (#714)

* Make rundown rendering bandaid(or not) fix to avoid incorrect scheduling problem

* Remove unneeded counter check.

* Extract magic value. Document it.

* Remove bandaid.

* Make right scheduling.

* Fix for path on double-dispatch during vsync-frame.

* Change logging a bit.

* Update doc

* Update doc

* Add comment on touchesCount, rephrase the body.

* Replace control flow with `also`

* Remove debug logs.

* Refactor 2 booleans into single enum.

* Simplify enum

* Replace Unit with empty body in `when` expressions.

* Replace comment style in function scopes.

* Make DrawSchedulingState private.

* Change comment style on private property.

* Add rationale behind drawSchedulingState initial value.
parent 53f70be8
...@@ -181,13 +181,32 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, ...@@ -181,13 +181,32 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol,
return _pointInside(skiaPoint, withEvent) return _pointInside(skiaPoint, withEvent)
} }
/*
* When there at least one tracked touch, we need notify redrawer about it. It should schedule CADisplayLink which
* affects frequency of polling UITouch events on high frequency display and forces it to match display refresh rate.
*/
private var touchesCount = 0
set(value) {
field = value
val needHighFrequencyPolling = value > 0
skiaLayer?.redrawer?.needsProactiveDisplayLink = needHighFrequencyPolling
}
override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) { override fun touchesBegan(touches: Set<*>, withEvent: UIEvent?) {
super.touchesBegan(touches, withEvent) super.touchesBegan(touches, withEvent)
touchesCount += touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.DOWN)
} }
override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) { override fun touchesEnded(touches: Set<*>, withEvent: UIEvent?) {
super.touchesEnded(touches, withEvent) super.touchesEnded(touches, withEvent)
touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
...@@ -198,6 +217,9 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, ...@@ -198,6 +217,9 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol,
override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) { override fun touchesCancelled(touches: Set<*>, withEvent: UIEvent?) {
super.touchesCancelled(touches, withEvent) super.touchesCancelled(touches, withEvent)
touchesCount -= touches.size
sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP) sendTouchEventToSkikoView(withEvent!!, SkikoPointerEventKind.UP)
} }
...@@ -225,6 +247,8 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol, ...@@ -225,6 +247,8 @@ class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol,
platform = event platform = event
) )
) )
// If invalidation doesn't happen while onPointerEvent is processed, it's too late to schedule any work for this frame.
skiaLayer?.redrawer?.preventDrawDispatchDuringCurrentFrame()
} }
private val UITouch.isPressed get() = private val UITouch.isPressed get() =
......
...@@ -16,12 +16,15 @@ import platform.Metal.MTLCreateSystemDefaultDevice ...@@ -16,12 +16,15 @@ import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.* import platform.QuartzCore.*
import platform.UIKit.UIScreen
import platform.UIKit.UIView
import platform.UIKit.window import platform.UIKit.window
import platform.darwin.NSInteger
import platform.darwin.NSObject import platform.darwin.NSObject
private enum class DrawSchedulingState {
AVAILABLE_ON_NEXT_FRAME,
AVAILABLE_ON_CURRENT_FRAME,
SCHEDULED_ON_NEXT_FRAME
}
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer private val layer: SkiaLayer
) : Redrawer { ) : Redrawer {
...@@ -33,12 +36,62 @@ internal class MetalRedrawer( ...@@ -33,12 +36,62 @@ internal class MetalRedrawer(
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue") private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private var currentDrawable: CAMetalDrawableProtocol? = null private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer() private val metalLayer = MetalLayer()
/*
* Initial value is [DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME] because voluntarily dispatching a frame
* disregarding CADisplayLink timing (which is not accessible while it's paused) can cause frame drifting in worst
* cases adding one frame latency due to presentation mechanism, if followed by steady draw dispatch
* (which is often the case).
* TODO: look closer to what happens after blank frames leave it in AVAILABLE_ON_CURRENT_FRAME. Touch driven events sequence negate that problem.
*/
private var drawSchedulingState = DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME
/**
* UITouch events are dispatched right before next CADisplayLink callback by iOS.
* It's too late to encode any work for this frame after this happens.
* Any work dispatched before the next CADisplayLink callback should be scheduled after that callback.
*/
fun preventDrawDispatchDuringCurrentFrame() {
if (drawSchedulingState == DrawSchedulingState.AVAILABLE_ON_CURRENT_FRAME) {
drawSchedulingState = DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME
}
}
/**
* 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
set(value) {
field = value
if (value) {
caDisplayLink.setPaused(false)
}
}
private val frameListener: NSObject = FrameTickListener { private val frameListener: NSObject = FrameTickListener {
when (drawSchedulingState) {
DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME -> {
drawSchedulingState = DrawSchedulingState.AVAILABLE_ON_CURRENT_FRAME
}
DrawSchedulingState.SCHEDULED_ON_NEXT_FRAME -> {
drawIfLayerIsShowing()
drawSchedulingState = DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME
}
DrawSchedulingState.AVAILABLE_ON_CURRENT_FRAME -> {
// still available, do nothing
}
}
if (!needsProactiveDisplayLink) {
caDisplayLink.setPaused(true) caDisplayLink.setPaused(true)
if (layer.isShowing()) {
draw()
} }
} }
private val caDisplayLink = CADisplayLink.displayLinkWithTarget( private val caDisplayLink = CADisplayLink.displayLinkWithTarget(
target = frameListener, target = frameListener,
selector = NSSelectorFromString(FrameTickListener::onDisplayLinkTick.name) selector = NSSelectorFromString(FrameTickListener::onDisplayLinkTick.name)
...@@ -82,14 +135,46 @@ internal class MetalRedrawer( ...@@ -82,14 +135,46 @@ internal class MetalRedrawer(
override fun needRedraw() { override fun needRedraw() {
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
drawImmediatelyIfPossible()
if (drawSchedulingState == DrawSchedulingState.SCHEDULED_ON_NEXT_FRAME) {
caDisplayLink.setPaused(false) caDisplayLink.setPaused(false)
} }
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
draw() draw()
} }
/*
* Dispatch redraw immediately during current frame if possible and updates [drawSchedulingState] to relevant value
*/
private fun drawImmediatelyIfPossible() {
when (drawSchedulingState) {
DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME -> {
drawSchedulingState = DrawSchedulingState.SCHEDULED_ON_NEXT_FRAME
}
DrawSchedulingState.AVAILABLE_ON_CURRENT_FRAME -> {
drawIfLayerIsShowing()
drawSchedulingState = DrawSchedulingState.AVAILABLE_ON_NEXT_FRAME
}
DrawSchedulingState.SCHEDULED_ON_NEXT_FRAME -> {
// already scheduled, do nothing
}
}
}
private fun drawIfLayerIsShowing() {
if (layer.isShowing()) {
draw()
}
}
private fun draw() { private fun draw() {
// TODO: maybe make flush async as in JVM version. // TODO: maybe make flush async as in JVM version.
autoreleasepool { //todo measure performance without autoreleasepool autoreleasepool { //todo measure performance without autoreleasepool
......
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