Skip to content

Commit 072252b

Browse files
Postpone drawable acqusition
1 parent aa5b9ed commit 072252b

File tree

4 files changed

+82
-45
lines changed

4 files changed

+82
-45
lines changed

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,14 @@ class ComposeScene internal constructor(
517517
* Render the current content on [canvas]. Passed [nanoTime] will be used to drive all
518518
* animations in the content (or any other code, which uses [withFrameNanos]
519519
*/
520-
fun render(canvas: Canvas, nanoTime: Long): Unit = postponeInvalidation {
520+
@Deprecated("Use `fun render(retrieveCanvas: () -> Canvas?, nanoTime: Long): Unit` instead")
521+
fun render(canvas: Canvas, nanoTime: Long): Unit = render(retrieveCanvas = { canvas }, nanoTime)
522+
523+
/**
524+
* Flush async operations, notify all animations and perform recomposition-layout-draw sequence.
525+
* [Canvas] to draw on is retrieved lazily to postpone acquiring drawable as late as possible.
526+
*/
527+
fun render(retrieveCanvas: () -> Canvas?, nanoTime: Long): Unit = postponeInvalidation {
521528
recomposeDispatcher.flush()
522529
frameClock.sendFrame(nanoTime) // Recomposition
523530
sendAndPerformSnapshotChanges() // Apply changes from recomposition phase to layout phase
@@ -526,7 +533,11 @@ class ComposeScene internal constructor(
526533
syntheticEventSender.updatePointerPosition()
527534
sendAndPerformSnapshotChanges() // Apply changes from layout phase to draw phase
528535
needDraw = false
529-
forEachOwner { it.draw(canvas) }
536+
537+
retrieveCanvas()?.let { canvas ->
538+
forEachOwner { it.draw(canvas) }
539+
}
540+
530541
forEachOwner { it.clearInvalidObservations() }
531542
}
532543

compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeWindow.uikit.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import kotlinx.cinterop.ObjCAction
4646
import kotlinx.cinterop.readValue
4747
import kotlinx.cinterop.useContents
4848
import kotlinx.coroutines.Dispatchers
49+
import org.jetbrains.skia.Canvas
4950
import org.jetbrains.skia.Surface
5051
import org.jetbrains.skiko.OS
5152
import org.jetbrains.skiko.OSVersion
@@ -637,7 +638,7 @@ internal actual class ComposeWindow : UIViewController {
637638
override fun retrieveCATransactionCommands(): List<() -> Unit> =
638639
interopContext.getActionsAndClear()
639640

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

649-
scene.render(surface.canvas, nanos)
650+
scene.render(retrieveCanvas, nanos)
650651
}
651652
}
652653

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

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,10 @@ internal interface MetalRedrawerCallbacks {
148148
/**
149149
* Draw into a surface.
150150
*
151-
* @param surface The surface to be drawn.
151+
* @param retrieveCanvas Callback to lazily retrieve canvas to postpone drawable acquiring.
152152
* @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.
153153
*/
154-
fun draw(surface: Surface, targetTimestamp: NSTimeInterval)
154+
fun render(retrieveCanvas: () -> Canvas?, targetTimestamp: NSTimeInterval)
155155

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

275+
private data class FrameRenderTarget(
276+
val drawable: CAMetalDrawableProtocol,
277+
val renderTarget: BackendRenderTarget,
278+
val surface: Surface
279+
) {
280+
fun dispose() {
281+
surface.close()
282+
renderTarget.close()
283+
// TODO manually release metalDrawable when K/N API arrives
284+
}
285+
}
286+
287+
private fun createFrameRenderTarget(width: Int, height: Int): FrameRenderTarget? {
288+
dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
289+
290+
val metalDrawable = metalLayer.nextDrawable()
291+
292+
if (metalDrawable == null) {
293+
// TODO: anomaly, log
294+
// Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
295+
dispatch_semaphore_signal(inflightSemaphore)
296+
return null
297+
}
298+
299+
val renderTarget =
300+
BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())
301+
302+
val surface = Surface.makeFromBackendRenderTarget(
303+
context,
304+
renderTarget,
305+
SurfaceOrigin.TOP_LEFT,
306+
SurfaceColorFormat.BGRA_8888,
307+
ColorSpace.sRGB,
308+
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
309+
)
310+
311+
if (surface == null) {
312+
renderTarget.close()
313+
// TODO: manually release metalDrawable when K/N API arrives
314+
dispatch_semaphore_signal(inflightSemaphore)
315+
return null
316+
}
317+
318+
return FrameRenderTarget(metalDrawable, renderTarget, surface)
319+
}
320+
275321
private fun draw(waitUntilCompletion: Boolean, targetTimestamp: NSTimeInterval) {
276322
check(NSThread.isMainThread)
277323

@@ -286,41 +332,21 @@ internal class MetalRedrawer(
286332
return@autoreleasepool
287333
}
288334

289-
dispatch_semaphore_wait(inflightSemaphore, DISPATCH_TIME_FOREVER)
335+
var frameRenderTarget: FrameRenderTarget? = null
290336

291-
val metalDrawable = metalLayer.nextDrawable()
292-
293-
if (metalDrawable == null) {
294-
// TODO: anomaly, log
295-
// Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
296-
dispatch_semaphore_signal(inflightSemaphore)
297-
return@autoreleasepool
337+
// Lambda which overwrites local frameRenderTarget if called, clears [Canvas] and returns it to the callee
338+
val retrieveCanvas = {
339+
createFrameRenderTarget(width, height)?.also {
340+
frameRenderTarget = it
341+
it.surface.canvas.clear(Color.WHITE)
342+
}?.surface?.canvas
298343
}
299344

300-
val renderTarget =
301-
BackendRenderTarget.makeMetal(width, height, metalDrawable.texture.objcPtr())
302-
303-
val surface = Surface.makeFromBackendRenderTarget(
304-
context,
305-
renderTarget,
306-
SurfaceOrigin.TOP_LEFT,
307-
SurfaceColorFormat.BGRA_8888,
308-
ColorSpace.sRGB,
309-
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
310-
)
311-
312-
if (surface == null) {
313-
// TODO: anomaly, log
314-
// Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
315-
renderTarget.close()
316-
// TODO: manually release metalDrawable when K/N API arrives
317-
dispatch_semaphore_signal(inflightSemaphore)
318-
return@autoreleasepool
319-
}
345+
callbacks.render(retrieveCanvas, lastRenderTimestamp)
320346

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

325351
val caTransactionCommands = callbacks.retrieveCATransactionCommands()
326352
val presentsWithTransaction =
@@ -333,7 +359,7 @@ internal class MetalRedrawer(
333359

334360
if (!presentsWithTransaction) {
335361
// If there are no pending changes in UIKit interop, present the drawable ASAP
336-
commandBuffer.presentDrawable(metalDrawable)
362+
commandBuffer.presentDrawable(nonNullFrameRenderTarget.drawable)
337363
}
338364

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

354-
surface.close()
355-
renderTarget.close()
356-
// TODO manually release metalDrawable when K/N API arrives
380+
nonNullFrameRenderTarget.dispose()
357381

358382
// Track current inflight command buffers to synchronously wait for their schedule in case app goes background
359383
if (inflightCommandBuffers.size == metalLayer.maximumDrawableCount.toInt()) {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import platform.UIKit.*
3131
import platform.darwin.NSInteger
3232
import kotlin.math.max
3333
import kotlin.math.min
34+
import org.jetbrains.skia.Canvas
3435
import org.jetbrains.skia.Surface
3536
import org.jetbrains.skiko.SkikoInputModifiers
3637
import org.jetbrains.skiko.SkikoKey
@@ -51,7 +52,7 @@ internal interface SkikoUIViewDelegate {
5152

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

54-
fun draw(surface: Surface, targetTimestamp: NSTimeInterval)
55+
fun render(retrieveCanvas: () -> Canvas?, targetTimestamp: NSTimeInterval)
5556
}
5657

5758
@Suppress("CONFLICTING_OVERLOADS")
@@ -79,8 +80,8 @@ internal class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {
7980
private val _redrawer: MetalRedrawer = MetalRedrawer(
8081
_metalLayer,
8182
callbacks = object : MetalRedrawerCallbacks {
82-
override fun draw(surface: Surface, targetTimestamp: NSTimeInterval) {
83-
delegate?.draw(surface, targetTimestamp)
83+
override fun render(retrieveCanvas: () -> Canvas?, targetTimestamp: NSTimeInterval) {
84+
delegate?.render(retrieveCanvas, targetTimestamp)
8485
}
8586

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

0 commit comments

Comments
 (0)