Skip to content

Commit

Permalink
Fix high CPU usage when window is occluded on macOS Native (#1003)
Browse files Browse the repository at this point in the history
My app is used a lot in the background, and I noticed high CPU usage in
the native macOS version. With this PR, CPU usage is reduced when window
is invisible by disabling drawing. This was already implemented for the
JVM, and that behaviour is now copied to native macOS. So the code is
similar to JVM parts in:
-
https://github.com/JetBrains/skiko/blob/master/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt
-
https://github.com/JetBrains/skiko/blob/master/skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm

The draw function is now suspend, which required moving some other
functions around. I tried to keep the implementation similar to the JVM
version.

# Testing
Tested using `./gradlew runNative` in `samples/SkiaMultiplatformSample`.
Now the CPU usage is reduced a lot when moving another (non transparent)
app over the window.

# Old behaviour


https://github.com/user-attachments/assets/60ad3398-3f44-4e2f-aca7-44a4607d0840

# New behaviour with 300ms timeout
See reduction in CPU usage when window is invisible


https://github.com/user-attachments/assets/106efdeb-0044-4411-9b26-5a6b8886ff04


# New behaviour without 300ms timeout



https://github.com/user-attachments/assets/b986548b-d7ab-497b-9ddb-5b46ca016f83
  • Loading branch information
Thomas-Vos authored Feb 14, 2025
1 parent 96fba18 commit 852c96b
Showing 1 changed file with 41 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import kotlinx.cinterop.autoreleasepool
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.usePinned
import kotlinx.cinterop.useContents
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOsMetalContextHandler
import platform.AppKit.NSWindowDidChangeOcclusionStateNotification
import platform.AppKit.NSWindowOcclusionStateVisible
import platform.CoreGraphics.CGColorCreate
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGContextRef
Expand All @@ -26,6 +30,11 @@ import platform.QuartzCore.kCALayerHeightSizable
import platform.QuartzCore.kCALayerWidthSizable
import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake
import platform.Foundation.NSNotification
import platform.Foundation.NSNotificationCenter
import platform.Foundation.NSOperationQueue
import platform.darwin.NSObjectProtocol
import kotlin.concurrent.Volatile

/**
* Metal [Redrawer] implementation for MacOs.
Expand All @@ -43,9 +52,24 @@ internal class MacOsMetalRedrawer(
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer()
private val occlusionObserver: NSObjectProtocol
private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED)
@Volatile private var isWindowOccluded = false

init {
metalLayer.init(skiaLayer, contextHandler, device)

val window = skiaLayer.nsView.window!!
occlusionObserver = NSNotificationCenter.defaultCenter.addObserverForName(
name = NSWindowDidChangeOcclusionStateNotification,
`object` = window,
queue = NSOperationQueue.mainQueue,
usingBlock = { notification: NSNotification? ->
val isOccluded = window.occlusionState and NSWindowOcclusionStateVisible == 0uL
isWindowOccluded = isOccluded
windowOcclusionStateChannel.trySend(isOccluded)
}
)
}

private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
Expand All @@ -72,6 +96,7 @@ internal class MacOsMetalRedrawer(
override fun dispose() {
if (!isDisposed) {
metalLayer.dispose()
NSNotificationCenter.defaultCenter.removeObserver(occlusionObserver)
isDisposed = true
}
}
Expand Down Expand Up @@ -115,17 +140,30 @@ internal class MacOsMetalRedrawer(
*/
override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" }
draw()
autoreleasepool {
if (!isDisposed) {
skiaLayer.update(getTimeNanos())
contextHandler.draw()
}
}
}

private fun draw() {
// TODO: maybe make flush async as in JVM version.
private suspend fun draw() {
autoreleasepool {
if (!isDisposed) {
skiaLayer.update(getTimeNanos())
contextHandler.draw()
}
}

// When window is not visible - it doesn't make sense to redraw fast to avoid battery drain.
if (isWindowOccluded) {
withTimeoutOrNull(300) {
// If the window becomes non-occluded, stop waiting immediately
@Suppress("ControlFlowWithEmptyBody")
while (windowOcclusionStateChannel.receive()) { }
}
}
}

fun finishFrame() {
Expand Down

0 comments on commit 852c96b

Please sign in to comment.