Unverified Commit fa65bb4c authored by Alexander Maryanovsky's avatar Alexander Maryanovsky Committed by GitHub

[Metal] Decouple onRender from draw (#1057)

parent 0a5f9471
...@@ -15,8 +15,9 @@ internal class AndroidOpenGLRedrawer( ...@@ -15,8 +15,9 @@ internal class AndroidOpenGLRedrawer(
private val properties: SkiaLayerProperties private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
override fun dispose() = TODO() override fun dispose() = TODO()
override fun needRedraw() = TODO() override fun needRedraw(canUpdateImmediately: Boolean) = TODO()
override fun redrawImmediately() = TODO() override fun redrawImmediately(updateNeeded: Boolean) = TODO()
override fun update(nanoTime: Long) = TODO()
override val renderInfo: String get() = "Android renderer" override val renderInfo: String get() = "Android renderer"
} }
......
...@@ -57,7 +57,7 @@ actual open class SkiaLayer { ...@@ -57,7 +57,7 @@ actual open class SkiaLayer {
} }
} }
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
glView?.apply { glView?.apply {
scheduleFrame() scheduleFrame()
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
...@@ -8,10 +9,12 @@ import org.jetbrains.skiko.redrawer.Redrawer ...@@ -8,10 +9,12 @@ import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.RedrawerManager import org.jetbrains.skiko.redrawer.RedrawerManager
import java.awt.Color import java.awt.Color
import java.awt.Component import java.awt.Component
import java.awt.Graphics
import java.awt.Point import java.awt.Point
import java.awt.event.* import java.awt.event.*
import java.awt.geom.AffineTransform import java.awt.geom.AffineTransform
import java.awt.im.InputMethodRequests import java.awt.im.InputMethodRequests
import java.beans.PropertyChangeListener
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
import javax.accessibility.Accessible import javax.accessibility.Accessible
import javax.swing.JComponent import javax.swing.JComponent
...@@ -100,18 +103,29 @@ actual open class SkiaLayer internal constructor( ...@@ -100,18 +103,29 @@ actual open class SkiaLayer internal constructor(
isOpaque = false isOpaque = false
layout = null layout = null
backedLayer = object : HardwareLayer(externalAccessibleFactory) { backedLayer = object : HardwareLayer(externalAccessibleFactory) {
override fun paint(g: java.awt.Graphics) {
Logger.debug { "Paint called on $this" } @Suppress("OVERRIDE_DEPRECATION")
checkContentScale() override fun reshape(x: Int, y: Int, width: Int, height: Int) {
Logger.debug { "reshape(x=$x, y=$y, w=$width, h=$height) called on $this" }
// 1. JPanel.paint is not always called (in rare cases). super.reshape(x, y, width, height)
// For example if we call 'jframe.isResizable = false` on Ubuntu
// redrawer?.syncBounds()
// 2. HardwareLayer.paint is also not always called. // There's no reason for the render delegate to directly cause resizing SkiaLayer, but protect
// For example, on macOs when we resize window or change DPI // against it anyway.
// if (!isRendering) {
// 3. to avoid double paint in one single frame, use needRedraw instead of redrawImmediately // When the layer isn't yet showing, paint will not be called,
redrawer?.needRedraw() // but in order to avoid the background flashing when the layer
// does show, we already draw to the native surface.
redrawer?.redrawImmediately(updateNeeded = true)
} else {
redrawer?.needRedraw(throttledToVsync = false)
}
}
override fun paint(g: Graphics) {
Logger.debug { "paint called on $this" }
val updateNeeded = checkContentScale()
redrawer?.redrawImmediately(updateNeeded = updateNeeded)
} }
override fun getInputMethodRequests(): InputMethodRequests? { override fun getInputMethodRequests(): InputMethodRequests? {
...@@ -175,7 +189,6 @@ actual open class SkiaLayer internal constructor( ...@@ -175,7 +189,6 @@ actual open class SkiaLayer internal constructor(
} }
} }
addPropertyChangeListener("graphicsContextScaleTransform") { addPropertyChangeListener("graphicsContextScaleTransform") {
Logger.debug { "graphicsContextScaleTransform changed for $this" } Logger.debug { "graphicsContextScaleTransform changed for $this" }
latestReceivedGraphicsContextScaleTransform = it.newValue as AffineTransform latestReceivedGraphicsContextScaleTransform = it.newValue as AffineTransform
...@@ -185,6 +198,7 @@ actual open class SkiaLayer internal constructor( ...@@ -185,6 +198,7 @@ actual open class SkiaLayer internal constructor(
// Workaround for JBR-5259 // Workaround for JBR-5259
if (hostOs == OS.Windows) { if (hostOs == OS.Windows) {
peerBufferSizeFixJob?.cancel() peerBufferSizeFixJob?.cancel()
@OptIn(DelicateCoroutinesApi::class)
peerBufferSizeFixJob = GlobalScope.launch(MainUIDispatcher) { peerBufferSizeFixJob = GlobalScope.launch(MainUIDispatcher) {
backedLayer.setLocation(1, 0) backedLayer.setLocation(1, 0)
backedLayer.setLocation(0, 0) backedLayer.setLocation(0, 0)
...@@ -193,6 +207,16 @@ actual open class SkiaLayer internal constructor( ...@@ -193,6 +207,16 @@ actual open class SkiaLayer internal constructor(
} }
} }
// Override to make final, because it's called it in the init block
final override fun addAncestorListener(listener: AncestorListener?) {
super.addAncestorListener(listener)
}
// Override to make final, because it's called it in the init block
final override fun addPropertyChangeListener(propertyName: String?, listener: PropertyChangeListener?) {
super.addPropertyChangeListener(propertyName, listener)
}
private var fullscreenAdapter = FullscreenAdapter(backedLayer) private var fullscreenAdapter = FullscreenAdapter(backedLayer)
override fun removeNotify() { override fun removeNotify() {
...@@ -356,6 +380,29 @@ actual open class SkiaLayer internal constructor( ...@@ -356,6 +380,29 @@ actual open class SkiaLayer internal constructor(
} }
} }
@Suppress("OVERRIDE_DEPRECATION")
override fun reshape(x: Int, y: Int, w: Int, h: Int) {
super.reshape(x, y, w, h)
// Calling redrawImmediately as early as possible improves the situation with
// the visual glitch when the drawn content is scaled during window resize.
// Note, however, that this actually causes the reverse glitch (content appears
// scaled in the other direction from the window size), but this seems to
// happen less often.
//
// Calling redraw during layout might break software renderers,
// so apply this fix only for the Direct3D case.
if (renderApi == GraphicsApi.DIRECT3D && isShowing) {
redrawer?.syncBounds()
redrawer?.redrawImmediately(updateNeeded = true)
}
// Setting the bounds of children should be done only in the layout pass,
// but unfortunately, Compose expects the drawing area to be resized
// immediately when `SkiaLayer` is resized.
validate()
}
override fun doLayout() { override fun doLayout() {
Logger.debug { "doLayout on $this" } Logger.debug { "doLayout on $this" }
backedLayer.setBounds( backedLayer.setBounds(
...@@ -365,52 +412,19 @@ actual open class SkiaLayer internal constructor( ...@@ -365,52 +412,19 @@ actual open class SkiaLayer internal constructor(
adjustSizeToContentScale(contentScale, height) adjustSizeToContentScale(contentScale, height)
) )
backedLayer.validate() backedLayer.validate()
redrawer?.syncBounds()
}
override fun paint(g: java.awt.Graphics) {
Logger.debug { "Paint called on: $this" }
checkContentScale()
tryRedrawImmediately()
}
override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
super.setBounds(x, y, width, height)
// To avoid visual artifacts on Windows/Direct3D,
// redrawing should be performed immediately, without scheduling to "later".
// Subscribing to events instead of overriding this method won't help too.
//
// Please note that calling redraw during layout might break software renderers,
// so applying this fix only for Direct3D case.
if (renderApi == GraphicsApi.DIRECT3D && isShowing) {
redrawer?.syncBounds()
tryRedrawImmediately()
}
}
private fun tryRedrawImmediately() {
// It might be called inside `renderDelegate`,
// so to avoid recursive call (not supported) just schedule redrawing.
//
// For example if we call some AWT function inside renderer.onRender,
// such as `jframe.isEnabled = false` on Linux
if (isRendering) {
redrawer?.needRedraw()
} else {
redrawer?.redrawImmediately()
}
} }
// Workaround for JBR-5274 and JBR-5305 // Workaround for JBR-5274 and JBR-5305
fun checkContentScale() { fun checkContentScale(): Boolean {
val currentGraphicsContextScaleTransform = graphicsConfiguration.defaultTransform val currentGraphicsContextScaleTransform = graphicsConfiguration.defaultTransform
if (currentGraphicsContextScaleTransform != latestReceivedGraphicsContextScaleTransform) { return (currentGraphicsContextScaleTransform != latestReceivedGraphicsContextScaleTransform).also {
firePropertyChange( if (it) {
"graphicsContextScaleTransform", firePropertyChange(
latestReceivedGraphicsContextScaleTransform, "graphicsContextScaleTransform",
currentGraphicsContextScaleTransform latestReceivedGraphicsContextScaleTransform,
) currentGraphicsContextScaleTransform
)
}
} }
} }
...@@ -526,30 +540,25 @@ actual open class SkiaLayer internal constructor( ...@@ -526,30 +540,25 @@ actual open class SkiaLayer internal constructor(
/** /**
* Redraw on the next animation Frame (on vsync signal if vsync is enabled). * Redraw on the next animation Frame (on vsync signal if vsync is enabled).
*/ */
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" } check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
redrawer?.needRedraw() redrawer?.needRedraw(throttledToVsync)
} }
@Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this)
internal fun update(nanoTime: Long) { internal fun update(nanoTime: Long) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" } check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
checkContentScale() checkContentScale()
FrameWatcher.nextFrame() FrameWatcher.nextFrame()
fpsCounter?.tick()
// The current approach is to render into a picture in the main thread, and render this picture in the render thread // The current approach is to render into a picture in the main thread, and render this picture in the render thread
// If this approach will be changed, create an issue in https://youtrack.jetbrains.com/issues/CMP for changing it in // If this approach will be changed, create an issue in https://youtrack.jetbrains.com/issues/CMP for changing it in
// https://github.com/JetBrains/compose-multiplatform/blob/e4e2d329709cded91a09cc612d4defbce37aad96/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt#L151 as well // https://github.com/JetBrains/compose-multiplatform/blob/e4e2d329709cded91a09cc612d4defbce37aad96/benchmarks/multiplatform/benchmarks/src/commonMain/kotlin/MeasureComposable.kt#L151 as well
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0) val pictureWidth = (backedLayer.width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0) val pictureHeight = (backedLayer.height * contentScale).toInt().coerceAtLeast(0)
val bounds = Rect.makeWH(pictureWidth.toFloat(), pictureHeight.toFloat()) val bounds = Rect.makeWH(pictureWidth.toFloat(), pictureHeight.toFloat())
val pictureRecorder = pictureRecorder!! val pictureRecorder = pictureRecorder!!
...@@ -578,10 +587,14 @@ actual open class SkiaLayer internal constructor( ...@@ -578,10 +587,14 @@ actual open class SkiaLayer internal constructor(
} }
} }
@Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this)
internal inline fun inDrawScope(body: () -> Unit) { internal inline fun inDrawScope(body: () -> Unit) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" } check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
try { try {
fpsCounter?.tick()
body() body()
} catch (e: CancellationException) { } catch (e: CancellationException) {
// ignore // ignore
...@@ -589,7 +602,7 @@ actual open class SkiaLayer internal constructor( ...@@ -589,7 +602,7 @@ actual open class SkiaLayer internal constructor(
if (!isDisposed) { if (!isDisposed) {
Logger.warn(e) { "Exception in draw scope" } Logger.warn(e) { "Exception in draw scope" }
redrawerManager.findNextWorkingRenderApi() redrawerManager.findNextWorkingRenderApi()
redrawer?.redrawImmediately() redrawer?.redrawImmediately(updateNeeded = true)
} }
} }
} }
...@@ -651,7 +664,7 @@ fun orderEmojiAndSymbolsPopup() { ...@@ -651,7 +664,7 @@ fun orderEmojiAndSymbolsPopup() {
internal fun defaultFPSCounter( internal fun defaultFPSCounter(
component: Component component: Component
): FPSCounter? = with(SkikoProperties) { ): FPSCounter? = with(SkikoProperties) {
if (!SkikoProperties.fpsEnabled) return@with null if (!fpsEnabled) return@with null
// it is slow on Linux (100ms), so we cache it. Also refreshRate available only after window is visible // it is slow on Linux (100ms), so we cache it. Also refreshRate available only after window is visible
val refreshRate by lazy { component.graphicsConfiguration.device.displayMode.refreshRate } val refreshRate by lazy { component.graphicsConfiguration.device.displayMode.refreshRate }
......
...@@ -66,6 +66,7 @@ internal class MetalContextHandler( ...@@ -66,6 +66,7 @@ internal class MetalContextHandler(
super.flush() super.flush()
surface?.flushAndSubmit() surface?.flushAndSubmit()
finishFrame() finishFrame()
Logger.debug { "MetalContextHandler finished drawing frame" }
} }
override fun rendererInfo(): String { override fun rendererInfo(): String {
......
...@@ -33,7 +33,7 @@ internal abstract class AWTRedrawer( ...@@ -33,7 +33,7 @@ internal abstract class AWTRedrawer(
* Should be called when the device name is known as early, as possible. * Should be called when the device name is known as early, as possible.
*/ */
protected fun onDeviceChosen(deviceName: String?) { protected fun onDeviceChosen(deviceName: String?) {
require(!isDisposed) { "$javaClass is disposed" } checkDisposed()
require(deviceAnalytics == null) { "deviceAnalytics is not null" } require(deviceAnalytics == null) { "deviceAnalytics is not null" }
rendererAnalytics.deviceChosen() rendererAnalytics.deviceChosen()
deviceAnalytics = analytics.device(Version.skiko, hostOs, graphicsApi, deviceName) deviceAnalytics = analytics.device(Version.skiko, hostOs, graphicsApi, deviceName)
...@@ -44,13 +44,13 @@ internal abstract class AWTRedrawer( ...@@ -44,13 +44,13 @@ internal abstract class AWTRedrawer(
* Should be called when initialization of graphic context is ended. Only call it after [onDeviceChosen] * Should be called when initialization of graphic context is ended. Only call it after [onDeviceChosen]
*/ */
protected fun onContextInit() { protected fun onContextInit() {
require(!isDisposed) { "$javaClass is disposed" } checkDisposed()
requireNotNull(deviceAnalytics) { "deviceAnalytics is not null. Call onDeviceChosen after choosing the drawing device" } requireNotNull(deviceAnalytics) { "deviceAnalytics is not null. Call onDeviceChosen after choosing the drawing device" }
deviceAnalytics?.contextInit() deviceAnalytics?.contextInit()
} }
protected fun update(nanoTime: Long) { override fun update(nanoTime: Long) {
require(!isDisposed) { "$javaClass is disposed" } checkDisposed()
layer.update(nanoTime) layer.update(nanoTime)
} }
...@@ -69,4 +69,8 @@ internal abstract class AWTRedrawer( ...@@ -69,4 +69,8 @@ internal abstract class AWTRedrawer(
isFirstFrameRendered = true isFirstFrameRendered = true
} }
} }
protected fun checkDisposed() {
check(!isDisposed) { "${this.javaClass.simpleName} is disposed" }
}
} }
\ No newline at end of file
...@@ -22,22 +22,26 @@ internal abstract class AbstractDirectSoftwareRedrawer( ...@@ -22,22 +22,26 @@ internal abstract class AbstractDirectSoftwareRedrawer(
} }
if (layer.isShowing) { if (layer.isShowing) {
update(System.nanoTime()) update()
draw() draw()
} }
} }
protected var device = 0L protected var device = 0L
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
protected open fun draw() = inDrawScope(contextHandler::draw) protected open fun draw() = inDrawScope(contextHandler::draw)
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
update(System.nanoTime()) if (updateNeeded) {
draw() update()
}
if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
draw()
}
} }
open fun resize(width: Int, height: Int) = resize(device, width, height) open fun resize(width: Int, height: Int) = resize(device, width, height)
......
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.withContext
import org.jetbrains.skia.BackendRenderTarget import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.* import org.jetbrains.skiko.*
...@@ -60,16 +59,20 @@ internal class AngleRedrawer( ...@@ -60,16 +59,20 @@ internal class AngleRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "ANGLE redrawer is disposed" } checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
check(!isDisposed) { "ANGLE redrawer is disposed" } checkDisposed()
if (updateNeeded) {
update()
}
inDrawScope { inDrawScope {
update(System.nanoTime()) if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately)
}
} }
} }
......
...@@ -46,7 +46,7 @@ internal class Direct3DRedrawer( ...@@ -46,7 +46,7 @@ internal class Direct3DRedrawer(
private val frameDispatcher = FrameDispatcher(MainUIDispatcher) { private val frameDispatcher = FrameDispatcher(MainUIDispatcher) {
if (layer.isShowing) { if (layer.isShowing) {
update(System.nanoTime()) update()
draw() draw()
} }
} }
...@@ -63,16 +63,20 @@ internal class Direct3DRedrawer( ...@@ -63,16 +63,20 @@ internal class Direct3DRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "Direct3DRedrawer is disposed" } checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
check(!isDisposed) { "Direct3DRedrawer is disposed" } checkDisposed()
if (updateNeeded) {
update()
}
inDrawScope { inDrawScope {
update(System.nanoTime()) if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately)
}
} }
} }
......
...@@ -14,5 +14,6 @@ private val defaultFactory = Executors.defaultThreadFactory() ...@@ -14,5 +14,6 @@ private val defaultFactory = Executors.defaultThreadFactory()
internal val dispatcherToBlockOn = Executors.newCachedThreadPool { internal val dispatcherToBlockOn = Executors.newCachedThreadPool {
defaultFactory.newThread(it).apply { defaultFactory.newThread(it).apply {
isDaemon = true isDaemon = true
name = "skiko-dispatcher-to-block-on"
} }
}.asCoroutineDispatcher() }.asCoroutineDispatcher()
\ No newline at end of file
...@@ -60,7 +60,7 @@ internal class LinuxOpenGLRedrawer( ...@@ -60,7 +60,7 @@ internal class LinuxOpenGLRedrawer(
} }
override fun dispose() { override fun dispose() {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" } checkDisposed()
frameJob.cancel() frameJob.cancel()
layer.backedLayer.lockLinuxDrawingSurface { layer.backedLayer.lockLinuxDrawingSurface {
// makeCurrent is mandatory to destroy context, otherwise, OpenGL will destroy wrong context (from another window). // makeCurrent is mandatory to destroy context, otherwise, OpenGL will destroy wrong context (from another window).
...@@ -72,15 +72,17 @@ internal class LinuxOpenGLRedrawer( ...@@ -72,15 +72,17 @@ internal class LinuxOpenGLRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" } checkDisposed()
toRedraw.add(this) toRedraw.add(this)
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface { override fun redrawImmediately(updateNeeded: Boolean) = layer.backedLayer.lockLinuxDrawingSurface {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" } checkDisposed()
update(System.nanoTime()) if (updateNeeded) {
update()
}
inDrawScope { inDrawScope {
it.makeCurrent(context) it.makeCurrent(context)
contextHandler.draw() contextHandler.draw()
...@@ -116,7 +118,6 @@ internal class LinuxOpenGLRedrawer( ...@@ -116,7 +118,6 @@ internal class LinuxOpenGLRedrawer(
toRedrawVisible.maxByOrNull { it.frameLimit }?.limitFramesIfNeeded() toRedrawVisible.maxByOrNull { it.frameLimit }?.limitFramesIfNeeded()
val nanoTime = System.nanoTime() val nanoTime = System.nanoTime()
for (redrawer in toRedrawVisible) { for (redrawer in toRedrawVisible) {
try { try {
redrawer.update(nanoTime) redrawer.update(nanoTime)
......
...@@ -31,8 +31,8 @@ internal class LinuxSoftwareRedrawer( ...@@ -31,8 +31,8 @@ internal class LinuxSoftwareRedrawer(
super.draw() super.draw()
} }
override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface { override fun redrawImmediately(updateNeeded: Boolean) = layer.backedLayer.lockLinuxDrawingSurface {
super.redrawImmediately() super.redrawImmediately(updateNeeded)
} }
override fun resize(width: Int, height: Int) = layer.backedLayer.lockLinuxDrawingSurface { override fun resize(width: Int, height: Int) = layer.backedLayer.lockLinuxDrawingSurface {
......
...@@ -4,6 +4,7 @@ import kotlinx.coroutines.* ...@@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.jetbrains.skiko.* import org.jetbrains.skiko.*
import org.jetbrains.skiko.context.MetalContextHandler import org.jetbrains.skiko.context.MetalContextHandler
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.SwingUtilities.* import javax.swing.SwingUtilities.*
/** /**
...@@ -24,7 +25,6 @@ internal value class MetalDevice(val ptr: Long) ...@@ -24,7 +25,6 @@ internal value class MetalDevice(val ptr: Long)
* *
* This [MetalRedrawer] draws content on-screen for maximum efficiency, * This [MetalRedrawer] draws content on-screen for maximum efficiency,
* but it may prevent for using it in embedded components (such as interop with Swing). * but it may prevent for using it in embedded components (such as interop with Swing).
* For off-screen implementation see [MetalOffScreenRedrawer]
* *
* Content to draw is provided by [SkiaLayer.draw]. * Content to draw is provided by [SkiaLayer.draw].
* *
...@@ -34,7 +34,7 @@ internal value class MetalDevice(val ptr: Long) ...@@ -34,7 +34,7 @@ internal value class MetalDevice(val ptr: Long)
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
analytics: SkiaLayerAnalytics, analytics: SkiaLayerAnalytics,
private val properties: SkiaLayerProperties properties: SkiaLayerProperties
) : AWTRedrawer(layer, analytics, GraphicsApi.METAL) { ) : AWTRedrawer(layer, analytics, GraphicsApi.METAL) {
private val contextHandler: MetalContextHandler private val contextHandler: MetalContextHandler
...@@ -60,7 +60,7 @@ internal class MetalRedrawer( ...@@ -60,7 +60,7 @@ internal class MetalRedrawer(
} }
private val adapter = chooseMetalAdapter(properties.adapterPriority) private val adapter = chooseMetalAdapter(properties.adapterPriority)
private val displayLinkThrottler = DisplayLinkThrottler(layer.windowHandle) private val vSyncer = if (properties.isVsyncEnabled) MetalVSyncer(layer.windowHandle) else null
private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED) private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED)
@Volatile private var isWindowOccluded = false @Volatile private var isWindowOccluded = false
...@@ -78,12 +78,7 @@ internal class MetalRedrawer( ...@@ -78,12 +78,7 @@ internal class MetalRedrawer(
override val renderInfo: String get() = contextHandler.rendererInfo() override val renderInfo: String get() = contextHandler.rendererInfo()
private val frameDispatcher = FrameDispatcher(MainUIDispatcher) { private val frameDispatcher = FrameScheduler()
if (layer.isShowing) {
update(System.nanoTime())
draw()
}
}
init { init {
onContextInit() onContextInit()
...@@ -94,39 +89,41 @@ internal class MetalRedrawer( ...@@ -94,39 +89,41 @@ internal class MetalRedrawer(
contextHandler.dispose() contextHandler.dispose()
disposeDevice(device.ptr) disposeDevice(device.ptr)
adapter.dispose() adapter.dispose()
displayLinkThrottler.dispose() vSyncer?.dispose()
_device = null _device = null
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "MetalRedrawer is disposed" } checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame(needUpdate = true, throttledToVsync = throttledToVsync)
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
check(!isDisposed) { "MetalRedrawer is disposed" } checkDisposed()
inDrawScope { if (updateNeeded) {
update(System.nanoTime()) update()
if (!isDisposed) { // Redrawer may be disposed in user code, during `update` }
performDraw(waitVsync = SkikoProperties.macOSWaitForPreviousFrameVsyncOnRedrawImmediately) // Trying to draw immediately in Metal will result in lost (undrawn)
// frames if there are more than two between consecutive vsync events.
if (layer.isShowing) {
frameDispatcher.scheduleFrame(needUpdate = false, throttledToVsync = false)
} else {
// But if the layer isn't showing yet, we want to draw immediately,
// so that if it shows before the next vsync, there is no background flash
inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
performDraw()
}
} }
} }
} }
private suspend fun draw() { private suspend fun draw() {
// 2,3 GHz 8-Core Intel Core i9
//
// Test1. 8 windows, multiple clocks, 800x600
//
// Executors.newSingleThreadExecutor().asCoroutineDispatcher(): 20 FPS, 130% CPU
// Dispatchers.IO: 58 FPS, 460% CPU
//
// Test2. 60 windows, single clock, 800x600
//
// Executors.newSingleThreadExecutor().asCoroutineDispatcher(): 50 FPS, 150% CPU
// Dispatchers.IO: 50 FPS, 200% CPU
inDrawScope { inDrawScope {
// Move drawing to another thread to free the main thread
// It can be expensive to run it in the main thread and FPS can become unstable.
// This is visible by running [SkiaLayerPerformanceTest], standard deviation is increased significantly.
withContext(dispatcherToBlockOn) { withContext(dispatcherToBlockOn) {
performDraw() performDraw()
} }
...@@ -150,14 +147,8 @@ internal class MetalRedrawer( ...@@ -150,14 +147,8 @@ internal class MetalRedrawer(
windowOcclusionStateChannel.trySend(isOccluded) windowOcclusionStateChannel.trySend(isOccluded)
} }
private fun performDraw(waitVsync: Boolean = true) = synchronized(drawLock) { private fun performDraw() = synchronized(drawLock) {
if (!isDisposed) { if (!isDisposed) {
if (waitVsync) {
// Wait for vsync because:
// - macOS drops the second/next drawables if they are sent in the same vsync
// - it makes frames consistent and limits FPS
displayLinkThrottler.waitVSync()
}
autoreleasepool { autoreleasepool {
contextHandler.draw() contextHandler.draw()
} }
...@@ -178,7 +169,7 @@ internal class MetalRedrawer( ...@@ -178,7 +169,7 @@ internal class MetalRedrawer(
} }
override fun setVisible(isVisible: Boolean) { override fun setVisible(isVisible: Boolean) {
Logger.debug { "MetalRedrawer#setVisible $this $isVisible" } Logger.debug { "MetalRedrawer#setVisible($isVisible)" }
setLayerVisible(device.ptr, isVisible) setLayerVisible(device.ptr, isVisible)
} }
...@@ -196,4 +187,44 @@ internal class MetalRedrawer( ...@@ -196,4 +187,44 @@ internal class MetalRedrawer(
* @note see https://developer.apple.com/documentation/quartzcore/cametallayer/2887087-displaysyncenabled * @note see https://developer.apple.com/documentation/quartzcore/cametallayer/2887087-displaysyncenabled
*/ */
private external fun setDisplaySyncEnabled(device: Long, enabled: Boolean) private external fun setDisplaySyncEnabled(device: Long, enabled: Boolean)
private inner class FrameScheduler {
private var updateRequested = AtomicBoolean(false)
private fun updateIfRequested() {
if (updateRequested.getAndSet(false)) {
update()
}
}
private val updateDispatcher = FrameDispatcher(MainUIDispatcher) {
if (layer.isShowing) {
updateIfRequested()
}
}
private val frameDispatcher = FrameDispatcher(MainUIDispatcher) {
if (layer.isShowing) {
updateIfRequested()
draw()
}
vSyncer?.waitForVSync()
}
fun scheduleFrame(needUpdate: Boolean, throttledToVsync: Boolean) {
if (needUpdate) {
updateRequested.set(true)
if (!throttledToVsync) {
updateDispatcher.scheduleFrame()
}
}
frameDispatcher.scheduleFrame()
}
fun cancel() {
updateDispatcher.cancel()
frameDispatcher.cancel()
}
}
} }
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jetbrains.skiko.Library import org.jetbrains.skiko.Library
import org.jetbrains.skiko.RendezvousBroadcastChannel
internal class DisplayLinkThrottler(windowPtr: Long) {
/**
* A utility allowing several coroutines to wait for the next vsync.
*/
internal class MetalVSyncer(windowPtr: Long) {
// The underlying throttler that blocks a thread
private val displayLinkThrottler = DisplayLinkThrottler(windowPtr)
private val channel = RendezvousBroadcastChannel<Unit>()
// A channel to trigger the thread that waits on vsync to doing so
private val triggerResumeOnVSync = Channel<Unit>(Channel.CONFLATED)
private val job = CoroutineScope(dispatcherToBlockOn).launch {
while (isActive) {
triggerResumeOnVSync.receive() // Suspend until needed
displayLinkThrottler.waitVSync() // This blocks (not suspends!) the thread
if (isActive) {
channel.sendAll(Unit)
}
}
}.also {
it.invokeOnCompletion {
displayLinkThrottler.dispose()
}
}
/**
* Suspends until the next vsync.
*/
suspend fun waitForVSync() {
triggerResumeOnVSync.trySend(Unit)
channel.receive()
}
fun dispose() {
job.cancel()
}
}
private class DisplayLinkThrottler(windowPtr: Long) {
private val implPtr = create(windowPtr) private val implPtr = create(windowPtr)
internal fun dispose() = dispose(implPtr) fun dispose() = dispose(implPtr)
/* /*
* Creates a DisplayLink if needed with refresh rate matching NSScreen of NSWindow passed in [windowPtr]. * Creates a DisplayLink if needed with refresh rate matching NSScreen of NSWindow passed in [windowPtr].
...@@ -24,4 +70,4 @@ internal class DisplayLinkThrottler(windowPtr: Long) { ...@@ -24,4 +70,4 @@ internal class DisplayLinkThrottler(windowPtr: Long) {
Library.load() Library.load()
} }
} }
} }
\ No newline at end of file
...@@ -8,7 +8,7 @@ import org.jetbrains.skiko.context.SoftwareContextHandler ...@@ -8,7 +8,7 @@ import org.jetbrains.skiko.context.SoftwareContextHandler
internal class SoftwareRedrawer( internal class SoftwareRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
analytics: SkiaLayerAnalytics, analytics: SkiaLayerAnalytics,
private val properties: SkiaLayerProperties properties: SkiaLayerProperties
) : AWTRedrawer(layer, analytics, GraphicsApi.SOFTWARE_FAST) { ) : AWTRedrawer(layer, analytics, GraphicsApi.SOFTWARE_FAST) {
init { init {
onDeviceChosen("Software") onDeviceChosen("Software")
...@@ -17,16 +17,16 @@ internal class SoftwareRedrawer( ...@@ -17,16 +17,16 @@ internal class SoftwareRedrawer(
private val contextHandler = SoftwareContextHandler(layer) private val contextHandler = SoftwareContextHandler(layer)
override val renderInfo: String get() = contextHandler.rendererInfo() override val renderInfo: String get() = contextHandler.rendererInfo()
private val frameJob = Job() private val frameJob = if (properties.isVsyncEnabled && properties.isVsyncFramelimitFallbackEnabled) Job() else null
private val frameLimiter = layerFrameLimiter(CoroutineScope(frameJob), layer.backedLayer) private val frameLimiter = frameJob?.let {
layerFrameLimiter(CoroutineScope(it), layer.backedLayer)
}
private val frameDispatcher = FrameDispatcher(MainUIDispatcher) { private val frameDispatcher = FrameDispatcher(MainUIDispatcher) {
if (properties.isVsyncEnabled && properties.isVsyncFramelimitFallbackEnabled) { frameLimiter?.awaitNextFrame()
frameLimiter.awaitNextFrame()
}
if (layer.isShowing) { if (layer.isShowing) {
update(System.nanoTime()) update()
inDrawScope(contextHandler::draw) inDrawScope(contextHandler::draw)
} }
} }
...@@ -36,18 +36,25 @@ internal class SoftwareRedrawer( ...@@ -36,18 +36,25 @@ internal class SoftwareRedrawer(
} }
override fun dispose() { override fun dispose() {
frameJob.cancel() frameJob?.cancel()
frameDispatcher.cancel() frameDispatcher.cancel()
contextHandler.dispose() contextHandler.dispose()
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
update(System.nanoTime()) checkDisposed()
inDrawScope(contextHandler::draw) if (updateNeeded) {
update()
}
inDrawScope {
if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
contextHandler.draw()
}
}
} }
} }
\ No newline at end of file
...@@ -55,22 +55,26 @@ internal class WindowsOpenGLRedrawer( ...@@ -55,22 +55,26 @@ internal class WindowsOpenGLRedrawer(
super.dispose() super.dispose()
} }
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" } check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
toRedraw.add(this) toRedraw.add(this)
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" } check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
if (updateNeeded) {
update()
}
inDrawScope { inDrawScope {
update(System.nanoTime()) if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
makeCurrent() makeCurrent()
contextHandler.draw() contextHandler.draw()
swapBuffers() swapBuffers()
OpenGLApi.instance.glFinish() OpenGLApi.instance.glFinish()
if (SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) { if (SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) {
dwmFlush() dwmFlush()
}
} }
} }
} }
...@@ -95,7 +99,6 @@ internal class WindowsOpenGLRedrawer( ...@@ -95,7 +99,6 @@ internal class WindowsOpenGLRedrawer(
toRedraw.clear() toRedraw.clear()
val nanoTime = System.nanoTime() val nanoTime = System.nanoTime()
for (redrawer in toRedrawVisible) { for (redrawer in toRedrawVisible) {
try { try {
redrawer.update(nanoTime) redrawer.update(nanoTime)
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
...@@ -12,6 +13,7 @@ import org.jetbrains.skia.paragraph.ParagraphStyle ...@@ -12,6 +13,7 @@ import org.jetbrains.skia.paragraph.ParagraphStyle
import org.jetbrains.skia.paragraph.TextStyle import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.context.JvmContextHandler import org.jetbrains.skiko.context.JvmContextHandler
import org.jetbrains.skiko.redrawer.MetalRedrawer import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skiko.redrawer.MetalVSyncer
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.swing.SkiaSwingLayer import org.jetbrains.skiko.swing.SkiaSwingLayer
import org.jetbrains.skiko.util.ScreenshotTestRule import org.jetbrains.skiko.util.ScreenshotTestRule
...@@ -36,9 +38,10 @@ import javax.swing.JPanel ...@@ -36,9 +38,10 @@ import javax.swing.JPanel
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import javax.swing.WindowConstants import javax.swing.WindowConstants
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.math.absoluteValue
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.time.Duration import kotlin.time.Duration
...@@ -103,11 +106,11 @@ class SkiaLayerTest { ...@@ -103,11 +106,11 @@ class SkiaLayerTest {
override fun keyTyped(e: KeyEvent?) { override fun keyTyped(e: KeyEvent?) {
launch { launch {
val redrawer = window.layer.redrawer as MetalRedrawer val redrawer = window.layer.redrawer as MetalRedrawer
redrawer.redrawImmediately() redrawer.redrawImmediately(updateNeeded = true)
counter1 += 1 counter1 += 1
redrawer.redrawImmediately() redrawer.redrawImmediately(updateNeeded = true)
counter2 += 1 counter2 += 1
redrawer.redrawImmediately() redrawer.redrawImmediately(updateNeeded = true)
} }
} }
}) })
...@@ -273,19 +276,16 @@ class SkiaLayerTest { ...@@ -273,19 +276,16 @@ class SkiaLayerTest {
renderedWidth = -1 renderedWidth = -1
layer.size = Dimension(30, 40) layer.size = Dimension(30, 40)
layer.needRedraw()
delay(1000) delay(1000)
assertEquals((30 * density).toInt(), renderedWidth) assertEquals((30 * density).toInt(), renderedWidth)
renderedWidth = -1 renderedWidth = -1
layer.size = Dimension(0, 0) layer.size = Dimension(0, 0)
layer.needRedraw()
delay(1000) delay(1000)
assertEquals(0, renderedWidth) assertEquals(0, renderedWidth)
renderedWidth = -1 renderedWidth = -1
layer.size = Dimension(40, 40) layer.size = Dimension(40, 40)
layer.needRedraw()
delay(1000) delay(1000)
assertEquals((40 * density).toInt(), renderedWidth) assertEquals((40 * density).toInt(), renderedWidth)
} finally { } finally {
...@@ -546,10 +546,12 @@ class SkiaLayerTest { ...@@ -546,10 +546,12 @@ class SkiaLayerTest {
} }
} }
private abstract class BaseTestRedrawer: Redrawer { private abstract class BaseTestRedrawer(val layer: SkiaLayer): Redrawer {
override fun dispose() = Unit override fun dispose() = Unit
override fun needRedraw() = Unit override fun needRedraw(throttledToVsync: Boolean) = Unit
override fun redrawImmediately() = Unit override fun redrawImmediately(updateNeeded: Boolean) = Unit
override fun update(nanoTime: Long) = layer.update(nanoTime)
override val renderInfo: String override val renderInfo: String
get() = "" get() = ""
} }
...@@ -557,12 +559,12 @@ class SkiaLayerTest { ...@@ -557,12 +559,12 @@ class SkiaLayerTest {
@Test(timeout = 60000) @Test(timeout = 60000)
fun `fallback to software renderer, fail on init context`() = uiTest { fun `fallback to software renderer, fail on init context`() = uiTest {
testFallbackToSoftware { layer, _, _, _ -> testFallbackToSoftware { layer, _, _, _ ->
object : BaseTestRedrawer() { object : BaseTestRedrawer(layer) {
private val contextHandler = object : JvmContextHandler(layer) { private val contextHandler = object : JvmContextHandler(layer) {
override fun initContext() = false override fun initContext() = false
override fun initCanvas() = Unit override fun initCanvas() = Unit
} }
override fun redrawImmediately() = layer.inDrawScope(contextHandler::draw) override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope(contextHandler::draw)
} }
} }
} }
...@@ -575,8 +577,8 @@ class SkiaLayerTest { ...@@ -575,8 +577,8 @@ class SkiaLayerTest {
@Test(timeout = 60000) @Test(timeout = 60000)
fun `fallback to software renderer, fail on draw`() = uiTest { fun `fallback to software renderer, fail on draw`() = uiTest {
testFallbackToSoftware { layer, _, _, _ -> testFallbackToSoftware { layer, _, _, _ ->
object : BaseTestRedrawer() { object : BaseTestRedrawer(layer) {
override fun redrawImmediately() = layer.inDrawScope { override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope {
throw RenderException() throw RenderException()
} }
} }
...@@ -631,8 +633,8 @@ class SkiaLayerTest { ...@@ -631,8 +633,8 @@ class SkiaLayerTest {
fun `renderApi change callback is invoked on fallback`() = uiTest { fun `renderApi change callback is invoked on fallback`() = uiTest {
val window = UiTestWindow( val window = UiTestWindow(
renderFactory = OverrideNonSoftwareRenderFactory { layer, _, _, _ -> renderFactory = OverrideNonSoftwareRenderFactory { layer, _, _, _ ->
object : BaseTestRedrawer() { object : BaseTestRedrawer(layer) {
override fun redrawImmediately() = layer.inDrawScope { override fun redrawImmediately(updateNeeded: Boolean) = layer.inDrawScope {
throw RenderException() throw RenderException()
} }
} }
...@@ -1017,14 +1019,27 @@ class SkiaLayerTest { ...@@ -1017,14 +1019,27 @@ class SkiaLayerTest {
@Test @Test
fun `no window flash on hide or dispose while animation is running`() = uiTest { fun `no window flash on hide or dispose while animation is running`() = uiTest {
assumeTrue(hostOs == OS.MacOS) // Until the issue is fixed on Windows and Linux assumeTrue(hostOs.isMacOS)
// Until the issue is fixed in other redrawers
// Don't use assumeTrue, as uiTest iterates over multiple renderers,
// and if one of them skipped, the whole test is skipped
if (renderApi != GraphicsApi.METAL) return@uiTest
// Put up a large green window, and then repeatedly show and hide/dispose // Put up a large green window, and then repeatedly show and hide/dispose
// a smaller black window on top of it while screenshotting the pixel at the center, // a smaller black window on top of it while screenshotting the pixel at the center,
// and making sure that pixel is always either black or green. // and making sure that pixel is always either black or green.
val bgColor = Color.GREEN // Green // We can't compare colors exactly because java.awt.Robot can return a slightly different color due to
val fgColor = Color.BLACK // Black // system color profile
fun Color.closeTo(other: Color): Boolean {
val diffLimit = 10
return (red - other.red).absoluteValue < diffLimit
&& (green - other.green).absoluteValue < diffLimit
&& (blue - other.blue).absoluteValue < diffLimit
}
val bgColor = Color.GREEN
val fgColor = Color.BLACK
val backgroundWindow = JFrame().also { val backgroundWindow = JFrame().also {
it.size = Dimension(1000, 1000) it.size = Dimension(1000, 1000)
it.location = Point(200, 200) it.location = Point(200, 200)
...@@ -1051,10 +1066,10 @@ class SkiaLayerTest { ...@@ -1051,10 +1066,10 @@ class SkiaLayerTest {
Point(it.x + it.width/2, it.y + it.height/2) Point(it.x + it.width/2, it.y + it.height/2)
} }
var nonBlackPixelDetected = false var nonBlackPixelDetected: Color? = null
val stopThread = AtomicBoolean(false) val stopThread = AtomicBoolean(false)
// This semaphore ensures that screenshots are only taken when the window is becoming hidden/disposed. // This semaphore ensures that screenshots are only taken when the window is becoming hidden/disposed.
// It's needed because the window can (and does, with SOFTWARE_COMPAT) also flash when becoming visible. // It's necessary because the window can (and does, with SOFTWARE_COMPAT) also flash when becoming visible.
val semaphore = Semaphore(1, true) val semaphore = Semaphore(1, true)
val t = thread { val t = thread {
val robot = Robot() val robot = Robot()
...@@ -1062,36 +1077,37 @@ class SkiaLayerTest { ...@@ -1062,36 +1077,37 @@ class SkiaLayerTest {
semaphore.acquire() semaphore.acquire()
val pixel = robot.getPixelColor(pixelLocation.x, pixelLocation.y) val pixel = robot.getPixelColor(pixelLocation.x, pixelLocation.y)
semaphore.release() semaphore.release()
if ((pixel != fgColor) && (pixel != bgColor)) { if (!pixel.closeTo(fgColor) && !pixel.closeTo(bgColor)) {
println("window is visible: ${window.isVisible}") nonBlackPixelDetected = pixel
nonBlackPixelDetected = true
return@thread return@thread
} }
} }
} }
try { try {
// Check with `window.isVisible = false`
repeat(20) { repeat(20) {
delay(200) delay(200)
window.isVisible = false window.isVisible = false
delay(300) delay(300)
assertFalse(nonBlackPixelDetected, "Detected a non-black pixel when hiding window") assertNull(nonBlackPixelDetected, "Detected a non-black pixel when hiding window")
// Acquire the semaphore while making the window visible, to disable screenshotting // Acquire the semaphore while making the window visible, to disable screenshotting
semaphore.acquire() semaphore.acquire()
window.isVisible = true window.isVisible = true
delay(1000) delay(500)
semaphore.release() semaphore.release()
} }
// Check with `window.dispose()`
repeat(20) { repeat(20) {
delay(200) delay(200)
window.dispose() window.dispose()
delay(300) delay(300)
assertFalse(nonBlackPixelDetected, "Detected a non-black pixel when disposing window") assertNull(nonBlackPixelDetected, "Detected a non-black pixel when disposing window")
// Acquire the semaphore while making the window visible, to disable screenshotting // Acquire the semaphore while making the window visible, to disable screenshotting
semaphore.acquire() semaphore.acquire()
window.isVisible = true window.isVisible = true
delay(1000) delay(500)
semaphore.release() semaphore.release()
} }
} finally { } finally {
...@@ -1103,6 +1119,142 @@ class SkiaLayerTest { ...@@ -1103,6 +1119,142 @@ class SkiaLayerTest {
} }
} }
@Test
fun `temporary change is not visible`() = uiTest {
assumeTrue(hostOs.isMacOS)
// The separation between update and draw is only implemented in MetalRedrawer at the moment
// Don't use assumeTrue, as uiTest iterates over multiple renderers,
// and if one of them skipped, the whole test is skipped
if (renderApi != GraphicsApi.METAL) return@uiTest
val color = Color.BLACK
val tempColor = Color.WHITE
lateinit var renderDelegate: SolidColorRenderer
val renderChannel = Channel<Unit>(Channel.CONFLATED)
val window = UiTestWindow {
size = Dimension(600, 600)
location = Point(400, 400)
renderDelegate = object: SolidColorRenderer(
layer = layer,
color = color,
continuousRedraw = true
) {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
super.onRender(canvas, width, height, nanoTime)
assertTrue(renderChannel.trySend(Unit).isSuccess)
}
}
layer.renderDelegate = renderDelegate
contentPane.add(layer, BorderLayout.CENTER)
}
window.isVisible = true
delay(1500)
val pixelLocation = window.bounds.let {
Point(it.x + it.width/2, it.y + it.height/2)
}
val robot = Robot()
var tempColorVisibleCount = 0
try {
repeat(50) {
// Wait for just after the next vsync, so we have plenty of time until the one after it
val vSyncer = MetalVSyncer(window.layer.windowHandle)
vSyncer.waitForVSync()
// Set the color to temp, then immediately back to normal
renderDelegate.color = tempColor
renderDelegate.layer.needRedraw(throttledToVsync = false)
renderChannel.receive() // Wait until render is actually called
renderDelegate.color = color
renderDelegate.layer.needRedraw(throttledToVsync = false)
// Check whether the temp color was visible
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < 400) {
val pixel = robot.getPixelColor(pixelLocation.x, pixelLocation.y)
if (pixel != color) {
tempColorVisibleCount++
}
}
}
// Because the temp color can theoretically be visible if the JVM hiccups and a vsync happens before the
// color is reverted, we allow a small percentage of the tries to fail. This way the flakiness of the test
// is reduced.
// Note that in practice, however, this test had never failed on an M1 Ultra machine with a 60Hz monitor.
assertTrue(tempColorVisibleCount < 5)
} finally {
window.dispose()
}
}
@Test
fun `needRedraw throttled and regular calls render and draw once`() = uiTest {
// Check that calling both needRedraw(true) and needRedraw(false) causes only one render and one draw call
var renderCalls = 0
val renderChannel = Channel<Unit>(Channel.CONFLATED)
var drawCalls = 0
val deviceAnalytics = object : SkiaLayerAnalytics.DeviceAnalytics {
override fun beforeFrameRender() {
drawCalls++
}
}
val analytics = object : SkiaLayerAnalytics {
@ExperimentalSkikoApi
override fun device(
skikoVersion: String,
os: OS,
api: GraphicsApi,
deviceName: String?
): SkiaLayerAnalytics.DeviceAnalytics {
return deviceAnalytics
}
}
val window = UiTestWindow(analytics = analytics) {
size = Dimension(600, 600)
location = Point(400, 400)
layer.renderDelegate = object: SkikoRenderDelegate {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
renderCalls++
renderChannel.trySend(Unit)
}
}
contentPane.add(layer, BorderLayout.CENTER)
}
window.isVisible = true
// Wait for things to settle down, specifically the workaround for JBR-5259, which moves
// the backed layer when graphicsContextScaleTransform changes
delay(100)
try {
renderChannel.receive()
renderCalls = 0
drawCalls = 0
withContext(MainUIDispatcher) {
window.layer.needRedraw(true)
window.layer.needRedraw(false)
}
delay(100)
assertEquals("Render was called more than once on needRedraw(true), needRedraw(false)", 1, renderCalls)
assertEquals("Draw was called more than once on needRedraw(true), needRedraw(false)", 1, drawCalls)
renderCalls = 0
drawCalls = 0
withContext(MainUIDispatcher) {
window.layer.needRedraw(false)
window.layer.needRedraw(true)
}
delay(100)
assertEquals("Render was called more than once on needRedraw(false), needRedraw(true)", 1, renderCalls)
assertEquals("Draw was called more than once on needRedraw(true), needRedraw(true)", 1, drawCalls)
} finally {
window.dispose()
}
}
private class RectRenderer( private class RectRenderer(
private val getContentScale: () -> Float, private val getContentScale: () -> Float,
var rectWidth: Int, var rectWidth: Int,
...@@ -1139,6 +1291,8 @@ class SkiaLayerTest { ...@@ -1139,6 +1291,8 @@ class SkiaLayerTest {
} }
} }
private class AnimatedBoxRenderer( private class AnimatedBoxRenderer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
private val pixelsPerSecond: Double, private val pixelsPerSecond: Double,
...@@ -1166,7 +1320,7 @@ class SkiaLayerTest { ...@@ -1166,7 +1320,7 @@ class SkiaLayerTest {
} }
} }
private class SolidColorRenderer( private open class SolidColorRenderer(
val layer: SkiaLayer, val layer: SkiaLayer,
color: Color, color: Color,
continuousRedraw: Boolean = false continuousRedraw: Boolean = false
...@@ -1175,11 +1329,18 @@ class SkiaLayerTest { ...@@ -1175,11 +1329,18 @@ class SkiaLayerTest {
var continuousRedraw = continuousRedraw var continuousRedraw = continuousRedraw
set(value) { set(value) {
if (value) if (value)
layer.needRedraw() layer.needRedraw(throttledToVsync = true)
field = value
}
var color = color
set(value) {
Logger.debug { "Color set to $value" }
field = value field = value
paint.color = color.rgb
} }
val paint = Paint().also { it.color = color.rgb } private var paint = Paint().also { it.color = color.rgb }
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), paint) canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), paint)
......
...@@ -13,11 +13,11 @@ import kotlin.coroutines.CoroutineContext ...@@ -13,11 +13,11 @@ import kotlin.coroutines.CoroutineContext
*/ */
class FrameDispatcher( class FrameDispatcher(
scope: CoroutineScope, scope: CoroutineScope,
private val onFrame: suspend () -> Unit private val onFrame: suspend CoroutineScope.() -> Unit
) { ) {
constructor( constructor(
context: CoroutineContext, context: CoroutineContext,
onFrame: suspend () -> Unit onFrame: suspend CoroutineScope.() -> Unit
) : this( ) : this(
CoroutineScope(context), CoroutineScope(context),
onFrame onFrame
...@@ -56,7 +56,7 @@ class FrameDispatcher( ...@@ -56,7 +56,7 @@ class FrameDispatcher(
fun scheduleFrame() { fun scheduleFrame() {
if (!frameScheduled) { if (!frameScheduled) {
frameScheduled = true frameScheduled = true
frameChannel.trySend(Unit).isSuccess frameChannel.trySend(Unit)
} }
} }
} }
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlin.time.Duration.Companion.nanoseconds
interface SkikoLoggerInterface { interface SkikoLoggerInterface {
val isTraceEnabled: Boolean val isTraceEnabled: Boolean
val isDebugEnabled: Boolean val isDebugEnabled: Boolean
...@@ -36,11 +38,14 @@ internal enum class LogLevel { ...@@ -36,11 +38,14 @@ internal enum class LogLevel {
} }
} }
class DefaultConsoleLogger(override val isTraceEnabled: Boolean = false, class DefaultConsoleLogger(
override val isDebugEnabled: Boolean = false, override val isTraceEnabled: Boolean = false,
override val isInfoEnabled: Boolean = true, override val isDebugEnabled: Boolean = false,
override val isWarnEnabled: Boolean = true, override val isInfoEnabled: Boolean = true,
override val isErrorEnabled: Boolean = true): SkikoLoggerInterface { override val isWarnEnabled: Boolean = true,
override val isErrorEnabled: Boolean = true,
private val logTimestamp: Boolean = false
): SkikoLoggerInterface {
companion object { companion object {
fun fromLevel(level: String): DefaultConsoleLogger { fun fromLevel(level: String): DefaultConsoleLogger {
...@@ -55,48 +60,52 @@ class DefaultConsoleLogger(override val isTraceEnabled: Boolean = false, ...@@ -55,48 +60,52 @@ class DefaultConsoleLogger(override val isTraceEnabled: Boolean = false,
} }
} }
private fun logMessagePrefix() = if (logTimestamp) {
"[${currentNanoTime().nanoseconds.inWholeMilliseconds.mod(1_000)}]"
} else "[SKIKO]"
override fun trace(message: String) { override fun trace(message: String) {
println("[SKIKO] trace: $message") println("${logMessagePrefix()} trace: $message")
} }
override fun trace(t: Throwable, message: String) { override fun trace(t: Throwable, message: String) {
println("[SKIKO] trace: $message") println("${logMessagePrefix()} trace: $message")
println(t) println(t)
} }
override fun debug(message: String) { override fun debug(message: String) {
println("[SKIKO] debug: $message") println("${logMessagePrefix()} debug: $message")
} }
override fun debug(t: Throwable, message: String) { override fun debug(t: Throwable, message: String) {
println("[SKIKO] debug: $message") println("${logMessagePrefix()} debug: $message")
println(t) println(t)
} }
override fun info(message: String) { override fun info(message: String) {
println("[SKIKO] info: $message") println("${logMessagePrefix()} info: $message")
} }
override fun info(t: Throwable, message: String) { override fun info(t: Throwable, message: String) {
println("[SKIKO] info: $message") println("${logMessagePrefix()} info: $message")
println(t) println(t)
} }
override fun warn(message: String) { override fun warn(message: String) {
println("[SKIKO] warn: $message") println("${logMessagePrefix()} warn: $message")
} }
override fun warn(t: Throwable, message: String) { override fun warn(t: Throwable, message: String) {
println("[SKIKO] warn: $message") println("${logMessagePrefix()} warn: $message")
println(t) println(t)
} }
override fun error(message: String) { override fun error(message: String) {
println("[SKIKO] error: $message") println("${logMessagePrefix()} error: $message")
} }
override fun error(t: Throwable, message: String) { override fun error(t: Throwable, message: String) {
println("[SKIKO] error: $message") println("${logMessagePrefix()} error: $message")
println(t) println(t)
} }
} }
......
...@@ -2,33 +2,37 @@ package org.jetbrains.skiko ...@@ -2,33 +2,37 @@ package org.jetbrains.skiko
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.Continuation import kotlin.coroutines.*
import kotlin.coroutines.resume
/** /**
* Behaves as `Channel<Unit>(Channel.RENDEZVOUS)`, but with ability to send a value to all current consumers * Behaves like `Channel<Unit>(Channel.RENDEZVOUS)`, but with the ability to send a value to all current consumers
* (which will await on `receive` method). * suspended in [receive].
*/ */
internal class RendezvousBroadcastChannel<T> { internal class RendezvousBroadcastChannel<T> {
private val onRequest = Channel<Unit>(Channel.CONFLATED) private val onRequest = Channel<Unit>(Channel.CONFLATED)
private val receivers = mutableListOf<Continuation<T>>() private var suspended = mutableListOf<Continuation<T>>()
private var suspendedCopy = mutableListOf<Continuation<T>>()
/** /**
* Send value to all current consumers which await value on `receive` method, or await for the first one. * Send value to all current consumers that await value on `receive` method, or await for the first one.
* *
* Can't be called concurrently from multiple threads. * Can't be called concurrently from multiple threads.
*/ */
suspend fun sendAll(value: T) { suspend fun sendAll(value: T) {
onRequest.receive() onRequest.receive()
val receiversCopy = maybeSynchronized(receivers) {
mutableListOf<Continuation<T>>().apply { // Swap the lists
addAll(receivers) maybeSynchronized(this) {
receivers.clear() val tmp = suspended
} suspended = suspendedCopy
suspendedCopy = tmp
} }
for (receiver in receiversCopy) {
receiver.resume(value) // Safe to touch `suspendedCopy` without lock because receive will now add to `suspended`.
for (cont in suspendedCopy) {
cont.resume(value)
} }
suspendedCopy.clear()
} }
/** /**
...@@ -37,8 +41,8 @@ internal class RendezvousBroadcastChannel<T> { ...@@ -37,8 +41,8 @@ internal class RendezvousBroadcastChannel<T> {
* Can be called concurrently from multiple threads. * Can be called concurrently from multiple threads.
*/ */
suspend fun receive(): T = suspendCancellableCoroutine { continuation -> suspend fun receive(): T = suspendCancellableCoroutine { continuation ->
maybeSynchronized(receivers) { maybeSynchronized(this) {
receivers.add(continuation) suspended.add(continuation)
} }
onRequest.trySend(Unit) onRequest.trySend(Unit)
} }
......
...@@ -55,9 +55,13 @@ expect open class SkiaLayer { ...@@ -55,9 +55,13 @@ expect open class SkiaLayer {
fun detach() fun detach()
/** /**
* Force redraw. * Request redrawing of the content; The [renderDelegate] will be asked to re-render, and the result will be drawn
* on the screen.
*
* @param throttledToVsync Whether to throttle calling [renderDelegate]'s [SkikoRenderDelegate.onRender] to at most
* once between vsync signals (if vsync is enabled).
*/ */
fun needRedraw() fun needRedraw(throttledToVsync: Boolean = true)
/** /**
* Drawing function. * Drawing function.
......
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlin.time.TimeSource
private val initialTime = TimeSource.Monotonic.markNow()
internal interface Redrawer { internal interface Redrawer {
fun dispose() fun dispose()
fun needRedraw() fun needRedraw(throttledToVsync: Boolean)
fun redrawImmediately() fun redrawImmediately(updateNeeded: Boolean)
fun syncBounds() = Unit fun syncBounds() = Unit
fun update(nanoTime: Long = initialTime.elapsedNow().inWholeNanoseconds)
fun setVisible(isVisible: Boolean) = Unit fun setVisible(isVisible: Boolean) = Unit
val renderInfo: String val renderInfo: String
} }
\ No newline at end of file
...@@ -48,7 +48,7 @@ actual open class SkiaLayer { ...@@ -48,7 +48,7 @@ actual open class SkiaLayer {
/** /**
* Schedules a drawFrame to the appropriate moment. * Schedules a drawFrame to the appropriate moment.
*/ */
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
state?.needRedraw() state?.needRedraw()
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.TimeSource
/** /**
* Limit the duration of the frames (to avoid high CPU usage) to [frameMillis]. * Limit the duration of the frames (to avoid high CPU usage) to [frameMillis].
...@@ -32,7 +30,7 @@ class FrameLimiter( ...@@ -32,7 +30,7 @@ class FrameLimiter(
private suspend fun preciseDelay(millis: Long) { private suspend fun preciseDelay(millis: Long) {
val start = currentTime() val start = currentTime()
// delay aren't precise, so we should measure what is the actual precision of delay is, // delay isn't precise, so we should measure what the actual precision of delay is,
// so we don't wait longer than we need // so we don't wait longer than we need
var actual1msDelay = 1.milliseconds var actual1msDelay = 1.milliseconds
......
...@@ -42,10 +42,6 @@ object SkikoProperties { ...@@ -42,10 +42,6 @@ object SkikoProperties {
} }
} }
val macOSWaitForPreviousFrameVsyncOnRedrawImmediately: Boolean get() {
return getProperty("skiko.rendering.macos.waitForPreviousFrameVsyncOnRedrawImmediately")?.toBoolean() ?: true
}
val windowsWaitForVsyncOnRedrawImmediately: Boolean get() { val windowsWaitForVsyncOnRedrawImmediately: Boolean get() {
return getProperty("skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false return getProperty("skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.pauseDispatcher
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
......
...@@ -17,7 +17,7 @@ actual open class SkiaLayer { ...@@ -17,7 +17,7 @@ actual open class SkiaLayer {
set(value) {} set(value) {}
actual val component: Any? actual val component: Any?
get() = TODO("Not yet implemented") get() = TODO("Not yet implemented")
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
TODO("unimplemented") TODO("unimplemented")
} }
actual fun attachTo(container: Any) { actual fun attachTo(container: Any) {
......
...@@ -85,13 +85,13 @@ actual open class SkiaLayer { ...@@ -85,13 +85,13 @@ actual open class SkiaLayer {
@ObjCAction @ObjCAction
fun frameDidChange(notification: NSNotification) { fun frameDidChange(notification: NSNotification) {
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.redrawImmediately() redrawer?.redrawImmediately(updateNeeded = true)
} }
@ObjCAction @ObjCAction
fun windowDidChangeBackingProperties(notification: NSNotification) { fun windowDidChangeBackingProperties(notification: NSNotification) {
redrawer?.syncBounds() redrawer?.syncBounds()
redrawer?.redrawImmediately() redrawer?.redrawImmediately(updateNeeded = true)
} }
fun addObserver() { fun addObserver() {
...@@ -140,8 +140,8 @@ actual open class SkiaLayer { ...@@ -140,8 +140,8 @@ actual open class SkiaLayer {
/** /**
* Schedules a frame to an appropriate moment. * Schedules a frame to an appropriate moment.
*/ */
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
redrawer?.needRedraw() redrawer?.needRedraw(throttledToVsync)
} }
/** /**
......
...@@ -14,6 +14,7 @@ import org.jetbrains.skiko.SkikoDispatchers ...@@ -14,6 +14,7 @@ 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 org.jetbrains.skiko.currentNanoTime
import platform.AppKit.NSWindowDidChangeOcclusionStateNotification import platform.AppKit.NSWindowDidChangeOcclusionStateNotification
import platform.AppKit.NSWindowOcclusionStateVisible import platform.AppKit.NSWindowOcclusionStateVisible
import platform.CoreGraphics.CGColorCreate import platform.CoreGraphics.CGColorCreate
...@@ -28,7 +29,6 @@ import platform.QuartzCore.CATransaction ...@@ -28,7 +29,6 @@ import platform.QuartzCore.CATransaction
import platform.QuartzCore.kCAGravityTopLeft import platform.QuartzCore.kCAGravityTopLeft
import platform.QuartzCore.kCALayerHeightSizable import platform.QuartzCore.kCALayerHeightSizable
import platform.QuartzCore.kCALayerWidthSizable import platform.QuartzCore.kCALayerWidthSizable
import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake import platform.CoreGraphics.CGSizeMake
import platform.Foundation.NSNotification import platform.Foundation.NSNotification
import platform.Foundation.NSNotificationCenter import platform.Foundation.NSNotificationCenter
...@@ -127,22 +127,33 @@ internal class MacOsMetalRedrawer( ...@@ -127,22 +127,33 @@ internal class MacOsMetalRedrawer(
CATransaction.flush() CATransaction.flush()
} }
private fun checkDisposed() {
check(!isDisposed) { "MetalRedrawer is disposed" }
}
/** /**
* Schedules a frame [draw] to an appropriate moment. * Schedules a frame [draw] to an appropriate moment.
*/ */
override fun needRedraw() { override fun needRedraw(throttledToVsync: Boolean) {
check(!isDisposed) { "MetalRedrawer is disposed" } checkDisposed()
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun update(nanoTime: Long) {
checkDisposed()
skiaLayer.update(nanoTime)
}
/** /**
* Invokes [draw] right away. * Invokes [draw] right away.
*/ */
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
check(!isDisposed) { "MetalRedrawer is disposed" } checkDisposed()
autoreleasepool { autoreleasepool {
if (!isDisposed) { if (!isDisposed && updateNeeded) {
skiaLayer.update(getTimeNanos()) update()
}
if (!isDisposed) { // Redrawer may be disposed in user code, during `update`
contextHandler.draw() contextHandler.draw()
} }
} }
...@@ -151,7 +162,7 @@ internal class MacOsMetalRedrawer( ...@@ -151,7 +162,7 @@ internal class MacOsMetalRedrawer(
private suspend fun draw() { private suspend fun draw() {
autoreleasepool { autoreleasepool {
if (!isDisposed) { if (!isDisposed) {
skiaLayer.update(getTimeNanos()) update()
contextHandler.draw() contextHandler.draw()
} }
} }
...@@ -208,7 +219,7 @@ internal class MetalLayer : CAMetalLayer { ...@@ -208,7 +219,7 @@ internal class MetalLayer : CAMetalLayer {
this.framebufferOnly = false this.framebufferOnly = false
skiaLayer.nsView.layer = this skiaLayer.nsView.layer = this
skiaLayer.nsView.wantsLayer = true skiaLayer.nsView.wantsLayer = true
this.contentsGravity = kCAGravityTopLeft; this.contentsGravity = kCAGravityTopLeft
} }
fun dispose() { fun dispose() {
...@@ -216,7 +227,7 @@ internal class MetalLayer : CAMetalLayer { ...@@ -216,7 +227,7 @@ internal class MetalLayer : CAMetalLayer {
} }
override fun drawInContext(ctx: CGContextRef?) { override fun drawInContext(ctx: CGContextRef?) {
skiaLayer.update(getTimeNanos()) skiaLayer.update(currentNanoTime())
contextHandler.draw() contextHandler.draw()
} }
} }
...@@ -7,6 +7,7 @@ import org.jetbrains.skiko.SkiaLayer ...@@ -7,6 +7,7 @@ import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkikoDispatchers import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOSOpenGLContextHandler import org.jetbrains.skiko.context.MacOSOpenGLContextHandler
import org.jetbrains.skiko.currentNanoTime
import platform.CoreFoundation.CFTimeInterval import platform.CoreFoundation.CFTimeInterval
import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectMake
import platform.CoreVideo.CVTimeStamp import platform.CoreVideo.CVTimeStamp
...@@ -15,7 +16,6 @@ import platform.OpenGLCommon.CGLPixelFormatObj ...@@ -15,7 +16,6 @@ import platform.OpenGLCommon.CGLPixelFormatObj
import platform.OpenGLCommon.CGLSetCurrentContext import platform.OpenGLCommon.CGLSetCurrentContext
import platform.QuartzCore.CAOpenGLLayer import platform.QuartzCore.CAOpenGLLayer
import platform.QuartzCore.* import platform.QuartzCore.*
import kotlin.system.getTimeNanos
/** /**
* OpenGL [Redrawer] implementation for MacOs. * OpenGL [Redrawer] implementation for MacOs.
...@@ -35,7 +35,7 @@ internal class MacOsOpenGLRedrawer( ...@@ -35,7 +35,7 @@ internal class MacOsOpenGLRedrawer(
} }
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) { private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
redrawImmediately() redrawImmediately(updateNeeded = true)
} }
override fun dispose() { override fun dispose() {
...@@ -63,11 +63,15 @@ internal class MacOsOpenGLRedrawer( ...@@ -63,11 +63,15 @@ internal class MacOsOpenGLRedrawer(
CATransaction.flush() CATransaction.flush()
} }
override fun needRedraw() { override fun update(nanoTime: Long) {
skiaLayer.update(nanoTime)
}
override fun needRedraw(throttledToVsync: Boolean) {
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately(updateNeeded: Boolean) {
glLayer.setNeedsDisplay() glLayer.setNeedsDisplay()
skiaLayer.nsView.setNeedsDisplay(true) skiaLayer.nsView.setNeedsDisplay(true)
} }
...@@ -90,7 +94,7 @@ internal class MacosGLLayer : CAOpenGLLayer { ...@@ -90,7 +94,7 @@ internal class MacosGLLayer : CAOpenGLLayer {
this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable ) this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable )
skiaLayer.nsView.layer = this skiaLayer.nsView.layer = this
skiaLayer.nsView.wantsLayer = true skiaLayer.nsView.wantsLayer = true
this.contentsGravity = kCAGravityTopLeft; this.contentsGravity = kCAGravityTopLeft
} }
fun setFrame(x: Int, y: Int, width: Int, height: Int) { fun setFrame(x: Int, y: Int, width: Int, height: Int) {
...@@ -121,9 +125,9 @@ internal class MacosGLLayer : CAOpenGLLayer { ...@@ -121,9 +125,9 @@ internal class MacosGLLayer : CAOpenGLLayer {
forLayerTime: CFTimeInterval, forLayerTime: CFTimeInterval,
displayTime: CPointer<CVTimeStamp>? displayTime: CPointer<CVTimeStamp>?
) { ) {
CGLSetCurrentContext(ctx); CGLSetCurrentContext(ctx)
try { try {
skiaLayer.update(getTimeNanos()) skiaLayer.update(currentNanoTime())
contextHandler.draw() contextHandler.draw()
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTrace() e.printStackTrace()
......
...@@ -4,8 +4,6 @@ import kotlinx.cinterop.useContents ...@@ -4,8 +4,6 @@ import kotlinx.cinterop.useContents
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry import org.jetbrains.skia.PixelGeometry
import org.jetbrains.skia.Surface import org.jetbrains.skia.Surface
import platform.UIKit.*
import kotlin.system.getTimeNanos
actual open class SkiaLayer { actual open class SkiaLayer {
internal var needRedrawCallback: () -> Unit = {} internal var needRedrawCallback: () -> Unit = {}
...@@ -25,7 +23,7 @@ actual open class SkiaLayer { ...@@ -25,7 +23,7 @@ actual open class SkiaLayer {
get() = false get() = false
set(_) { throw UnsupportedOperationException() } set(_) { throw UnsupportedOperationException() }
actual fun needRedraw() { actual fun needRedraw(throttledToVsync: Boolean) {
needRedrawCallback.invoke() needRedrawCallback.invoke()
} }
...@@ -63,7 +61,7 @@ actual open class SkiaLayer { ...@@ -63,7 +61,7 @@ actual open class SkiaLayer {
} }
internal fun draw(surface: Surface) { internal fun draw(surface: Surface) {
renderDelegate?.onRender(surface.canvas, surface.width, surface.height, getTimeNanos()) renderDelegate?.onRender(surface.canvas, surface.width, surface.height, currentNanoTime())
} }
actual val pixelGeometry: PixelGeometry actual val pixelGeometry: PixelGeometry
......
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