Skip to content

Commit

Permalink
Postpone drawable acqusition
Browse files Browse the repository at this point in the history
  • Loading branch information
elijah-semyonov committed Sep 15, 2023
1 parent aa5b9ed commit 072252b
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,14 @@ class ComposeScene internal constructor(
* Render the current content on [canvas]. Passed [nanoTime] will be used to drive all
* animations in the content (or any other code, which uses [withFrameNanos]
*/
fun render(canvas: Canvas, nanoTime: Long): Unit = postponeInvalidation {
@Deprecated("Use `fun render(retrieveCanvas: () -> Canvas?, nanoTime: Long): Unit` instead")
fun render(canvas: Canvas, nanoTime: Long): Unit = render(retrieveCanvas = { canvas }, nanoTime)

/**
* Flush async operations, notify all animations and perform recomposition-layout-draw sequence.
* [Canvas] to draw on is retrieved lazily to postpone acquiring drawable as late as possible.
*/
fun render(retrieveCanvas: () -> Canvas?, nanoTime: Long): Unit = postponeInvalidation {
recomposeDispatcher.flush()
frameClock.sendFrame(nanoTime) // Recomposition
sendAndPerformSnapshotChanges() // Apply changes from recomposition phase to layout phase
Expand All @@ -526,7 +533,11 @@ class ComposeScene internal constructor(
syntheticEventSender.updatePointerPosition()
sendAndPerformSnapshotChanges() // Apply changes from layout phase to draw phase
needDraw = false
forEachOwner { it.draw(canvas) }

retrieveCanvas()?.let { canvas ->
forEachOwner { it.draw(canvas) }
}

forEachOwner { it.clearInvalidObservations() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 @@ -637,7 +638,7 @@ internal actual class ComposeWindow : UIViewController {
override fun retrieveCATransactionCommands(): List<() -> Unit> =
interopContext.getActionsAndClear()

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@ internal interface MetalRedrawerCallbacks {
/**
* Draw into a surface.
*
* @param surface The surface to be drawn.
* @param retrieveCanvas Callback to lazily retrieve canvas to postpone drawable acquiring.
* @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(retrieveCanvas: () -> Canvas?, targetTimestamp: NSTimeInterval)

/**
* Retrieve a list of pending actions which need to be synchronized with Metal rendering using CATransaction mechanism.
Expand Down Expand Up @@ -272,6 +272,52 @@ internal class MetalRedrawer(
draw(waitUntilCompletion = true, CACurrentMediaTime())
}

private data class FrameRenderTarget(
val drawable: CAMetalDrawableProtocol,
val renderTarget: BackendRenderTarget,
val surface: Surface
) {
fun dispose() {
surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
}
}

private fun createFrameRenderTarget(width: Int, height: Int): FrameRenderTarget? {
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." }
dispatch_semaphore_signal(inflightSemaphore)
return null
}

val renderTarget =
BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())

val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)

if (surface == null) {
renderTarget.close()
// TODO: manually release metalDrawable when K/N API arrives
dispatch_semaphore_signal(inflightSemaphore)
return null
}

return FrameRenderTarget(metalDrawable, renderTarget, surface)
}

private fun draw(waitUntilCompletion: Boolean, targetTimestamp: NSTimeInterval) {
check(NSThread.isMainThread)

Expand All @@ -286,41 +332,21 @@ internal class MetalRedrawer(
return@autoreleasepool
}

dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
var frameRenderTarget: FrameRenderTarget? = null

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." }
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
// Lambda which overwrites local frameRenderTarget if called, clears [Canvas] and returns it to the callee
val retrieveCanvas = {
createFrameRenderTarget(width, height)?.also {
frameRenderTarget = it
it.surface.canvas.clear(Color.WHITE)
}?.surface?.canvas
}

val renderTarget =
BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())

val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)

if (surface == null) {
// TODO: anomaly, log
// Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
renderTarget.close()
// TODO: manually release metalDrawable when K/N API arrives
dispatch_semaphore_signal(inflightSemaphore)
return@autoreleasepool
}
callbacks.render(retrieveCanvas, lastRenderTimestamp)

surface.canvas.clear(Color.WHITE)
callbacks.draw(surface, lastRenderTimestamp)
surface.flushAndSubmit()
// Wrap up all the rendering work, if callee of retrieveCanvas encoded any work, return otherwise
val nonNullFrameRenderTarget = frameRenderTarget ?: return@autoreleasepool
nonNullFrameRenderTarget.surface.flushAndSubmit()

val caTransactionCommands = callbacks.retrieveCATransactionCommands()
val presentsWithTransaction =
Expand All @@ -333,7 +359,7 @@ internal class MetalRedrawer(

if (!presentsWithTransaction) {
// If there are no pending changes in UIKit interop, present the drawable ASAP
commandBuffer.presentDrawable(metalDrawable)
commandBuffer.presentDrawable(nonNullFrameRenderTarget.drawable)
}

commandBuffer.addCompletedHandler {
Expand All @@ -346,14 +372,12 @@ internal class MetalRedrawer(
// If there are pending changes in UIKit interop, [waitUntilScheduled](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443036-waituntilscheduled) is called
// to ensure that transaction is available
commandBuffer.waitUntilScheduled()
metalDrawable.present()
nonNullFrameRenderTarget.drawable.present()
caTransactionCommands.fastForEach { it.invoke() }
CATransaction.flush()
}

surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
nonNullFrameRenderTarget.dispose()

// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
if (inflightCommandBuffers.size == metalLayer.maximumDrawableCount.toInt()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import platform.UIKit.*
import platform.darwin.NSInteger
import kotlin.math.max
import kotlin.math.min
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Surface
import org.jetbrains.skiko.SkikoInputModifiers
import org.jetbrains.skiko.SkikoKey
Expand All @@ -51,7 +52,7 @@ internal interface SkikoUIViewDelegate {

fun retrieveCATransactionCommands(): List<() -> Unit>

fun draw(surface: Surface, targetTimestamp: NSTimeInterval)
fun render(retrieveCanvas: () -> 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(retrieveCanvas: () -> Canvas?, targetTimestamp: NSTimeInterval) {
delegate?.render(retrieveCanvas, targetTimestamp)
}

override fun retrieveCATransactionCommands(): List<() -> Unit> =
Expand Down

0 comments on commit 072252b

Please sign in to comment.