Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,6 @@ internal class ComposeLayersViewController(
hide()

transaction.actions.fastForEach { it.invoke() }

// If the layers list is empty, the draw call will clear the canvas.
metalView.redrawer.draw(waitUntilCompletion = false)
} else {
// It wasn't the last layer, pending transactions should be added to the list
removedLayersTransactions.add(transaction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ internal class ComposeContainerView(
onDidMoveToWindow(window)

updateRedrawerState()
setNeedsSynchronousDraw()

// To avoid a situation where a user decided to call [layoutIfNeeded] on the detached view
// using a certain frame and it will be attached to the window later, so there is a chance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ internal class MetalRedrawer(

private val inflightCommandBuffersGroup = dispatch_group_create()
private val drawCanvasSemaphore = dispatch_semaphore_create(1)
// A guard flag to have proper assertion when draw() method is called recursively.
private var isDrawRecursiveCall = false

var isForcedToPresentWithTransactionEveryFrame = false

Expand Down Expand Up @@ -319,153 +321,165 @@ internal class MetalRedrawer(
@OptIn(BetaInteropApi::class)
private fun draw(waitUntilCompletion: Boolean, targetTimestamp: NSTimeInterval) = trace("MetalRedrawer:draw") {
check(NSThread.isMainThread)
check(!isDrawRecursiveCall) {
"Attempt to call MetalRedrawer.draw() recursively which may lead to the PictureRecorder corruption."
}
isDrawRecursiveCall = true

lastRenderTimestamp = maxOf(targetTimestamp, lastRenderTimestamp)

autoreleasepool {
val (width, height) = metalLayer.drawableSize.useContents {
width.roundToInt() to height.roundToInt()
}

if (width <= 0 || height <= 0) {
return@autoreleasepool
}
try {
lastRenderTimestamp = maxOf(targetTimestamp, lastRenderTimestamp)

// Perform timestep and record all draw commands into [Picture]
val picture = trace("MetalRedrawer:draw:pictureRecording") {
pictureRecorder.beginRecording(
left = 0f,
top = 0f,
width.toFloat(),
height.toFloat()
).also { canvas ->
render(canvas, lastRenderTimestamp)
autoreleasepool {
val (width, height) = metalLayer.drawableSize.useContents {
width.roundToInt() to height.roundToInt()
}

pictureRecorder.finishRecordingAsPicture()
}

if (!currentFrameRate.isNaN()) {
preferredFramesPerSecond = currentFrameRate.toLong()
currentFrameRate = Float.NaN
}

val metalDrawable = trace("MetalRedrawer:draw:nextDrawable") {
metalDrawablesHandler.nextDrawable()
}

if (metalDrawable == null) {
// TODO: anomaly, log
// Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
picture.close()
return@autoreleasepool
}

val renderTarget = BackendRenderTarget.makeMetal(
width,
height,
texturePtr = metalDrawablesHandler.drawableTexture(metalDrawable).rawValue
)
if (width <= 0 || height <= 0) {
return@autoreleasepool
}

val surface = Surface.makeFromBackendRenderTarget(
context,
renderTarget,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB,
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN)
)
// Perform timestep and record all draw commands into [Picture]
val picture = trace("MetalRedrawer:draw:pictureRecording") {
pictureRecorder.beginRecording(
left = 0f,
top = 0f,
width.toFloat(),
height.toFloat()
).also { canvas ->
render(canvas, lastRenderTimestamp)
}

if (surface == null) {
// TODO: anomaly, log
// Logger.warn { "'Surface.makeFromBackendRenderTarget' returned null. Skipping the frame." }
picture.close()
renderTarget.close()
metalDrawablesHandler.releaseDrawable(metalDrawable)
return@autoreleasepool
}
pictureRecorder.finishRecordingAsPicture()
}

val interopTransaction = retrieveInteropTransaction()
if (!currentFrameRate.isNaN()) {
preferredFramesPerSecond = currentFrameRate.toLong()
currentFrameRate = Float.NaN
}

val presentsWithTransaction =
isForcedToPresentWithTransactionEveryFrame
|| interopTransaction.actions.isNotEmpty()
|| isInteropActive != interopTransaction.isInteropActive
metalLayer.presentsWithTransaction = presentsWithTransaction
val metalDrawable = trace("MetalRedrawer:draw:nextDrawable") {
metalDrawablesHandler.nextDrawable()
}

if (interopTransaction.isInteropActive) {
isInteropActive = true
}
if (metalDrawable == null) {
// TODO: anomaly, log
// Logger.warn { "'metalLayer.nextDrawable()' returned null. 'metalLayer.allowsNextDrawableTimeout' should be set to false. Skipping the frame." }
picture.close()
return@autoreleasepool
}

val mustEncodeAndPresentOnMainThread = presentsWithTransaction || waitUntilCompletion || !useSeparateRenderThreadWhenPossible
val renderTarget = BackendRenderTarget.makeMetal(
width,
height,
texturePtr = metalDrawablesHandler.drawableTexture(metalDrawable).rawValue
)

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." }
picture.close()
renderTarget.close()
metalDrawablesHandler.releaseDrawable(metalDrawable)
return@autoreleasepool
}

val encodeAndPresentBlock = {
trace("MetalRedrawer:draw:encodeAndPresent") {
if (useSeparateRenderThreadWhenPossible) {
dispatch_semaphore_wait(drawCanvasSemaphore, DISPATCH_TIME_FOREVER)
}
val interopTransaction = retrieveInteropTransaction()

surface.canvas.drawPicture(picture)
picture.close()
surface.flushAndSubmit()
val presentsWithTransaction =
isForcedToPresentWithTransactionEveryFrame
|| interopTransaction.actions.isNotEmpty()
|| isInteropActive != interopTransaction.isInteropActive
metalLayer.presentsWithTransaction = presentsWithTransaction

surface.close()
renderTarget.close()
if (interopTransaction.isInteropActive) {
isInteropActive = true
}

if (useSeparateRenderThreadWhenPossible) {
dispatch_semaphore_signal(drawCanvasSemaphore)
}
val mustEncodeAndPresentOnMainThread =
presentsWithTransaction || waitUntilCompletion || !useSeparateRenderThreadWhenPossible

val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"
val encodeAndPresentBlock = {
trace("MetalRedrawer:draw:encodeAndPresent") {
if (useSeparateRenderThreadWhenPossible) {
dispatch_semaphore_wait(drawCanvasSemaphore, DISPATCH_TIME_FOREVER)
}

if (!presentsWithTransaction) {
// scheduleDrawablePresentation consumes metalDrawable
// don't use metalDrawable after this call
metalDrawablesHandler.scheduleDrawablePresentation(metalDrawable, commandBuffer)
}
surface.canvas.drawPicture(picture)
picture.close()
surface.flushAndSubmit()

dispatch_group_enter(inflightCommandBuffersGroup)
commandBuffer.addCompletedHandler {
dispatch_group_leave(inflightCommandBuffersGroup)
}
commandBuffer.commit()
surface.close()
renderTarget.close()

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
trace("MetalRedrawer:draw:waitTransaction") {
commandBuffer.waitUntilScheduled()
if (useSeparateRenderThreadWhenPossible) {
dispatch_semaphore_signal(drawCanvasSemaphore)
}

// presentDrawable consumes metalDrawable
// don't use metalDrawable after this call
metalDrawablesHandler.presentDrawable(metalDrawable)
val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"

interopTransaction.actions.fastForEach {
it.invoke()
if (!presentsWithTransaction) {
// scheduleDrawablePresentation consumes metalDrawable
// don't use metalDrawable after this call
metalDrawablesHandler.scheduleDrawablePresentation(
metalDrawable,
commandBuffer
)
}

if (interopTransaction.isInteropActive.not()) {
isInteropActive = false
dispatch_group_enter(inflightCommandBuffersGroup)
commandBuffer.addCompletedHandler {
dispatch_group_leave(inflightCommandBuffersGroup)
}
commandBuffer.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
trace("MetalRedrawer:draw:waitTransaction") {
commandBuffer.waitUntilScheduled()
}

// presentDrawable consumes metalDrawable
// don't use metalDrawable after this call
metalDrawablesHandler.presentDrawable(metalDrawable)

interopTransaction.actions.fastForEach {
it.invoke()
}

if (interopTransaction.isInteropActive.not()) {
isInteropActive = false
}
}
}

if (waitUntilCompletion) {
trace("MetalRedrawer:draw:waitUntilCompleted") {
commandBuffer.waitUntilCompleted()
if (waitUntilCompletion) {
trace("MetalRedrawer:draw:waitUntilCompleted") {
commandBuffer.waitUntilCompleted()
}
}
}
}
}

if (mustEncodeAndPresentOnMainThread) {
encodeAndPresentBlock()
} else {
dispatch_async(renderingDispatchQueue) {
if (mustEncodeAndPresentOnMainThread) {
encodeAndPresentBlock()
} else {
dispatch_async(renderingDispatchQueue) {
encodeAndPresentBlock()
}
}
}
} finally {
isDrawRecursiveCall = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class ComposeLaunchTest {

assertEquals(1, drawsCount, "Expected to draw only one frame on startup")

image.forEachPixel { _, _, color ->
image.forEachPixel(step = 4) { _, _, color ->
assertEquals(Color.Blue, color, "Expected to draw blue background")
}

Expand Down Expand Up @@ -123,7 +123,7 @@ class ComposeLaunchTest {

assertEquals(1, drawsCount, "Expected to draw only one frame on startup")

image.forEachPixel { _, _, color ->
image.forEachPixel(step = 4) { _, _, color ->
assertEquals(Color.Blue, color, "Expected to draw blue background")
}

Expand Down
Loading