Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS modify invalidation logic #797

Merged
merged 11 commits into from
Sep 13, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package bugs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move it to more specific package?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to, it's our sandbox

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package already exists, so if want to rename it we should do it in a separate PR.

I don't have preference to either naming style (bugs or androidx.compose.mpp.demo.bugs). Both are fine to me.


import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.mpp.demo.Screen
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import kotlinx.coroutines.delay

val DispatchersMainDelayCheck = Screen.Example("DispatchersMainDelayCheck") {
var counter by remember { mutableStateOf(0) }

LazyColumn(Modifier.fillMaxSize()) {
items(100) { index ->
Text("${index + counter}")
}
}

LaunchedEffect(Unit) {
while (true) {
delay(1000)

counter += 1
}
}
}
3 changes: 2 additions & 1 deletion compose/mpp/demo/src/uikitMain/kotlin/bugs/IosBugs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ val IosBugs = Screen.Selection(
UIKitViewAndDropDownMenu,
KeyboardEmptyWhiteSpace,
KeyboardPasswordType,
UIKitRenderSync
UIKitRenderSync,
DispatchersMainDelayCheck
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.toCompose
import androidx.compose.ui.platform.Platform
import androidx.compose.ui.unit.Density
import kotlinx.coroutines.CoroutineDispatcher
import org.jetbrains.skia.Canvas
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.DpRect
import androidx.compose.ui.unit.toDpRect
import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.Point
import org.jetbrains.skiko.*

Expand Down Expand Up @@ -113,7 +113,7 @@ internal class ComposeLayer(
}

private val scene = ComposeScene(
coroutineContext = getMainDispatcher(),
coroutineContext = Dispatchers.Main,
platform = platform,
density = density,
invalidate = layer::needRedraw,
Expand Down Expand Up @@ -179,8 +179,6 @@ internal class ComposeLayer(
}
}

internal expect fun getMainDispatcher(): CoroutineDispatcher

private fun currentMillis() = (currentNanoTime() / 1E6).toLong()


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,4 @@
*/
package androidx.compose.ui.native

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineDispatcher

internal actual fun getMainDispatcher(): CoroutineDispatcher = Dispatchers.Main

internal actual val supportsMultitouch: Boolean get() = false
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import androidx.compose.ui.interop.LocalLayerContainer
import androidx.compose.ui.interop.LocalUIKitInteropContext
import androidx.compose.ui.interop.LocalUIViewController
import androidx.compose.ui.interop.UIKitInteropContext
import androidx.compose.ui.native.getMainDispatcher
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.uikit.*
Expand All @@ -46,6 +45,7 @@ import kotlinx.cinterop.ExportObjCClass
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.Surface
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
Expand Down Expand Up @@ -586,7 +586,7 @@ internal actual class ComposeWindow : UIViewController {
}

val scene = ComposeScene(
coroutineContext = getMainDispatcher(),
coroutineContext = Dispatchers.Main,
platform = platform,
density = density,
invalidate = skikoUIView::needRedraw,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,56 @@ private class DisplayLinkConditions(
}

/**
* Indicates that scene is invalidated and next display link callback will draw
* Indicates that application is running foreground now
*/
var needsRedrawOnNextVsync: Boolean = false
var isApplicationActive: Boolean = false
set(value) {
field = value

update()
}

/**
* Indicates that application is running foreground now
* Number of subsequent vsync that will issue a draw
*/
var isApplicationActive: Boolean = false
private var scheduledRedrawsCount = 0
set(value) {
field = value

update()
}

/**
* Handle display link callback by updating internal state and dispatching the draw, if needed.
*/
inline fun onDisplayLinkTick(draw: () -> Unit) {
if (scheduledRedrawsCount > 0) {
scheduledRedrawsCount -= 1
draw()
}
}

/**
* Mark next [FRAMES_COUNT_TO_SCHEDULE_ON_NEED_REDRAW] frames to issue a draw dispatch and unpause displayLink if needed.
*/
fun needRedraw() {
scheduledRedrawsCount = FRAMES_COUNT_TO_SCHEDULE_ON_NEED_REDRAW
}

private fun update() {
val isUnpaused = isApplicationActive && (needsToBeProactive || needsRedrawOnNextVsync)
val isUnpaused = isApplicationActive && (needsToBeProactive || scheduledRedrawsCount > 0)
setPausedCallback(!isUnpaused)
}

companion object {
/**
* Right now `needRedraw` doesn't reentry from within `draw` callback during animation which leads to a situation where CADisplayLink is first paused
* and then asynchronously unpaused. This effectively makes Pro Motion display lose a frame before running on highest possible frequency again.
* To avoid this, we need to render at least two frames (instead of just one) after each `needRedraw` assuming that invalidation comes inbetween them and
* displayLink is not paused by the end of RuntimeLoop tick.
*/
const val FRAMES_COUNT_TO_SCHEDULE_ON_NEED_REDRAW = 2
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
}
}

private class ApplicationStateListener(
Expand Down Expand Up @@ -174,13 +201,20 @@ internal class MetalRedrawer(
*/
private var caDisplayLink: CADisplayLink? = CADisplayLink.displayLinkWithTarget(
target = DisplayLinkProxy {
this.handleDisplayLinkTick()
val targetTimestamp = currentTargetTimestamp ?: return@DisplayLinkProxy

displayLinkConditions.onDisplayLinkTick {
draw(waitUntilCompletion = false, targetTimestamp)
}
},
selector = NSSelectorFromString(DisplayLinkProxy::handleDisplayLinkTick.name)
)

private val currentTargetTimestamp: NSTimeInterval?
get() = caDisplayLink?.targetTimestamp

private val displayLinkConditions = DisplayLinkConditions { paused ->
caDisplayLink?.setPaused(paused)
caDisplayLink?.paused = paused
}

private val applicationStateListener = ApplicationStateListener { isApplicationActive ->
Expand Down Expand Up @@ -225,19 +259,7 @@ internal class MetalRedrawer(
* Marks current state as dirty and unpauses display link if needed and enables draw dispatch operation on
* next vsync
*/
fun needRedraw() {
displayLinkConditions.needsRedrawOnNextVsync = true
}

private fun handleDisplayLinkTick() {
if (displayLinkConditions.needsRedrawOnNextVsync) {
displayLinkConditions.needsRedrawOnNextVsync = false

val targetTimestamp = caDisplayLink?.targetTimestamp ?: return

draw(waitUntilCompletion = false, targetTimestamp)
}
}
fun needRedraw() = displayLinkConditions.needRedraw()

/**
* Immediately dispatch draw and block the thread until it's finished and presented on the screen.
Expand Down