Unverified Commit 852c96bc authored by Thomas Vos's avatar Thomas Vos Committed by GitHub

Fix high CPU usage when window is occluded on macOS Native (#1003)

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
parent 96fba185
...@@ -5,6 +5,8 @@ import kotlinx.cinterop.autoreleasepool ...@@ -5,6 +5,8 @@ import kotlinx.cinterop.autoreleasepool
import kotlinx.cinterop.objcPtr import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.skia.BackendRenderTarget import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
...@@ -12,6 +14,8 @@ import org.jetbrains.skiko.SkikoDispatchers ...@@ -12,6 +14,8 @@ import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOsMetalContextHandler import org.jetbrains.skiko.context.MacOsMetalContextHandler
import platform.AppKit.NSWindowDidChangeOcclusionStateNotification
import platform.AppKit.NSWindowOcclusionStateVisible
import platform.CoreGraphics.CGColorCreate import platform.CoreGraphics.CGColorCreate
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGContextRef import platform.CoreGraphics.CGContextRef
...@@ -26,6 +30,11 @@ import platform.QuartzCore.kCALayerHeightSizable ...@@ -26,6 +30,11 @@ import platform.QuartzCore.kCALayerHeightSizable
import platform.QuartzCore.kCALayerWidthSizable import platform.QuartzCore.kCALayerWidthSizable
import kotlin.system.getTimeNanos import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake 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. * Metal [Redrawer] implementation for MacOs.
...@@ -43,9 +52,24 @@ internal class MacOsMetalRedrawer( ...@@ -43,9 +52,24 @@ internal class MacOsMetalRedrawer(
private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue") private val queue = device.newCommandQueue() ?: throw IllegalStateException("Couldn't create Metal command queue")
private var currentDrawable: CAMetalDrawableProtocol? = null private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer() private val metalLayer = MetalLayer()
private val occlusionObserver: NSObjectProtocol
private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED)
@Volatile private var isWindowOccluded = false
init { init {
metalLayer.init(skiaLayer, contextHandler, device) 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) { private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
...@@ -72,6 +96,7 @@ internal class MacOsMetalRedrawer( ...@@ -72,6 +96,7 @@ internal class MacOsMetalRedrawer(
override fun dispose() { override fun dispose() {
if (!isDisposed) { if (!isDisposed) {
metalLayer.dispose() metalLayer.dispose()
NSNotificationCenter.defaultCenter.removeObserver(occlusionObserver)
isDisposed = true isDisposed = true
} }
} }
...@@ -115,17 +140,30 @@ internal class MacOsMetalRedrawer( ...@@ -115,17 +140,30 @@ internal class MacOsMetalRedrawer(
*/ */
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
draw() autoreleasepool {
if (!isDisposed) {
skiaLayer.update(getTimeNanos())
contextHandler.draw()
}
}
} }
private fun draw() { private suspend fun draw() {
// TODO: maybe make flush async as in JVM version.
autoreleasepool { autoreleasepool {
if (!isDisposed) { if (!isDisposed) {
skiaLayer.update(getTimeNanos()) skiaLayer.update(getTimeNanos())
contextHandler.draw() 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() { fun finishFrame() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment