Unverified Commit a0d5e2fe authored by Igor Demin's avatar Igor Demin Committed by GitHub

Fix metal deadlock (#217)

parent 3923076d
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skiko.redrawer.Direct3DRedrawer
import org.jetbrains.skiko.redrawer.LinuxOpenGLRedrawer
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.redrawer.SoftwareRedrawer
import org.jetbrains.skiko.redrawer.WindowsOpenGLRedrawer
import java.awt.Component import java.awt.Component
import java.awt.Window import java.awt.Window
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
...@@ -17,7 +10,6 @@ internal interface PlatformOperations { ...@@ -17,7 +10,6 @@ internal interface PlatformOperations {
fun disableTitleBar(platformInfo: Long) fun disableTitleBar(platformInfo: Long)
fun orderEmojiAndSymbolsPopup() fun orderEmojiAndSymbolsPopup()
fun getDpiScale(component: Component): Float fun getDpiScale(component: Component): Float
fun createRedrawer(layer: SkiaLayer, renderApi: GraphicsApi, properties: SkiaLayerProperties): Redrawer
} }
internal val platformOperations: PlatformOperations by lazy { internal val platformOperations: PlatformOperations by lazy {
...@@ -42,16 +34,6 @@ internal val platformOperations: PlatformOperations by lazy { ...@@ -42,16 +34,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun orderEmojiAndSymbolsPopup() { override fun orderEmojiAndSymbolsPopup() {
osxOrderEmojiAndSymbolsPopup() osxOrderEmojiAndSymbolsPopup()
} }
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
GraphicsApi.METAL -> MetalRedrawer(layer, properties)
else -> MacOsOpenGLRedrawer(layer, properties)
}
} }
OS.Windows -> { OS.Windows -> {
object: PlatformOperations { object: PlatformOperations {
...@@ -76,16 +58,6 @@ internal val platformOperations: PlatformOperations by lazy { ...@@ -76,16 +58,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float { override fun getDpiScale(component: Component): Float {
return component.graphicsConfiguration.defaultTransform.scaleX.toFloat() return component.graphicsConfiguration.defaultTransform.scaleX.toFloat()
} }
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
GraphicsApi.DIRECT3D -> Direct3DRedrawer(layer, properties)
else -> WindowsOpenGLRedrawer(layer, properties)
}
} }
} }
OS.Linux -> { OS.Linux -> {
...@@ -111,15 +83,6 @@ internal val platformOperations: PlatformOperations by lazy { ...@@ -111,15 +83,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float { override fun getDpiScale(component: Component): Float {
return component.graphicsConfiguration.defaultTransform.scaleX.toFloat() return component.graphicsConfiguration.defaultTransform.scaleX.toFloat()
} }
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = when(renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
else -> LinuxOpenGLRedrawer(layer, properties)
}
} }
} }
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import java.io.File import java.io.File
import java.lang.RuntimeException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
import org.jetbrains.skiko.hostFullName
internal class RenderException(
message: String? = null,
cause: Exception? = null
) : RuntimeException(message, cause)
internal class RenderExceptionsHandler { internal class RenderExceptionsHandler {
companion object { companion object {
...@@ -16,7 +21,7 @@ internal class RenderExceptionsHandler { ...@@ -16,7 +21,7 @@ internal class RenderExceptionsHandler {
"${Library.cacheRoot}/skiko-render-exception-${ProcessHandle.current().pid()}.log" "${Library.cacheRoot}/skiko-render-exception-${ProcessHandle.current().pid()}.log"
) )
} }
val exception = Exception(message) val exception = RenderException(message)
if (System.getProperty("skiko.win.exception.logger.enabled") == "true") { if (System.getProperty("skiko.win.exception.logger.enabled") == "true") {
writeLog(exception) writeLog(exception)
} }
...@@ -40,16 +45,6 @@ internal class RenderExceptionsHandler { ...@@ -40,16 +45,6 @@ internal class RenderExceptionsHandler {
} }
output?.appendText(outputBuilder.toString()) output?.appendText(outputBuilder.toString())
} }
}
}
fun <T> withExceptionHandler(onException: () -> T, action: () -> T): T {
try {
return action()
} catch (e: Exception) {
println(e.message)
return onException()
} }
} }
......
package org.jetbrains.skiko
import org.jetbrains.skiko.context.*
import org.jetbrains.skiko.redrawer.*
internal interface RenderFactory {
fun createContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler
fun createRedrawer(layer: SkiaLayer, renderApi: GraphicsApi, properties: SkiaLayerProperties): Redrawer
companion object {
val Default = object : RenderFactory {
override fun createContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
return when (renderApi) {
GraphicsApi.SOFTWARE -> SoftwareContextHandler(layer)
GraphicsApi.OPENGL -> OpenGLContextHandler(layer)
GraphicsApi.DIRECT3D -> Direct3DContextHandler(layer)
GraphicsApi.METAL -> MetalContextHandler(layer)
else -> TODO("Unsupported yet.")
}
}
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer = when (hostOs) {
OS.MacOS -> when (renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
else -> MetalRedrawer(layer, properties)
}
OS.Windows -> when (renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
GraphicsApi.OPENGL -> WindowsOpenGLRedrawer(layer, properties)
else -> Direct3DRedrawer(layer, properties)
}
OS.Linux -> when (renderApi) {
GraphicsApi.SOFTWARE -> SoftwareRedrawer(layer, properties)
else -> LinuxOpenGLRedrawer(layer, properties)
}
}
}
}
}
\ No newline at end of file
...@@ -2,12 +2,11 @@ package org.jetbrains.skiko ...@@ -2,12 +2,11 @@ package org.jetbrains.skiko
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.createContextHandler
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.withExceptionHandler
import java.awt.Graphics import java.awt.Graphics
import java.awt.event.* import java.awt.event.*
import java.awt.im.InputMethodRequests import java.awt.im.InputMethodRequests
import java.util.concurrent.CancellationException
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
...@@ -17,8 +16,9 @@ interface SkiaRenderer { ...@@ -17,8 +16,9 @@ interface SkiaRenderer {
private class PictureHolder(val instance: Picture, val width: Int, val height: Int) private class PictureHolder(val instance: Picture, val width: Int, val height: Int)
open class SkiaLayer( open class SkiaLayer internal constructor(
private val properties: SkiaLayerProperties = SkiaLayerProperties() private val properties: SkiaLayerProperties = SkiaLayerProperties(),
private val renderFactory: RenderFactory
) : JPanel() { ) : JPanel() {
companion object { companion object {
init { init {
...@@ -33,6 +33,10 @@ open class SkiaLayer( ...@@ -33,6 +33,10 @@ open class SkiaLayer(
internal val backedLayer: HardwareLayer internal val backedLayer: HardwareLayer
internal constructor(
properties: SkiaLayerProperties = SkiaLayerProperties()
) : this(properties, RenderFactory.Default)
val canvas: java.awt.Canvas val canvas: java.awt.Canvas
get() = backedLayer get() = backedLayer
...@@ -131,12 +135,8 @@ open class SkiaLayer( ...@@ -131,12 +135,8 @@ open class SkiaLayer(
private var picture: PictureHolder? = null private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder() private val pictureRecorder = PictureRecorder()
private val pictureLock = Any() private val pictureLock = Any()
private val onExceptionAction: () -> Boolean = {
findNextWorkingRenderApi(false)
false
}
private fun findNextWorkingRenderApi(redraw: Boolean) { private fun findNextWorkingRenderApi() {
var thrown: Boolean var thrown: Boolean
do { do {
thrown = false thrown = false
...@@ -144,19 +144,23 @@ open class SkiaLayer( ...@@ -144,19 +144,23 @@ open class SkiaLayer(
renderApi = fallbackRenderApiQueue.removeAt(0) renderApi = fallbackRenderApiQueue.removeAt(0)
contextHandler?.dispose() contextHandler?.dispose()
redrawer?.dispose() redrawer?.dispose()
contextHandler = createContextHandler(this, renderApi) contextHandler = renderFactory.createContextHandler(this, renderApi)
redrawer = platformOperations.createRedrawer(this, renderApi, properties) redrawer = renderFactory.createRedrawer(this, renderApi, properties)
if (redraw) redrawer!!.redrawImmediately() redrawer?.syncSize()
} catch (e: Exception) { } catch (e: RenderException) {
println(e.message) println(e.message)
thrown = true thrown = true
} }
} while (thrown) } while (thrown && fallbackRenderApiQueue.isNotEmpty())
if (thrown && fallbackRenderApiQueue.isEmpty()) {
throw RenderException("Cannot fallback to any render API")
}
} }
open fun init() { protected open fun init() {
backedLayer.init() backedLayer.init()
findNextWorkingRenderApi(false) findNextWorkingRenderApi()
isInited = true isInited = true
} }
...@@ -196,6 +200,7 @@ open class SkiaLayer( ...@@ -196,6 +200,7 @@ open class SkiaLayer(
} }
super.setBounds(x, y, roundedWidth, roundedHeight) super.setBounds(x, y, roundedWidth, roundedHeight)
backedLayer.setSize(roundedWidth, roundedHeight) backedLayer.setSize(roundedWidth, roundedHeight)
redrawer?.syncSize()
} }
override fun paint(g: Graphics) { override fun paint(g: Graphics) {
...@@ -203,7 +208,7 @@ open class SkiaLayer( ...@@ -203,7 +208,7 @@ open class SkiaLayer(
if (backedLayer.checkContentScale()) { if (backedLayer.checkContentScale()) {
notifyChange(PropertyKind.ContentScale) notifyChange(PropertyKind.ContentScale)
} }
redrawer?.syncSize() redrawer?.syncSize() // setBounds not always called (for example when we change density on Linux
// `paint` can be called when we already inside `draw` method. // `paint` can be called when we already inside `draw` method.
// //
...@@ -211,12 +216,10 @@ open class SkiaLayer( ...@@ -211,12 +216,10 @@ open class SkiaLayer(
// such as `jframe.isEnabled = false` on Linux // such as `jframe.isEnabled = false` on Linux
// //
// To avoid recursive call of `draw` (we don't support recursive calls) we just schedule redrawing. // To avoid recursive call of `draw` (we don't support recursive calls) we just schedule redrawing.
withExceptionHandler(onExceptionAction) { if (isRendering) {
if (isRendering) { redrawer?.needRedraw()
redrawer?.needRedraw() } else {
} else { redrawer?.redrawImmediately()
redrawer?.redrawImmediately()
}
} }
} }
...@@ -339,22 +342,32 @@ open class SkiaLayer( ...@@ -339,22 +342,32 @@ open class SkiaLayer(
} }
} }
internal fun prepareDrawContext(): Boolean { internal inline fun inDrawScope(body: () -> Unit) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
contextHandler?.apply { try {
if (!initContext()) { body()
findNextWorkingRenderApi(true) } catch (e: CancellationException) {
return false // ignore
} } catch (e: RenderException) {
withExceptionHandler(onExceptionAction) { if (!isDisposed) {
initCanvas() println(e.message)
true findNextWorkingRenderApi()
redrawer?.redrawImmediately()
} }
} }
return true
} }
// can be called from non-swing thread
// throws exception if initialization of graphic context was not successful
internal fun draw() { internal fun draw() {
contextHandler?.apply {
if (!initContext()) {
throw RenderException("Cannot init graphic context")
}
initCanvas()
}
check(!isDisposed) { "SkiaLayer is disposed" } check(!isDisposed) { "SkiaLayer is disposed" }
contextHandler?.apply { contextHandler?.apply {
clearCanvas() clearCanvas()
......
...@@ -4,6 +4,7 @@ import org.jetbrains.skia.ColorSpace ...@@ -4,6 +4,7 @@ import org.jetbrains.skia.ColorSpace
import org.jetbrains.skia.Surface import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceColorFormat import org.jetbrains.skia.SurfaceColorFormat
import org.jetbrains.skia.SurfaceOrigin import org.jetbrains.skia.SurfaceOrigin
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.AngleRedrawer import org.jetbrains.skiko.redrawer.AngleRedrawer
...@@ -17,7 +18,7 @@ internal class AngleContextHandler(layer: SkiaLayer) : ContextHandler(layer) { ...@@ -17,7 +18,7 @@ internal class AngleContextHandler(layer: SkiaLayer) : ContextHandler(layer) {
if (context == null) { if (context == null) {
device = angleRedrawer.createDevice() device = angleRedrawer.createDevice()
if (device == 0L) { if (device == 0L) {
throw Exception("Failed to create Angle device.") throw RenderException("Failed to create Angle device.")
} }
context = angleRedrawer.makeContext() context = angleRedrawer.makeContext()
} }
......
...@@ -13,16 +13,6 @@ import org.jetbrains.skiko.hostFullName ...@@ -13,16 +13,6 @@ import org.jetbrains.skiko.hostFullName
import org.jetbrains.skiko.javaLocation import org.jetbrains.skiko.javaLocation
import org.jetbrains.skiko.javaVendor import org.jetbrains.skiko.javaVendor
internal fun createContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
return when (renderApi) {
GraphicsApi.SOFTWARE -> SoftwareContextHandler(layer)
GraphicsApi.OPENGL -> OpenGLContextHandler(layer)
GraphicsApi.DIRECT3D -> Direct3DContextHandler(layer)
GraphicsApi.METAL -> MetalContextHandler(layer)
else -> TODO("Unsupported yet.")
}
}
internal abstract class ContextHandler(val layer: SkiaLayer) { internal abstract class ContextHandler(val layer: SkiaLayer) {
open val bleachConstant = if (hostOs == OS.MacOS) 0 else -1 open val bleachConstant = if (hostOs == OS.MacOS) 0 else -1
var context: DirectContext? = null var context: DirectContext? = null
......
...@@ -15,7 +15,6 @@ internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer) { ...@@ -15,7 +15,6 @@ internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer) {
try { try {
if (context == null) { if (context == null) {
context = metalRedrawer.makeContext() context = metalRedrawer.makeContext()
metalRedrawer.syncSize()
if (System.getProperty("skiko.hardwareInfo.enabled") == "true") { if (System.getProperty("skiko.hardwareInfo.enabled") == "true") {
println(rendererInfo()) println(rendererInfo())
} }
......
...@@ -17,7 +17,7 @@ internal class Direct3DRedrawer( ...@@ -17,7 +17,7 @@ internal class Direct3DRedrawer(
private val device = createDirectXDevice(getAdapterPriority(), layer.contentHandle).also { private val device = createDirectXDevice(getAdapterPriority(), layer.contentHandle).also {
if (it == 0L || !isVideoCardSupported(layer.renderApi)) { if (it == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Failed to create DirectX12 device.") throw RenderException("Failed to create DirectX12 device.")
} }
} }
...@@ -42,7 +42,7 @@ internal class Direct3DRedrawer( ...@@ -42,7 +42,7 @@ internal class Direct3DRedrawer(
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "Direct3DRedrawer is disposed" } check(!isDisposed) { "Direct3DRedrawer is disposed" }
layer.update(System.nanoTime()) layer.update(System.nanoTime())
if (prepareDrawContext()) { layer.inDrawScope {
drawAndSwap(withVsync = false) drawAndSwap(withVsync = false)
} }
} }
...@@ -52,17 +52,13 @@ internal class Direct3DRedrawer( ...@@ -52,17 +52,13 @@ internal class Direct3DRedrawer(
} }
private suspend fun draw() { private suspend fun draw() {
if (prepareDrawContext()) { layer.inDrawScope {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
drawAndSwap(withVsync = properties.isVsyncEnabled) drawAndSwap(withVsync = properties.isVsyncEnabled)
} }
} }
} }
private fun prepareDrawContext() = synchronized(drawLock) {
layer.prepareDrawContext()
}
private fun drawAndSwap(withVsync: Boolean) = synchronized(drawLock) { private fun drawAndSwap(withVsync: Boolean) = synchronized(drawLock) {
if (!isDisposed) { if (!isDisposed) {
layer.draw() layer.draw()
......
...@@ -17,7 +17,7 @@ internal class LinuxOpenGLRedrawer( ...@@ -17,7 +17,7 @@ internal class LinuxOpenGLRedrawer(
context = it.createContext() context = it.createContext()
it.makeCurrent(context) it.makeCurrent(context)
if (context == 0L || !isVideoCardSupported(layer.renderApi)) { if (context == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Cannot create Linux GL context") throw RenderException("Cannot create Linux GL context")
} }
it.setSwapInterval(swapInterval) it.setSwapInterval(swapInterval)
} }
...@@ -75,9 +75,7 @@ internal class LinuxOpenGLRedrawer( ...@@ -75,9 +75,7 @@ internal class LinuxOpenGLRedrawer(
} }
private fun draw() { private fun draw() {
if (layer.prepareDrawContext()) { layer.inDrawScope(layer::draw)
layer.draw()
}
} }
companion object { companion object {
......
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.*
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
// Current implementation is fragile (it works in all tested cases, but we can't test everything)
//
// We should investigate can we implement our own CAOpenGLLayer, without its restrictions.
// (see, for example https://github.com/gnustep/libs-gui/blob/master/Source/NSOpenGLView.m)
//
// P.S. MacOsOpenGLRedrawer will not be used by default in the future, because we will support Metal.
internal class MacOsOpenGLRedrawer(
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
private val containerLayerPtr = layer.backedLayer.useDrawingSurfacePlatformInfo(::initContainer)
private val drawLock = Any()
private var isDisposed = false
private val drawLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = true) {
override fun draw() = synchronized(drawLock) {
if (!isDisposed) {
if (layer.prepareDrawContext()) {
layer.draw()
}
}
}
suspend fun display() = display(::setNeedsDisplay)
}
// use a separate layer for vsync, because with single layer we cannot asynchronously update layer
// `update` is suspend, and runBlocking(Dispatchers.Swing) causes dead lock with AppKit Thread.
// AWT has a method to avoid dead locks but it is internal (sun.lwawt.macosx.LWCToolkit.invokeAndWait)
private val vsyncLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = false) {
@Volatile
private var canDraw = false
init {
setFrame(0, 0, 1, 1) // if frame has zero size then it will be not drawn at all
}
override fun draw() {
// Clear layer with transparent color, so it will be not pink color.
val opengl = OpenGLApi.instance
opengl.glClearColor(0f, 0f, 0f, 0f)
opengl.glClear(opengl.GL_COLOR_BUFFER_BIT)
}
override fun canDraw(): Boolean {
val canDraw = canDraw
if (!canDraw) {
isAsynchronous = false // stop asynchronous mode so we don't waste CPU cycles
}
return canDraw
}
private fun requestAsyncDisplay() {
// Use asynchronous mode instead of just setNeedsDisplay,
// so Core Animation will wait for the next frame in vsync signal
//
// Asynchronous mode means that Core Animation will automatically
// call canDraw/draw every vsync signal (~16.7ms on 60Hz monitor)
//
// Similar is implemented in Chromium:
// https://chromium.googlesource.com/chromium/chromium/+/0489078bf98350b00876070cf2fdce230905f47e/content/browser/renderer_host/compositing_iosurface_layer_mac.mm#57
if (!isAsynchronous) {
isAsynchronous = true
setNeedsDisplay()
}
}
suspend fun sync() {
canDraw = true
display(::requestAsyncDisplay)
canDraw = false
}
}
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
synchronized(drawLock) {
layer.update(System.nanoTime())
}
if (properties.isVsyncEnabled) {
drawLayer.setNeedsDisplay()
vsyncLayer.sync()
} else {
error("Drawing without vsync isn't supported on macOs with OpenGL")
}
}
override fun dispose() = synchronized(drawLock) {
frameDispatcher.cancel()
vsyncLayer.dispose()
drawLayer.dispose()
isDisposed = true
}
override fun syncSize() {
val globalPosition = convertPoint(layer, layer.x, layer.y, getRootPane(layer))
setContentScale(containerLayerPtr, layer.contentScale)
setContentScale(drawLayer.ptr, layer.contentScale)
drawLayer.setFrame(
globalPosition.x,
globalPosition.y,
layer.width.coerceAtLeast(0),
layer.height.coerceAtLeast(0)
)
}
override fun needRedraw() {
frameDispatcher.scheduleFrame()
}
override fun redrawImmediately() {
layer.update(System.nanoTime())
// macOs will call 'draw' itself because of 'setNeedsDisplayOnBoundsChange=true'.
// But we schedule new frame after vsync anyway.
// Because 'redrawImmediately' can be called after 'draw',
// and we need at least one 'draw' after 'redrawImmediately'.
//
// We don't use setNeedsDisplay, because frequent calls of it are unreliable.
// 'setNeedsDisplayOnBoundsChange=true' with combination of 'scheduleFrame' is enough
// to not see the white bars on resize.
frameDispatcher.scheduleFrame()
}
}
private abstract class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnBoundsChange: Boolean) {
@Suppress("LeakingThis")
val ptr = initAWTGLLayer(containerPtr, this, setNeedsDisplayOnBoundsChange)
private val display = Task()
fun setFrame(x: Int, y: Int, width: Int, height: Int) {
setFrame(containerPtr, ptr, x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat())
}
// Called in AWT Thread
open fun dispose() = disposeAWTGLLayer(ptr)
var isAsynchronous: Boolean
get() = isAsynchronous(ptr)
set(value) = setAsynchronous(ptr, value)
/**
* Schedule next [draw] as soon as possible (not waiting for vsync)
*
* WARNING!!!
*
* CAOpenGLLayer will not call [draw] if we call [setNeedsDisplay] too often.
*
* Experimentally we found out that after 15 draw's between two vsync's (900 FPS on 60 Hz display) will cause
* setNeedsDisplay to not schedule the next draw at all.
*
* Only after the next vsync, [setNeedsDisplay] will be working again.
*/
fun setNeedsDisplay() {
setNeedsDisplayOnMainThread(ptr)
}
protected suspend fun display(
startDisplay: () -> Unit
) = display.runAndAwait {
startDisplay()
}
// Called in AppKit Thread
protected open fun canDraw() = true
// Called in AppKit Thread
@Suppress("unused") // called from native code
private fun performDraw() {
try {
draw()
} catch (e: Throwable) {
e.printStackTrace()
}
display.finish()
}
// Called in AppKit Thread
protected abstract fun draw()
private external fun isAsynchronous(ptr: Long): Boolean
private external fun setAsynchronous(ptr: Long, isAsynchronous: Boolean)
private external fun setNeedsDisplayOnMainThread(nativePtr: Long)
protected external fun setFrame(containerPtr: Long, ptr: Long, x: Float, y: Float, width: Float, height: Float)
}
private external fun initContainer(platformInfo: Long): Long
private external fun setContentScale(layerNativePtr: Long, contentScale: Float)
private external fun initAWTGLLayer(containerPtr: Long, layer: AWTGLLayer, setNeedsDisplayOnBoundsChange: Boolean): Long
private external fun disposeAWTGLLayer(ptr: Long)
\ No newline at end of file
...@@ -7,8 +7,7 @@ import kotlinx.coroutines.withContext ...@@ -7,8 +7,7 @@ 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.*
import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.*
import javax.swing.SwingUtilities.getRootPane
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
...@@ -52,7 +51,7 @@ internal class MetalRedrawer( ...@@ -52,7 +51,7 @@ internal class MetalRedrawer(
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
setVSyncEnabled(device, enabled = false) setVSyncEnabled(device, enabled = false)
update(System.nanoTime()) update(System.nanoTime())
performDraw() layer.inDrawScope(::performDraw)
setVSyncEnabled(device, properties.isVsyncEnabled) setVSyncEnabled(device, properties.isVsyncEnabled)
} }
...@@ -72,10 +71,15 @@ internal class MetalRedrawer( ...@@ -72,10 +71,15 @@ internal class MetalRedrawer(
// //
// Executors.newSingleThreadExecutor().asCoroutineDispatcher(): 50 FPS, 150% CPU // Executors.newSingleThreadExecutor().asCoroutineDispatcher(): 50 FPS, 150% CPU
// Dispatchers.IO: 50 FPS, 200% CPU // Dispatchers.IO: 50 FPS, 200% CPU
withContext(Dispatchers.IO) { layer.inDrawScope {
val handle = startRendering() withContext(Dispatchers.IO) {
performDraw() val handle = startRendering()
endRendering(handle) try {
performDraw()
} finally {
endRendering(handle)
}
}
} }
// When window is not visible - it doesn't make sense to redraw fast to avoid battery drain. // When window is not visible - it doesn't make sense to redraw fast to avoid battery drain.
// In theory, we could be more precise, and just suspend rendering in // In theory, we could be more precise, and just suspend rendering in
...@@ -86,13 +90,12 @@ internal class MetalRedrawer( ...@@ -86,13 +90,12 @@ internal class MetalRedrawer(
private fun performDraw() = synchronized(drawLock) { private fun performDraw() = synchronized(drawLock) {
if (!isDisposed) { if (!isDisposed) {
if (layer.prepareDrawContext()) { layer.draw()
layer.draw()
}
} }
} }
override fun syncSize() { override fun syncSize() = synchronized(drawLock) {
check(isEventDispatchThread()) { "Method should be called from AWT event dispatch thread" }
val rootPane = getRootPane(layer) val rootPane = getRootPane(layer)
val globalPosition = convertPoint(layer, layer.x, layer.y, rootPane) val globalPosition = convertPoint(layer, layer.x, layer.y, rootPane)
setContentScale(device, layer.contentScale) setContentScale(device, layer.contentScale)
......
...@@ -21,9 +21,7 @@ internal class SoftwareRedrawer( ...@@ -21,9 +21,7 @@ internal class SoftwareRedrawer(
if (layer.isShowing) { if (layer.isShowing) {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
if (layer.prepareDrawContext()) { layer.inDrawScope(layer::draw)
layer.draw()
}
} }
} }
...@@ -40,8 +38,6 @@ internal class SoftwareRedrawer( ...@@ -40,8 +38,6 @@ internal class SoftwareRedrawer(
override fun redrawImmediately() { override fun redrawImmediately() {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
if (layer.prepareDrawContext()) { layer.inDrawScope(layer::draw)
layer.draw()
}
} }
} }
\ No newline at end of file
...@@ -14,7 +14,7 @@ internal class WindowsOpenGLRedrawer( ...@@ -14,7 +14,7 @@ internal class WindowsOpenGLRedrawer(
private val context = createContext(device).also { private val context = createContext(device).also {
makeCurrent(device, it) makeCurrent(device, it)
if (it == 0L || !isVideoCardSupported(layer.renderApi)) { if (it == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Cannot create Windows GL context") throw RenderException("Cannot create Windows GL context")
} }
} }
private var isDisposed = false private var isDisposed = false
...@@ -54,9 +54,7 @@ internal class WindowsOpenGLRedrawer( ...@@ -54,9 +54,7 @@ internal class WindowsOpenGLRedrawer(
} }
private fun draw() { private fun draw() {
if (layer.prepareDrawContext()) { layer.inDrawScope(layer::draw)
layer.draw()
}
} }
private fun makeCurrent() = makeCurrent(device, context) private fun makeCurrent() = makeCurrent(device, context)
......
...@@ -227,7 +227,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_resizeLay ...@@ -227,7 +227,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_resizeLay
[CATransaction begin]; [CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
device.layer.frame = frame; device.layer.frame = frame;
device.layer.drawableSize = drawableSize; // to avoid warning in console:
// CAMetalLayer ignoring invalid setDrawableSize width=0.000000 height=0.000000
if (width > 0 && height > 0) {
device.layer.drawableSize = drawableSize;
}
[CATransaction commit]; [CATransaction commit];
[CATransaction flush]; [CATransaction flush];
} }
......
...@@ -134,7 +134,7 @@ native crash in SkiaWindowTest "render single window" ...@@ -134,7 +134,7 @@ native crash in SkiaWindowTest "render single window"
@Test @Test
fun `FPS is near display refresh rate (multiple windows)`() = swingTest { fun `FPS is near display refresh rate (multiple windows)`() = swingTest {
val windows = (1..3).map { index -> val windows = (1..3).map { index ->
TestWindow(width = 40, height = 20, frameCount = 300, deviatedTerminalCount = 10).apply { TestWindow(width = 40, height = 20, frameCount = 300, deviatedTerminalCount = 20).apply {
toFront() toFront()
location = Point((index + 1) * 200, 200) location = Point((index + 1) * 200, 200)
} }
......
...@@ -2,6 +2,7 @@ package org.jetbrains.skiko ...@@ -2,6 +2,7 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import org.jetbrains.skia.Canvas import org.jetbrains.skia.Canvas
import org.jetbrains.skia.FontMgr import org.jetbrains.skia.FontMgr
import org.jetbrains.skia.Paint import org.jetbrains.skia.Paint
...@@ -10,8 +11,11 @@ import org.jetbrains.skia.paragraph.FontCollection ...@@ -10,8 +11,11 @@ import org.jetbrains.skia.paragraph.FontCollection
import org.jetbrains.skia.paragraph.ParagraphBuilder import org.jetbrains.skia.paragraph.ParagraphBuilder
import org.jetbrains.skia.paragraph.ParagraphStyle import org.jetbrains.skia.paragraph.ParagraphStyle
import org.jetbrains.skia.paragraph.TextStyle import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.util.ScreenshotTestRule import org.jetbrains.skiko.util.ScreenshotTestRule
import org.jetbrains.skiko.util.swingTest import org.jetbrains.skiko.util.swingTest
import org.junit.Assert.assertEquals
import org.junit.Assume.assumeTrue import org.junit.Assume.assumeTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
...@@ -21,7 +25,6 @@ import java.awt.event.WindowEvent ...@@ -21,7 +25,6 @@ import java.awt.event.WindowEvent
import javax.swing.JFrame import javax.swing.JFrame
import javax.swing.WindowConstants import javax.swing.WindowConstants
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue") @Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
...@@ -185,14 +188,14 @@ class SkiaWindowTest { ...@@ -185,14 +188,14 @@ class SkiaWindowTest {
} }
} }
@Test @Test(timeout = 60000)
fun `open windows stress test`() = swingTest { fun `stress test - open multiple windows`() = swingTest {
fun window(isAnimated: Boolean) = SkiaWindow().apply { fun window(isAnimated: Boolean) = SkiaWindow().apply {
setLocation(200,200) setLocation(200,200)
setSize(40, 20) setSize(40, 20)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = if (isAnimated) { layer.renderer = if (isAnimated) {
AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 2.0) AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 20.0)
} else { } else {
RectRenderer(layer, 20, 10, Color.RED) RectRenderer(layer, 20, 10, Color.RED)
} }
...@@ -227,10 +230,185 @@ class SkiaWindowTest { ...@@ -227,10 +230,185 @@ class SkiaWindowTest {
delay(5000) delay(5000)
} }
@Test(timeout = 60000)
fun `stress test - resize and paint immediately`() = swingTest {
fun openWindow() = SkiaWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false)
).apply {
setLocation(200,200)
setSize(400, 200)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 20.0)
isVisible = true
}
val window = openWindow()
repeat(100) {
window.size = Dimension(200 + Random.nextInt(200), 200 + Random.nextInt(200))
window.paint(window.graphics)
yield()
}
window.close()
}
@Test(timeout = 60000)
fun `stress test - open and paint immediately`() = swingTest {
fun openWindow() = SkiaWindow(
properties = SkiaLayerProperties(isVsyncEnabled = false)
).apply {
setLocation(200,200)
setSize(400, 200)
preferredSize = Dimension(400, 200)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = object : SkiaRenderer {
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
}
}
}
repeat(30) {
delay(100)
val window = openWindow()
window.isVisible = true
window.layer.needRedraw()
yield()
window.paint(window.graphics)
window.close()
}
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on init context`() = swingTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createContextHandler(
layer: SkiaLayer,
renderApi: GraphicsApi
) = object : ContextHandler(layer) {
override fun initContext() = false
override fun initCanvas() = Unit
}
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer = object : Redrawer {
override fun dispose() = Unit
override fun needRedraw() = Unit
override fun redrawImmediately() = layer.inDrawScope { layer.draw() }
}
}
)
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on create redrawer`() = swingTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createContextHandler(
layer: SkiaLayer,
renderApi: GraphicsApi
) = object : ContextHandler(layer) {
override fun initContext() = true
override fun initCanvas() = Unit
}
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = throw RenderException()
}
)
}
@Test(timeout = 60000)
fun `fallback to software renderer, fail on draw`() = swingTest {
testFallbackToSoftware(
object : RenderFactory {
override fun createContextHandler(
layer: SkiaLayer,
renderApi: GraphicsApi
) = object : ContextHandler(layer) {
override fun initContext() = true
override fun initCanvas() = Unit
}
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
) = object : Redrawer {
override fun dispose() = Unit
override fun needRedraw() = Unit
override fun redrawImmediately() = layer.inDrawScope {
throw RenderException()
}
}
}
)
}
private suspend fun testFallbackToSoftware(nonSoftwareRenderFactory: RenderFactory) {
val window = SkiaWindow(
layerFactory = {
SkiaLayer(
renderFactory = OverrideNonSoftwareRenderFactory(nonSoftwareRenderFactory)
)
}
)
try {
window.setLocation(200, 200)
window.setSize(400, 200)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
val renderer = RectRenderer(window.layer, 200, 100, Color.RED)
window.layer.renderer = renderer
window.isUndecorated = true
window.isVisible = true
delay(1000)
screenshots.assert(window.bounds, "frame1", "testFallbackToSoftware")
renderer.rectWidth = 100
window.layer.needRedraw()
delay(1000)
screenshots.assert(window.bounds, "frame2", "testFallbackToSoftware")
assertEquals(GraphicsApi.SOFTWARE, window.layer.renderApi)
} finally {
window.close()
}
}
private class OverrideNonSoftwareRenderFactory(
private val nonSoftwareRenderFactory: RenderFactory
) : RenderFactory {
override fun createContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
return if (renderApi == GraphicsApi.SOFTWARE) {
RenderFactory.Default.createContextHandler(layer, renderApi)
} else {
nonSoftwareRenderFactory.createContextHandler(layer, renderApi)
}
}
override fun createRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi,
properties: SkiaLayerProperties
): Redrawer {
return if (renderApi == GraphicsApi.SOFTWARE) {
RenderFactory.Default.createRedrawer(layer, renderApi, properties)
} else {
nonSoftwareRenderFactory.createRedrawer(layer, renderApi, properties)
}
}
}
@Test(timeout = 20000) @Test(timeout = 20000)
fun `render continuously empty content without vsync`() = swingTest { fun `render continuously empty content without vsync`() = swingTest {
assumeTrue(hostOs != OS.MacOS) // TODO remove when we will support drawing without vsync on macOs
val targetDrawCount = 500 val targetDrawCount = 500
var drawCount = 0 var drawCount = 0
val onDrawCompleted = CompletableDeferred<Unit>() val onDrawCompleted = CompletableDeferred<Unit>()
......
...@@ -20,8 +20,7 @@ class ScreenshotTestRule : TestRule { ...@@ -20,8 +20,7 @@ class ScreenshotTestRule : TestRule {
private val robot by lazy { Robot() } private val robot by lazy { Robot() }
private lateinit var testIdentifier: String private lateinit var testIdentifier: String
private val subDir = if (hostOs == OS.MacOS) "macos" else "windows_linux" private val screenshotsDir = File(System.getProperty("skiko.test.screenshots.dir")!!)
private val screenshotsDir = File(System.getProperty("skiko.test.screenshots.dir")!!).resolve(subDir)
override fun apply(base: Statement, description: Description): Statement { override fun apply(base: Statement, description: Description): Statement {
return object : Statement() { return object : Statement() {
...@@ -41,12 +40,20 @@ class ScreenshotTestRule : TestRule { ...@@ -41,12 +40,20 @@ class ScreenshotTestRule : TestRule {
} }
} }
fun assert(rectangle: Rectangle, id: String = "") { fun assert(
rectangle: Rectangle,
id: String = "",
testIdentifier: String = this.testIdentifier
) {
val actual = robot.createScreenCapture(rectangle) val actual = robot.createScreenCapture(rectangle)
assert(actual.toImage(), id) assert(actual.toImage(), id, testIdentifier)
} }
fun assert(actual: Image, id: String = "") { fun assert(
actual: Image,
id: String = "",
testIdentifier: String = this.testIdentifier
) {
val name = if (id.isNotEmpty()) "${testIdentifier}_$id" else testIdentifier val name = if (id.isNotEmpty()) "${testIdentifier}_$id" else testIdentifier
val actualFile = File(screenshotsDir, "${name}_actual.png") val actualFile = File(screenshotsDir, "${name}_actual.png")
val expectedFile = File(screenshotsDir, "$name.png") val expectedFile = File(screenshotsDir, "$name.png")
......
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