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 postpone CAMetalDrawable acqusition #820

Merged
merged 9 commits into from
Sep 19, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Surface
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
Expand Down Expand Up @@ -642,7 +643,7 @@ internal actual class ComposeWindow : UIViewController {
override fun retrieveInteropTransaction(): UIKitInteropTransaction =
interopContext.retrieve()

override fun draw(surface: Surface, targetTimestamp: NSTimeInterval) {
override fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) {
// The calculation is split in two instead of
// `(targetTimestamp * 1e9).toLong()`
// to avoid losing precision for fractional part
Expand All @@ -652,7 +653,7 @@ internal actual class ComposeWindow : UIViewController {
val nanos =
integral.roundToLong() * secondsToNanos + (fractional * 1e9).roundToLong()

scene.render(surface.canvas, nanos)
scene.render(canvas, nanos)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import platform.QuartzCore.*
import platform.UIKit.UIApplicationDidEnterBackgroundNotification
import platform.UIKit.UIApplicationWillEnterForegroundNotification
import platform.darwin.*
import kotlin.math.roundToInt
import org.jetbrains.skia.Rect
import platform.Foundation.NSTimeInterval
import platform.UIKit.UIApplication
import platform.UIKit.UIApplicationState
Expand Down Expand Up @@ -149,12 +151,12 @@ private class ApplicationStateListener(

internal interface MetalRedrawerCallbacks {
/**
* Draw into a surface.
* Perform time step and encode draw operations into canvas.
*
* @param surface The surface to be drawn.
* @param canvas Canvas to encode draw operations into.
* @param targetTimestamp Timestamp indicating the expected draw result presentation time. Implementation should forward its internal time clock to this targetTimestamp to achieve smooth visual change cadence.
*/
fun draw(surface: Surface, targetTimestamp: NSTimeInterval)
fun render(canvas: Canvas, targetTimestamp: NSTimeInterval)

/**
* Retrieve a transaction object, containing a list of pending actions
Expand All @@ -177,6 +179,7 @@ internal class MetalRedrawer(
private val context = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
private val inflightCommandBuffers = mutableListOf<MTLCommandBufferProtocol>()
private var lastRenderTimestamp: NSTimeInterval = CACurrentMediaTime()
private val pictureRecorder = PictureRecorder()

// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
private val inflightSemaphore =
Expand Down Expand Up @@ -267,6 +270,8 @@ internal class MetalRedrawer(
caDisplayLink?.invalidate()
caDisplayLink = null

pictureRecorder.close()

context.flush()
context.close()
}
Expand Down Expand Up @@ -302,13 +307,27 @@ internal class MetalRedrawer(
return@autoreleasepool
}

// Perform timestep and record all draw commands into [Picture]
pictureRecorder.beginRecording(Rect(
left = 0f,
top = 0f,
width.toFloat(),
height.toFloat()
)).also { canvas ->
canvas.clear(Color.WHITE)
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
callbacks.render(canvas, lastRenderTimestamp)
}

val picture = pictureRecorder.finishRecordingAsPicture()

dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)

val metalDrawable = metalLayer.nextDrawable()

if (metalDrawable == null) {
// TODO: anomaly, log
// Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
picture.close()
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
Expand All @@ -328,14 +347,14 @@ internal class MetalRedrawer(
if (surface == null) {
// TODO: anomaly, log
// Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
picture.close()
renderTarget.close()
// TODO: manually release metalDrawable when K/N API arrives
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}

surface.canvas.clear(Color.WHITE)
callbacks.draw(surface, lastRenderTimestamp)
surface.canvas.drawPicture(picture)
picture.close()
surface.flushAndSubmit()

val interopTransaction = callbacks.retrieveInteropTransaction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import platform.QuartzCore.CAMetalLayer
import platform.UIKit.*
import platform.darwin.NSInteger
import org.jetbrains.skia.Surface
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.SkikoInputModifiers
import org.jetbrains.skiko.SkikoKey
import org.jetbrains.skiko.SkikoKeyboardEvent
Expand All @@ -51,7 +52,7 @@ internal interface SkikoUIViewDelegate {

fun retrieveInteropTransaction(): UIKitInteropTransaction

fun draw(surface: Surface, targetTimestamp: NSTimeInterval)
fun render(canvas: Canvas, targetTimestamp: NSTimeInterval)
}

@Suppress("CONFLICTING_OVERLOADS")
Expand Down Expand Up @@ -79,8 +80,8 @@ internal class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
private val _redrawer: MetalRedrawer = MetalRedrawer(
_metalLayer,
callbacks = object : MetalRedrawerCallbacks {
override fun draw(surface: Surface, targetTimestamp: NSTimeInterval) {
delegate?.draw(surface, targetTimestamp)
override fun render(canvas: Canvas, targetTimestamp: NSTimeInterval) {
delegate?.render(canvas, targetTimestamp)
}

override fun retrieveInteropTransaction(): UIKitInteropTransaction =
Expand Down