Skip to content

Commit

Permalink
Move frame encoding to separate thread when possible (#829)
Browse files Browse the repository at this point in the history
## Proposed Changes

If no CATransaction sync is needed, perform Picture recorded commands
execution on a separate thread.
Fix a freeze on `waitUntilScheduled` if no transaction is available by
moving synchronization from interop scope (any UIView is present in the
composition) to per-frame scope (any UIView transaction is issued by
composition in current frame).

## Testing

Test: N/A

## Issues Fixed

Removes work from main thread, when possible, allowing Compose to run
there without waiting for CPU to finish encoding GPU commands.
A freeze on `waitUntilScheduled` during interop synchronization if no
transaction is available.

<img width="324" alt="Screenshot 2023-09-19 at 12 13 00"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/4167681/e54d83e3-ca58-43aa-a4a7-4764dd8ce841">
<img width="371" alt="Screenshot 2023-09-19 at 12 13 05"
src="https://github.com/JetBrains/compose-multiplatform-core/assets/4167681/8e4fcf0b-2b64-473e-a930-fae97a086a12">

## Depends on
#820
  • Loading branch information
elijah-semyonov committed Sep 26, 2023
1 parent 4d5d5a1 commit cf8b443
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ internal class UIKitInteropContext(
}
}

private inline fun <T> NSLock.doLocked(block: () -> T): T {
internal inline fun <T> NSLock.doLocked(block: () -> T): T {
lock()

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package androidx.compose.ui.window

import androidx.compose.ui.interop.UIKitInteropState
import androidx.compose.ui.interop.UIKitInteropTransaction
import androidx.compose.ui.interop.doLocked
import androidx.compose.ui.interop.isNotEmpty
import androidx.compose.ui.util.fastForEach
import kotlin.math.roundToInt
Expand All @@ -34,6 +35,7 @@ import platform.UIKit.UIApplicationWillEnterForegroundNotification
import platform.darwin.*
import kotlin.math.roundToInt
import org.jetbrains.skia.Rect
import platform.Foundation.NSLock
import platform.Foundation.NSTimeInterval
import platform.UIKit.UIApplication
import platform.UIKit.UIApplicationState
Expand Down Expand Up @@ -165,6 +167,27 @@ internal interface MetalRedrawerCallbacks {
fun retrieveInteropTransaction(): UIKitInteropTransaction
}

internal class InflightCommandBuffers(
private val maxInflightCount: Int
) {
private val lock = NSLock()
private val list = mutableListOf<MTLCommandBufferProtocol>()

fun waitUntilAllAreScheduled() = lock.doLocked {
list.fastForEach {
it.waitUntilScheduled()
}
}

fun add(commandBuffer: MTLCommandBufferProtocol) = lock.doLocked {
if (list.size == maxInflightCount) {
list.removeAt(0)
}

list.add(commandBuffer)
}
}

internal class MetalRedrawer(
private val metalLayer: CAMetalLayer,
private val callbacks: MetalRedrawerCallbacks,
Expand All @@ -177,13 +200,14 @@ internal class MetalRedrawer(
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>()
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 =
dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())
private val inflightCommandBuffers =
InflightCommandBuffers(metalLayer.maximumDrawableCount.toInt())

var isForcedToPresentWithTransactionEveryFrame = false

Expand Down Expand Up @@ -213,6 +237,7 @@ internal class MetalRedrawer(
// If active, make metalLayer transparent, opaque otherwise.
// Rendering into opaque CAMetalLayer allows direct-to-screen optimization.
metalLayer.setOpaque(!value)
metalLayer.drawsAsynchronously = !value
}

/**
Expand Down Expand Up @@ -242,10 +267,7 @@ internal class MetalRedrawer(
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()
}
inflightCommandBuffers.waitUntilAllAreScheduled()
}
}

Expand All @@ -271,8 +293,6 @@ internal class MetalRedrawer(
caDisplayLink = null

pictureRecorder.close()

context.flush()
context.close()
}

Expand Down Expand Up @@ -353,67 +373,76 @@ internal class MetalRedrawer(
return@autoreleasepool
}

surface.canvas.drawPicture(picture)
picture.close()
surface.flushAndSubmit()

val interopTransaction = callbacks.retrieveInteropTransaction()
if (interopTransaction.state == UIKitInteropState.BEGAN) {
isInteropActive = true
}
val presentsWithTransaction =
isForcedToPresentWithTransactionEveryFrame || isInteropActive
isForcedToPresentWithTransactionEveryFrame || interopTransaction.isNotEmpty()
metalLayer.presentsWithTransaction = presentsWithTransaction

// We only need to synchronize this specific frame if there are any pending changes or isForcedToPresentWithTransactionEveryFrame is true
val synchronizePresentation = isForcedToPresentWithTransactionEveryFrame || (presentsWithTransaction && interopTransaction.isNotEmpty())
val mustEncodeAndPresentOnMainThread = presentsWithTransaction || waitUntilCompletion

val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"
val encodeAndPresentBlock = {
surface.canvas.drawPicture(picture)
picture.close()
surface.flushAndSubmit()

if (!synchronizePresentation) {
// If there are no pending changes in UIKit interop, present the drawable ASAP
commandBuffer.presentDrawable(metalDrawable)
}
val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"

commandBuffer.addCompletedHandler {
// Signal work finish, allow a new command buffer to be scheduled
dispatch_semaphore_signal(inflightSemaphore)
}
commandBuffer.commit()

if (synchronizePresentation) {
// 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()
interopTransaction.actions.fastForEach {
it.invoke()
if (!presentsWithTransaction) {
commandBuffer.presentDrawable(metalDrawable)
}

if (interopTransaction.state == UIKitInteropState.ENDED) {
isInteropActive = false
commandBuffer.addCompletedHandler {
// Signal work finish, allow a new command buffer to be scheduled
dispatch_semaphore_signal(inflightSemaphore)
}
commandBuffer.commit()

CATransaction.commit()
}
if (presentsWithTransaction) {
// 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()

surface.close()
renderTarget.close()
// TODO manually release metalDrawable when K/N API arrives
interopTransaction.actions.fastForEach {
it.invoke()
}

// 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)
}
if (interopTransaction.state == UIKitInteropState.ENDED) {
isInteropActive = false
}
}

surface.close()
renderTarget.close()

inflightCommandBuffers.add(commandBuffer)
// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
inflightCommandBuffers.add(commandBuffer)

if (waitUntilCompletion) {
commandBuffer.waitUntilCompleted()
if (waitUntilCompletion) {
commandBuffer.waitUntilCompleted()
}
}

if (mustEncodeAndPresentOnMainThread) {
encodeAndPresentBlock()
} else {
dispatch_async(renderingDispatchQueue) {
autoreleasepool {
encodeAndPresentBlock()
}
}
}
}
}

companion object {
private val renderingDispatchQueue =
dispatch_queue_create("RenderingDispatchQueue", null)
}
}

private class DisplayLinkProxy(
Expand Down

0 comments on commit cf8b443

Please sign in to comment.