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

Fix metal deadlock (#217)

parent 3923076d
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.Window
import javax.swing.SwingUtilities
......@@ -17,7 +10,6 @@ internal interface PlatformOperations {
fun disableTitleBar(platformInfo: Long)
fun orderEmojiAndSymbolsPopup()
fun getDpiScale(component: Component): Float
fun createRedrawer(layer: SkiaLayer, renderApi: GraphicsApi, properties: SkiaLayerProperties): Redrawer
}
internal val platformOperations: PlatformOperations by lazy {
......@@ -42,16 +34,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun orderEmojiAndSymbolsPopup() {
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 -> {
object: PlatformOperations {
......@@ -76,16 +58,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float {
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 -> {
......@@ -111,15 +83,6 @@ internal val platformOperations: PlatformOperations by lazy {
override fun getDpiScale(component: Component): Float {
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
import java.io.File
import java.lang.RuntimeException
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.jvm.JvmStatic
import org.jetbrains.skiko.hostFullName
internal class RenderException(
message: String? = null,
cause: Exception? = null
) : RuntimeException(message, cause)
internal class RenderExceptionsHandler {
companion object {
......@@ -16,7 +21,7 @@ internal class RenderExceptionsHandler {
"${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") {
writeLog(exception)
}
......@@ -40,16 +45,6 @@ internal class RenderExceptionsHandler {
}
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
import org.jetbrains.skia.*
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.createContextHandler
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.withExceptionHandler
import java.awt.Graphics
import java.awt.event.*
import java.awt.im.InputMethodRequests
import java.util.concurrent.CancellationException
import javax.swing.JPanel
import javax.swing.SwingUtilities.isEventDispatchThread
......@@ -17,8 +16,9 @@ interface SkiaRenderer {
private class PictureHolder(val instance: Picture, val width: Int, val height: Int)
open class SkiaLayer(
private val properties: SkiaLayerProperties = SkiaLayerProperties()
open class SkiaLayer internal constructor(
private val properties: SkiaLayerProperties = SkiaLayerProperties(),
private val renderFactory: RenderFactory
) : JPanel() {
companion object {
init {
......@@ -33,6 +33,10 @@ open class SkiaLayer(
internal val backedLayer: HardwareLayer
internal constructor(
properties: SkiaLayerProperties = SkiaLayerProperties()
) : this(properties, RenderFactory.Default)
val canvas: java.awt.Canvas
get() = backedLayer
......@@ -131,12 +135,8 @@ open class SkiaLayer(
private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder()
private val pictureLock = Any()
private val onExceptionAction: () -> Boolean = {
findNextWorkingRenderApi(false)
false
}
private fun findNextWorkingRenderApi(redraw: Boolean) {
private fun findNextWorkingRenderApi() {
var thrown: Boolean
do {
thrown = false
......@@ -144,19 +144,23 @@ open class SkiaLayer(
renderApi = fallbackRenderApiQueue.removeAt(0)
contextHandler?.dispose()
redrawer?.dispose()
contextHandler = createContextHandler(this, renderApi)
redrawer = platformOperations.createRedrawer(this, renderApi, properties)
if (redraw) redrawer!!.redrawImmediately()
} catch (e: Exception) {
contextHandler = renderFactory.createContextHandler(this, renderApi)
redrawer = renderFactory.createRedrawer(this, renderApi, properties)
redrawer?.syncSize()
} catch (e: RenderException) {
println(e.message)
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()
findNextWorkingRenderApi(false)
findNextWorkingRenderApi()
isInited = true
}
......@@ -196,6 +200,7 @@ open class SkiaLayer(
}
super.setBounds(x, y, roundedWidth, roundedHeight)
backedLayer.setSize(roundedWidth, roundedHeight)
redrawer?.syncSize()
}
override fun paint(g: Graphics) {
......@@ -203,7 +208,7 @@ open class SkiaLayer(
if (backedLayer.checkContentScale()) {
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.
//
......@@ -211,14 +216,12 @@ open class SkiaLayer(
// such as `jframe.isEnabled = false` on Linux
//
// To avoid recursive call of `draw` (we don't support recursive calls) we just schedule redrawing.
withExceptionHandler(onExceptionAction) {
if (isRendering) {
redrawer?.needRedraw()
} else {
redrawer?.redrawImmediately()
}
}
}
// We need to delegate all event listeners to the Canvas (so and focus/input)
// Canvas is heavyweight AWT component, JPanel is lightweight Swing component
......@@ -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" }
try {
body()
} catch (e: CancellationException) {
// ignore
} catch (e: RenderException) {
if (!isDisposed) {
println(e.message)
findNextWorkingRenderApi()
redrawer?.redrawImmediately()
}
}
}
// can be called from non-swing thread
// throws exception if initialization of graphic context was not successful
internal fun draw() {
contextHandler?.apply {
if (!initContext()) {
findNextWorkingRenderApi(true)
return false
throw RenderException("Cannot init graphic context")
}
withExceptionHandler(onExceptionAction) {
initCanvas()
true
}
}
return true
}
internal fun draw() {
check(!isDisposed) { "SkiaLayer is disposed" }
contextHandler?.apply {
clearCanvas()
......
......@@ -4,6 +4,7 @@ import org.jetbrains.skia.ColorSpace
import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceColorFormat
import org.jetbrains.skia.SurfaceOrigin
import org.jetbrains.skiko.RenderException
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.AngleRedrawer
......@@ -17,7 +18,7 @@ internal class AngleContextHandler(layer: SkiaLayer) : ContextHandler(layer) {
if (context == null) {
device = angleRedrawer.createDevice()
if (device == 0L) {
throw Exception("Failed to create Angle device.")
throw RenderException("Failed to create Angle device.")
}
context = angleRedrawer.makeContext()
}
......
......@@ -13,16 +13,6 @@ import org.jetbrains.skiko.hostFullName
import org.jetbrains.skiko.javaLocation
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) {
open val bleachConstant = if (hostOs == OS.MacOS) 0 else -1
var context: DirectContext? = null
......
......@@ -15,7 +15,6 @@ internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer) {
try {
if (context == null) {
context = metalRedrawer.makeContext()
metalRedrawer.syncSize()
if (System.getProperty("skiko.hardwareInfo.enabled") == "true") {
println(rendererInfo())
}
......
......@@ -17,7 +17,7 @@ internal class Direct3DRedrawer(
private val device = createDirectXDevice(getAdapterPriority(), layer.contentHandle).also {
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(
override fun redrawImmediately() {
check(!isDisposed) { "Direct3DRedrawer is disposed" }
layer.update(System.nanoTime())
if (prepareDrawContext()) {
layer.inDrawScope {
drawAndSwap(withVsync = false)
}
}
......@@ -52,17 +52,13 @@ internal class Direct3DRedrawer(
}
private suspend fun draw() {
if (prepareDrawContext()) {
layer.inDrawScope {
withContext(Dispatchers.IO) {
drawAndSwap(withVsync = properties.isVsyncEnabled)
}
}
}
private fun prepareDrawContext() = synchronized(drawLock) {
layer.prepareDrawContext()
}
private fun drawAndSwap(withVsync: Boolean) = synchronized(drawLock) {
if (!isDisposed) {
layer.draw()
......
......@@ -17,7 +17,7 @@ internal class LinuxOpenGLRedrawer(
context = it.createContext()
it.makeCurrent(context)
if (context == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Cannot create Linux GL context")
throw RenderException("Cannot create Linux GL context")
}
it.setSwapInterval(swapInterval)
}
......@@ -75,9 +75,7 @@ internal class LinuxOpenGLRedrawer(
}
private fun draw() {
if (layer.prepareDrawContext()) {
layer.draw()
}
layer.inDrawScope(layer::draw)
}
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
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.*
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
import javax.swing.SwingUtilities.*
internal class MetalRedrawer(
private val layer: SkiaLayer,
......@@ -52,7 +51,7 @@ internal class MetalRedrawer(
check(!isDisposed) { "MetalRedrawer is disposed" }
setVSyncEnabled(device, enabled = false)
update(System.nanoTime())
performDraw()
layer.inDrawScope(::performDraw)
setVSyncEnabled(device, properties.isVsyncEnabled)
}
......@@ -72,11 +71,16 @@ internal class MetalRedrawer(
//
// Executors.newSingleThreadExecutor().asCoroutineDispatcher(): 50 FPS, 150% CPU
// Dispatchers.IO: 50 FPS, 200% CPU
layer.inDrawScope {
withContext(Dispatchers.IO) {
val handle = startRendering()
try {
performDraw()
} finally {
endRendering(handle)
}
}
}
// 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
// `NSWindowDidChangeOcclusionStateNotification`, but current approach seems to work as well in practise.
......@@ -86,13 +90,12 @@ internal class MetalRedrawer(
private fun performDraw() = synchronized(drawLock) {
if (!isDisposed) {
if (layer.prepareDrawContext()) {
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 globalPosition = convertPoint(layer, layer.x, layer.y, rootPane)
setContentScale(device, layer.contentScale)
......
......@@ -21,9 +21,7 @@ internal class SoftwareRedrawer(
if (layer.isShowing) {
layer.update(System.nanoTime())
if (layer.prepareDrawContext()) {
layer.draw()
}
layer.inDrawScope(layer::draw)
}
}
......@@ -40,8 +38,6 @@ internal class SoftwareRedrawer(
override fun redrawImmediately() {
layer.update(System.nanoTime())
if (layer.prepareDrawContext()) {
layer.draw()
}
layer.inDrawScope(layer::draw)
}
}
\ No newline at end of file
......@@ -14,7 +14,7 @@ internal class WindowsOpenGLRedrawer(
private val context = createContext(device).also {
makeCurrent(device, it)
if (it == 0L || !isVideoCardSupported(layer.renderApi)) {
throw IllegalArgumentException("Cannot create Windows GL context")
throw RenderException("Cannot create Windows GL context")
}
}
private var isDisposed = false
......@@ -54,9 +54,7 @@ internal class WindowsOpenGLRedrawer(
}
private fun draw() {
if (layer.prepareDrawContext()) {
layer.draw()
}
layer.inDrawScope(layer::draw)
}
private fun makeCurrent() = makeCurrent(device, context)
......
......@@ -227,7 +227,11 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_resizeLay
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
device.layer.frame = frame;
// 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 flush];
}
......
......@@ -134,7 +134,7 @@ native crash in SkiaWindowTest "render single window"
@Test
fun `FPS is near display refresh rate (multiple windows)`() = swingTest {
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()
location = Point((index + 1) * 200, 200)
}
......
......@@ -2,6 +2,7 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.FontMgr
import org.jetbrains.skia.Paint
......@@ -10,8 +11,11 @@ import org.jetbrains.skia.paragraph.FontCollection
import org.jetbrains.skia.paragraph.ParagraphBuilder
import org.jetbrains.skia.paragraph.ParagraphStyle
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.swingTest
import org.junit.Assert.assertEquals
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
......@@ -21,7 +25,6 @@ import java.awt.event.WindowEvent
import javax.swing.JFrame
import javax.swing.WindowConstants
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@Suppress("BlockingMethodInNonBlockingContext", "SameParameterValue")
......@@ -185,14 +188,14 @@ class SkiaWindowTest {
}
}
@Test
fun `open windows stress test`() = swingTest {
@Test(timeout = 60000)
fun `stress test - open multiple windows`() = swingTest {
fun window(isAnimated: Boolean) = SkiaWindow().apply {
setLocation(200,200)
setSize(40, 20)
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
layer.renderer = if (isAnimated) {
AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 2.0)
AnimatedBoxRenderer(layer, pixelsPerSecond = 20.0, size = 20.0)
} else {
RectRenderer(layer, 20, 10, Color.RED)
}
......@@ -227,10 +230,185 @@ class SkiaWindowTest {
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)
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
var drawCount = 0
val onDrawCompleted = CompletableDeferred<Unit>()
......
......@@ -20,8 +20,7 @@ class ScreenshotTestRule : TestRule {
private val robot by lazy { Robot() }
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")!!).resolve(subDir)
private val screenshotsDir = File(System.getProperty("skiko.test.screenshots.dir")!!)
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
......@@ -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)
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 actualFile = File(screenshotsDir, "${name}_actual.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