Skip to content

Commit cf8b443

Browse files
Move frame encoding to separate thread when possible (#829)
## 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
1 parent 4d5d5a1 commit cf8b443

File tree

2 files changed

+77
-48
lines changed

2 files changed

+77
-48
lines changed

compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInteropContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ internal class UIKitInteropContext(
131131
}
132132
}
133133

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

137137
try {

compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/MetalRedrawer.kt

Lines changed: 76 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package androidx.compose.ui.window
1818

1919
import androidx.compose.ui.interop.UIKitInteropState
2020
import androidx.compose.ui.interop.UIKitInteropTransaction
21+
import androidx.compose.ui.interop.doLocked
2122
import androidx.compose.ui.interop.isNotEmpty
2223
import androidx.compose.ui.util.fastForEach
2324
import kotlin.math.roundToInt
@@ -34,6 +35,7 @@ import platform.UIKit.UIApplicationWillEnterForegroundNotification
3435
import platform.darwin.*
3536
import kotlin.math.roundToInt
3637
import org.jetbrains.skia.Rect
38+
import platform.Foundation.NSLock
3739
import platform.Foundation.NSTimeInterval
3840
import platform.UIKit.UIApplication
3941
import platform.UIKit.UIApplicationState
@@ -165,6 +167,27 @@ internal interface MetalRedrawerCallbacks {
165167
fun retrieveInteropTransaction(): UIKitInteropTransaction
166168
}
167169

170+
internal class InflightCommandBuffers(
171+
private val maxInflightCount: Int
172+
) {
173+
private val lock = NSLock()
174+
private val list = mutableListOf<MTLCommandBufferProtocol>()
175+
176+
fun waitUntilAllAreScheduled() = lock.doLocked {
177+
list.fastForEach {
178+
it.waitUntilScheduled()
179+
}
180+
}
181+
182+
fun add(commandBuffer: MTLCommandBufferProtocol) = lock.doLocked {
183+
if (list.size == maxInflightCount) {
184+
list.removeAt(0)
185+
}
186+
187+
list.add(commandBuffer)
188+
}
189+
}
190+
168191
internal class MetalRedrawer(
169192
private val metalLayer: CAMetalLayer,
170193
private val callbacks: MetalRedrawerCallbacks,
@@ -177,13 +200,14 @@ internal class MetalRedrawer(
177200
private val queue = device.newCommandQueue()
178201
?: throw IllegalStateException("Couldn't create Metal command queue")
179202
private val context = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
180-
private val inflightCommandBuffers = mutableListOf<MTLCommandBufferProtocol>()
181203
private var lastRenderTimestamp: NSTimeInterval = CACurrentMediaTime()
182204
private val pictureRecorder = PictureRecorder()
183205

184206
// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
185207
private val inflightSemaphore =
186208
dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())
209+
private val inflightCommandBuffers =
210+
InflightCommandBuffers(metalLayer.maximumDrawableCount.toInt())
187211

188212
var isForcedToPresentWithTransactionEveryFrame = false
189213

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

218243
/**
@@ -242,10 +267,7 @@ internal class MetalRedrawer(
242267
if (!isApplicationActive) {
243268
// If application goes background, synchronously schedule all inflightCommandBuffers, as per
244269
// https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/preparing_your_metal_app_to_run_in_the_background?language=objc
245-
inflightCommandBuffers.forEach {
246-
// Will immediately return for MTLCommandBuffer's which are not in `Commited` status
247-
it.waitUntilScheduled()
248-
}
270+
inflightCommandBuffers.waitUntilAllAreScheduled()
249271
}
250272
}
251273

@@ -271,8 +293,6 @@ internal class MetalRedrawer(
271293
caDisplayLink = null
272294

273295
pictureRecorder.close()
274-
275-
context.flush()
276296
context.close()
277297
}
278298

@@ -353,67 +373,76 @@ internal class MetalRedrawer(
353373
return@autoreleasepool
354374
}
355375

356-
surface.canvas.drawPicture(picture)
357-
picture.close()
358-
surface.flushAndSubmit()
359-
360376
val interopTransaction = callbacks.retrieveInteropTransaction()
361377
if (interopTransaction.state == UIKitInteropState.BEGAN) {
362378
isInteropActive = true
363379
}
364380
val presentsWithTransaction =
365-
isForcedToPresentWithTransactionEveryFrame || isInteropActive
381+
isForcedToPresentWithTransactionEveryFrame || interopTransaction.isNotEmpty()
366382
metalLayer.presentsWithTransaction = presentsWithTransaction
367383

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

371-
val commandBuffer = queue.commandBuffer()!!
372-
commandBuffer.label = "Present"
386+
val encodeAndPresentBlock = {
387+
surface.canvas.drawPicture(picture)
388+
picture.close()
389+
surface.flushAndSubmit()
373390

374-
if (!synchronizePresentation) {
375-
// If there are no pending changes in UIKit interop, present the drawable ASAP
376-
commandBuffer.presentDrawable(metalDrawable)
377-
}
391+
val commandBuffer = queue.commandBuffer()!!
392+
commandBuffer.label = "Present"
378393

379-
commandBuffer.addCompletedHandler {
380-
// Signal work finish, allow a new command buffer to be scheduled
381-
dispatch_semaphore_signal(inflightSemaphore)
382-
}
383-
commandBuffer.commit()
384-
385-
if (synchronizePresentation) {
386-
// If there are pending changes in UIKit interop, [waitUntilScheduled](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443036-waituntilscheduled) is called
387-
// to ensure that transaction is available
388-
commandBuffer.waitUntilScheduled()
389-
metalDrawable.present()
390-
interopTransaction.actions.fastForEach {
391-
it.invoke()
394+
if (!presentsWithTransaction) {
395+
commandBuffer.presentDrawable(metalDrawable)
392396
}
393397

394-
if (interopTransaction.state == UIKitInteropState.ENDED) {
395-
isInteropActive = false
398+
commandBuffer.addCompletedHandler {
399+
// Signal work finish, allow a new command buffer to be scheduled
400+
dispatch_semaphore_signal(inflightSemaphore)
396401
}
402+
commandBuffer.commit()
397403

398-
CATransaction.commit()
399-
}
404+
if (presentsWithTransaction) {
405+
// If there are pending changes in UIKit interop, [waitUntilScheduled](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443036-waituntilscheduled) is called
406+
// to ensure that transaction is available
407+
commandBuffer.waitUntilScheduled()
408+
metalDrawable.present()
400409

401-
surface.close()
402-
renderTarget.close()
403-
// TODO manually release metalDrawable when K/N API arrives
410+
interopTransaction.actions.fastForEach {
411+
it.invoke()
412+
}
404413

405-
// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
406-
if (inflightCommandBuffers.size == metalLayer.maximumDrawableCount.toInt()) {
407-
inflightCommandBuffers.removeAt(0)
408-
}
414+
if (interopTransaction.state == UIKitInteropState.ENDED) {
415+
isInteropActive = false
416+
}
417+
}
418+
419+
surface.close()
420+
renderTarget.close()
409421

410-
inflightCommandBuffers.add(commandBuffer)
422+
// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
423+
inflightCommandBuffers.add(commandBuffer)
411424

412-
if (waitUntilCompletion) {
413-
commandBuffer.waitUntilCompleted()
425+
if (waitUntilCompletion) {
426+
commandBuffer.waitUntilCompleted()
427+
}
428+
}
429+
430+
if (mustEncodeAndPresentOnMainThread) {
431+
encodeAndPresentBlock()
432+
} else {
433+
dispatch_async(renderingDispatchQueue) {
434+
autoreleasepool {
435+
encodeAndPresentBlock()
436+
}
437+
}
414438
}
415439
}
416440
}
441+
442+
companion object {
443+
private val renderingDispatchQueue =
444+
dispatch_queue_create("RenderingDispatchQueue", null)
445+
}
417446
}
418447

419448
private class DisplayLinkProxy(

0 commit comments

Comments
 (0)