From 3d05e3d2afb44dcad290ed4b0fe11e9cc7ca6a12 Mon Sep 17 00:00:00 2001 From: Andrei Salavei Date: Fri, 9 Jan 2026 12:07:42 +0100 Subject: [PATCH 1/2] Optimize MetalRedrawer synchronization --- .../compose/ui/window/MetalRedrawer.ios.kt | 66 +++++-------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt index ad73f6ebf05b1..f488a21bf0382 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt @@ -25,14 +25,12 @@ import androidx.compose.ui.viewinterop.UIKitInteropTransaction import kotlin.math.roundToInt import kotlinx.cinterop.* import org.jetbrains.skia.* +import org.jetbrains.skia.Rect import platform.Foundation.NSRunLoop import platform.Foundation.NSSelectorFromString import platform.Foundation.NSThread -import platform.Metal.MTLCommandBufferProtocol import platform.QuartzCore.* import platform.darwin.* -import org.jetbrains.skia.Rect -import platform.Foundation.NSLock import platform.Foundation.NSRunLoopCommonModes import platform.Foundation.NSTimeInterval import platform.Metal.MTLCommandQueueProtocol @@ -105,27 +103,6 @@ private class DisplayLinkConditions( } } -internal class InflightCommandBuffers( - private val maxInflightCount: Int -) { - private val lock = NSLock() - private val list = mutableListOf() - - 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 var retrieveInteropTransaction: () -> UIKitInteropTransaction, @@ -149,12 +126,8 @@ internal class MetalRedrawer( 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 inflightCommandBuffersGroup = dispatch_group_create() private val drawCanvasSemaphore = dispatch_semaphore_create(1) - private val inflightCommandBuffers = - InflightCommandBuffers(metalLayer.maximumDrawableCount.toInt()) var isForcedToPresentWithTransactionEveryFrame = false @@ -256,7 +229,9 @@ internal class MetalRedrawer( displayLinkConditions.isActive = newValue if (!newValue) { - inflightCommandBuffers.waitUntilAllAreScheduled() + // If an 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 + dispatch_group_wait(inflightCommandBuffersGroup, dispatch_time(DISPATCH_TIME_NOW, 1L * NSEC_PER_SEC.toLong())) } } } @@ -278,6 +253,14 @@ internal class MetalRedrawer( caDisplayLink?.invalidate() caDisplayLink = null + // Wait until all scheduled rendering tasks are completed to eliminate race conditions + // when clearing resources + if (useSeparateRenderThreadWhenPossible) { + trace("MetalRedrawer:dispose:waitForAsyncRenderingTasks") { + dispatch_sync(renderingDispatchQueue) {} + } + } + pictureRecorder.close() context.close() } @@ -297,7 +280,6 @@ internal class MetalRedrawer( if (caDisplayLink == null) { return } - draw(waitUntilCompletion, CACurrentMediaTime()) } @@ -366,10 +348,6 @@ internal class MetalRedrawer( currentFrameRate = Float.NaN } - trace("MetalRedrawer:draw:waitInflightSemaphore") { - dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER) - } - val metalDrawable = trace("MetalRedrawer:draw:nextDrawable") { metalDrawablesHandler.nextDrawable() } @@ -378,7 +356,6 @@ internal class MetalRedrawer( // 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 } @@ -403,7 +380,6 @@ internal class MetalRedrawer( picture.close() renderTarget.close() metalDrawablesHandler.releaseDrawable(metalDrawable) - dispatch_semaphore_signal(inflightSemaphore) return@autoreleasepool } @@ -444,9 +420,9 @@ internal class MetalRedrawer( metalDrawablesHandler.scheduleDrawablePresentation(metalDrawable, commandBuffer) } + dispatch_group_enter(inflightCommandBuffersGroup) commandBuffer.addCompletedHandler { - // Signal work finish, allow a new command buffer to be scheduled - dispatch_semaphore_signal(inflightSemaphore) + dispatch_group_leave(inflightCommandBuffersGroup) } commandBuffer.commit() @@ -474,8 +450,6 @@ internal class MetalRedrawer( renderTarget.close() // Track current inflight command buffers to synchronously wait for their schedule in case app goes background - inflightCommandBuffers.add(commandBuffer) - if (waitUntilCompletion) { trace("MetalRedrawer:draw:waitUntilCompleted") { commandBuffer.waitUntilCompleted() @@ -552,13 +526,3 @@ private class DisplayLinkProxy( callback() } } - -private inline fun NSLock.doLocked(block: () -> T): T { - lock() - - try { - return block() - } finally { - unlock() - } -} From c322aedcebaffff85be13d9c95d2f16869b2e926 Mon Sep 17 00:00:00 2001 From: Andrei Salavei Date: Wed, 14 Jan 2026 15:42:26 +0100 Subject: [PATCH 2/2] Enhance comment on inflight command buffers handling Add comment to clarify background app behavior. --- .../kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt index f488a21bf0382..cd4e837ca7357 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/MetalRedrawer.ios.kt @@ -231,6 +231,7 @@ internal class MetalRedrawer( if (!newValue) { // If an 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 + // Set the expiration time to 1 second to ensure that the main thread does not get stuck when the app is suspended. dispatch_group_wait(inflightCommandBuffersGroup, dispatch_time(DISPATCH_TIME_NOW, 1L * NSEC_PER_SEC.toLong())) } }