Unverified Commit 57f3f868 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Replace async awaitRedraw by sync window.paint(window.graphics), refactor...

Replace async awaitRedraw by sync window.paint(window.graphics), refactor Direct 3D and Metal redrawers (#146)

* Refactor Direct3DContextHandle - move `device` to Direct3DRedrawer.kt

* Refactor finishFrame: split into Direct3DContextHandler.flush and Direct3DRedrawer.swap

* DirectX: redraw immediately

* Metal: redraw immediately

* Remove awaitRedraw

* Fix screenshot tests on macOs

* Discussions
parent 4804106d
......@@ -25,7 +25,7 @@ fun main() {
}
}
fun createWindow(title: String, exitOnClose: Boolean) = runBlocking(Dispatchers.Swing) {
fun createWindow(title: String, exitOnClose: Boolean) = SwingUtilities.invokeLater {
var mouseX = 0
var mouseY = 0
......@@ -113,7 +113,7 @@ fun createWindow(title: String, exitOnClose: Boolean) = runBlocking(Dispatchers.
// MANDATORY: set window preferred size before calling pack()
window.preferredSize = Dimension(800, 600)
window.pack()
window.layer.awaitRedraw()
window.layer.paint(window.graphics)
window.isVisible = true
}
......
#ifdef SK_DIRECT3D
#include <stdexcept>
#include <locale>
#include <Windows.h>
#include <jawt_md.h>
#include "jni_helpers.h"
#include "GrBackendSurface.h"
#include "GrDirectContext.h"
#include "SkSurface.h"
extern "C"
{
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_context_Direct3DContextHandler_flush(
JNIEnv *env, jobject redrawer, jlong contextPtr, jlong surfacePtr)
{
SkSurface *surface = fromJavaPointer<SkSurface *>(surfacePtr);
GrDirectContext *context = fromJavaPointer<GrDirectContext *>(contextPtr);
surface->flushAndSubmit(true);
surface->flush(SkSurface::BackendSurfaceAccess::kPresent, GrFlushInfo());
context->flush({});
context->submit(true);
}
}
#endif
......@@ -418,17 +418,10 @@ extern "C"
GR_D3D_CALL_ERRCHECK(d3dDevice->swapChain->ResizeBuffers(BuffersCount, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_finishFrame(
JNIEnv *env, jobject redrawer, jlong devicePtr, jlong contextPtr, jlong surfacePtr, jboolean isVsyncEnabled)
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_swap(
JNIEnv *env, jobject redrawer, jlong devicePtr, jboolean isVsyncEnabled)
{
DirectXDevice *d3dDevice = fromJavaPointer<DirectXDevice *>(devicePtr);
SkSurface *surface = fromJavaPointer<SkSurface *>(surfacePtr);
GrDirectContext *context = fromJavaPointer<GrDirectContext *>(contextPtr);
surface->flushAndSubmit(true);
surface->flush(SkSurface::BackendSurfaceAccess::kPresent, GrFlushInfo());
context->flush({});
context->submit(true);
// 1 value in [Present(1, 0)] enables vblank wait so this is how vertical sync works in DirectX.
const UINT64 fenceValue = d3dDevice->fenceValues[d3dDevice->bufferIndex];
......
......@@ -3,11 +3,8 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
/**
* Dispatch frame after call of [scheduleFrame].
......@@ -34,7 +31,6 @@ class FrameDispatcher(
frameChannel.receive()
frameScheduled = false
onFrame()
resumeFrameAwaiters(isActive = true)
// As per `yield()` documentation:
//
// For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and
......@@ -45,12 +41,6 @@ class FrameDispatcher(
}
}
init {
job.invokeOnCompletion {
resumeFrameAwaiters(isActive = false)
}
}
fun cancel() {
job.cancel()
}
......@@ -69,35 +59,4 @@ class FrameDispatcher(
frameChannel.offer(Unit)
}
}
private val frameAwaiters = mutableListOf<Continuation<Boolean>>()
/**
* Schedule next frame to render in the frame loop, and wait it to finish.
*
* If frame loop was completed (cancelled or there was an exception inside it) then don't wait and immediately continue execution.
*
* @return true if frame loop is active, false if it was completed.
*/
suspend fun awaitFrame(): Boolean {
return if (job.isActive) {
suspendCancellableCoroutine { continuation ->
synchronized(frameAwaiters) {
frameAwaiters.add(continuation)
}
scheduleFrame()
}
} else {
false
}
}
private fun resumeFrameAwaiters(isActive: Boolean) {
synchronized(frameAwaiters) {
for (frameAwaiter in frameAwaiters) {
frameAwaiter.resume(isActive)
}
frameAwaiters.clear()
}
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skia.*
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.createContextHandler
......@@ -67,8 +63,7 @@ open class SkiaLayer(
}
}
private val onInit = CompletableDeferred<Unit>()
private val isInited get() = onInit.isCompleted
private var isInited = false
private var isRendering = false
private fun checkInit() {
......@@ -138,7 +133,7 @@ open class SkiaLayer(
open fun init() {
backedLayer.init()
findNextWorkingRenderApi(false)
onInit.complete(Unit)
isInited = true
}
private val stateHandlers =
......@@ -281,20 +276,6 @@ open class SkiaLayer(
redrawer?.needRedraw()
}
/**
* Redraw on the next animation Frame (on vsync signal if vsync is enabled),
* and wait the frame to finish.
*
* @return true if frame was rendered, false if rendering loop was completed (cancelled or there was an exception inside it)
*/
suspend fun awaitRedraw(): Boolean {
return withContext(Dispatchers.Swing) {
check(!isDisposed) { "SkiaLayer is disposed" }
onInit.await()
redrawer?.awaitRedraw() != false
}
}
@Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this)
......
......@@ -10,18 +10,13 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
private val bufferCount = 2
private var surfaces: Array<Surface?> = arrayOfNulls(bufferCount)
val directXRedrawer: Direct3DRedrawer
private val directXRedrawer: Direct3DRedrawer
get() = layer.redrawer!! as Direct3DRedrawer
var device: Long = 0
override fun initContext(): Boolean {
try {
if (context == null) {
device = directXRedrawer.createDevice()
if (device == 0L) {
throw Exception("Failed to create DirectX12 device.")
}
context = directXRedrawer.makeContext(device)
context = directXRedrawer.makeContext()
if (System.getProperty("skiko.hardwareInfo.enabled") == "true") {
println(rendererInfo())
}
......@@ -55,14 +50,14 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
context?.flush()
if (!isD3DInited) {
directXRedrawer.initSwapChain(device)
directXRedrawer.initSwapChain()
} else {
directXRedrawer.resizeBuffers(device, w, h)
directXRedrawer.resizeBuffers(w, h)
}
try {
for (bufferIndex in 0..bufferCount - 1) {
surfaces[bufferIndex] = directXRedrawer.makeSurface(device, Native.getPtr(context!!), w, h, bufferIndex)
surfaces[bufferIndex] = directXRedrawer.makeSurface(Native.getPtr(context!!), w, h, bufferIndex)
}
} finally {
Reference.reachabilityFence(context!!)
......@@ -70,17 +65,16 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
if (!isD3DInited) {
isD3DInited = true
directXRedrawer.initFence(device)
directXRedrawer.initFence()
}
}
surface = surfaces[directXRedrawer.getBufferIndex(device)]
surface = surfaces[directXRedrawer.getBufferIndex()]
canvas = surface!!.canvas
}
override fun flush() {
try {
directXRedrawer.finishFrame(
device,
flush(
Native.getPtr(context!!),
Native.getPtr(surface!!)
)
......@@ -91,19 +85,20 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
}
override fun destroyContext() {
directXRedrawer.disposeDevice(device)
context?.close()
}
override fun disposeCanvas() {
for (bufferIndex in 0..bufferCount - 1) {
for (bufferIndex in 0 until bufferCount) {
surfaces[bufferIndex]?.close()
}
}
override fun rendererInfo(): String {
return super.rendererInfo() +
"Video card: ${directXRedrawer.getAdapterName(device)}\n" +
"Total VRAM: ${directXRedrawer.getAdapterMemorySize(device) / 1024 / 1024} MB\n"
"Video card: ${directXRedrawer.adapterName}\n" +
"Total VRAM: ${directXRedrawer.adapterMemorySize / 1024 / 1024} MB\n"
}
private external fun flush(context: Long, surface: Long)
}
......@@ -32,10 +32,6 @@ internal class AngleRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed) { "AngleRedrawer is disposed" }
update(System.nanoTime())
......
......@@ -16,14 +16,21 @@ internal class Direct3DRedrawer(
) : Redrawer {
private var isDisposed = false
private var disposeLock = Any()
private var drawLock = Any()
private val device = createDirectXDevice(getAdapterPriority(), layer.contentHandle).also {
if (it == 0L) {
throw Exception("Failed to create DirectX12 device.")
}
}
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
update(System.nanoTime())
draw()
}
override fun dispose() = synchronized(disposeLock) {
override fun dispose() = synchronized(drawLock) {
disposeDevice(device)
frameDispatcher.cancel()
isDisposed = true
}
......@@ -33,16 +40,12 @@ internal class Direct3DRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed) { "Direct3DRedrawer is disposed" }
// TODO now we wait until previous layer.draw is finished. it ends only on the next vsync.
// because of that we lose one frame on resize and can theoretically see very small white bars on the sides of the window
// to avoid this we should be able to draw in two modes: with vsync and without.
frameDispatcher.scheduleFrame()
layer.update(System.nanoTime())
if (prepareDrawContext()) {
drawAndSwap(withVsync = false)
}
}
private fun update(nanoTime: Long) {
......@@ -50,32 +53,33 @@ internal class Direct3DRedrawer(
}
private suspend fun draw() {
if (layer.prepareDrawContext()) {
if (prepareDrawContext()) {
withContext(Dispatchers.IO) {
synchronized(disposeLock) {
if (!isDisposed) {
layer.draw()
}
}
drawAndSwap(withVsync = properties.isVsyncEnabled)
}
}
}
fun makeContext(device: Long) = DirectContext(
private fun prepareDrawContext() = synchronized(drawLock) {
layer.prepareDrawContext()
}
private fun drawAndSwap(withVsync: Boolean) = synchronized(drawLock) {
if (!isDisposed) {
layer.draw()
swap(device, withVsync)
}
}
fun makeContext() = DirectContext(
makeDirectXContext(device)
)
fun makeSurface(device: Long, context: Long, width: Int, height: Int, index: Int) = Surface(
fun makeSurface(context: Long, width: Int, height: Int, index: Int) = Surface(
makeDirectXSurface(device, context, width, height, index)
)
fun createDevice(): Long = createDirectXDevice(getAdapterPriority(), layer.contentHandle)
fun finishFrame(device: Long, context: Long, surface: Long) {
finishFrame(device, context, surface, properties.isVsyncEnabled)
}
fun getAdapterPriority(): Int {
private fun getAdapterPriority(): Int {
val adapterPriority = GpuPriority.parse(System.getProperty("skiko.directx.gpu.priority"))
return when (adapterPriority) {
GpuPriority.Auto -> 0
......@@ -85,15 +89,23 @@ internal class Direct3DRedrawer(
}
}
external fun createDirectXDevice(adapterPriority: Int, contentHandle: Long): Long
external fun makeDirectXContext(device: Long): Long
external fun makeDirectXSurface(device: Long, context: Long, width: Int, height: Int, index: Int): Long
external fun resizeBuffers(device: Long, width: Int, height: Int)
private external fun finishFrame(device: Long, context: Long, surface: Long, isVsyncEnabled: Boolean)
external fun disposeDevice(device: Long)
external fun getBufferIndex(device: Long): Int
external fun initSwapChain(device: Long)
external fun initFence(device: Long)
external fun getAdapterName(device: Long): String
external fun getAdapterMemorySize(device: Long): Long
fun resizeBuffers(width: Int, height: Int) = resizeBuffers(device, width, height)
fun getBufferIndex() = getBufferIndex(device)
fun initSwapChain() = initSwapChain(device)
fun initFence() = initFence(device)
val adapterName get() = getAdapterName(device)
val adapterMemorySize get() = getAdapterMemorySize(device)
private external fun createDirectXDevice(adapterPriority: Int, contentHandle: Long): Long
private external fun makeDirectXContext(device: Long): Long
private external fun makeDirectXSurface(device: Long, context: Long, width: Int, height: Int, index: Int): Long
private external fun resizeBuffers(device: Long, width: Int, height: Int)
private external fun swap(device: Long, isVsyncEnabled: Boolean)
private external fun disposeDevice(device: Long)
private external fun getBufferIndex(device: Long): Int
private external fun initSwapChain(device: Long)
private external fun initFence(device: Long)
private external fun getAdapterName(device: Long): String
private external fun getAdapterMemorySize(device: Long): Long
}
......@@ -59,10 +59,6 @@ internal class LinuxOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
update(System.nanoTime())
......
......@@ -2,12 +2,7 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.Task
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
import org.jetbrains.skiko.*
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
......@@ -121,10 +116,6 @@ internal class MacOsOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
layer.update(System.nanoTime())
......
......@@ -7,13 +7,13 @@ import kotlinx.coroutines.withContext
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.*
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
import kotlin.time.ExperimentalTime
internal class MetalRedrawer(
private val layer: SkiaLayer,
properties: SkiaLayerProperties
private val properties: SkiaLayerProperties
) : Redrawer {
companion object {
init {
......@@ -21,18 +21,22 @@ internal class MetalRedrawer(
}
}
private var isDisposed = false
private var disposeLock = Any()
private var drawLock = Any()
private val device = layer.backedLayer.useDrawingSurfacePlatformInfo {
createMetalDevice(getAdapterPriority(), it)
}
private val windowHandle = layer.windowHandle
init {
setVSyncEnabled(device, properties.isVsyncEnabled)
}
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
update(System.nanoTime())
draw()
}
override fun dispose() = synchronized(disposeLock) {
override fun dispose() = synchronized(drawLock) {
frameDispatcher.cancel()
disposeDevice(device)
isDisposed = true
......@@ -43,16 +47,12 @@ internal class MetalRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" }
// TODO: now we wait until previous `layer.draw` is finished. it ends only on the next vsync.
// Because of that we lose one frame on resize and can theoretically see very small white bars on the sides
// of the window to avoid this we should be able to draw in two modes: with vsync and without.
frameDispatcher.scheduleFrame()
setVSyncEnabled(device, enabled = false)
update(System.nanoTime())
performDraw()
setVSyncEnabled(device, properties.isVsyncEnabled)
}
private fun update(nanoTime: Long) {
......@@ -60,25 +60,20 @@ internal class MetalRedrawer(
}
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
withContext(Dispatchers.IO) {
val handle = startRendering()
synchronized(disposeLock) {
if (!isDisposed)
if (layer.prepareDrawContext()) {
// 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
layer.draw()
}
}
performDraw()
endRendering(handle)
}
// When window is not visible - it doesn't make sense to redraw fast to avoid battery drain.
......@@ -88,6 +83,14 @@ internal class MetalRedrawer(
delay(300)
}
private fun performDraw() = synchronized(drawLock) {
if (!isDisposed) {
if (layer.prepareDrawContext()) {
layer.draw()
}
}
}
override fun syncSize() {
val rootPane = getRootPane(layer)
val globalPosition = convertPoint(layer, layer.x, layer.y, rootPane)
......@@ -131,6 +134,7 @@ internal class MetalRedrawer(
private external fun finishFrame(device: Long)
private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int)
private external fun setContentScale(device: Long, contentScale: Float)
private external fun setVSyncEnabled(device: Long, enabled: Boolean)
private external fun isOccluded(window: Long): Boolean
private external fun getAdapterName(device: Long): String
private external fun getAdapterMemorySize(device: Long): Long
......
package org.jetbrains.skiko.redrawer
interface Redrawer {
internal interface Redrawer {
fun dispose()
fun needRedraw()
fun redrawImmediately()
suspend fun awaitRedraw(): Boolean
fun syncSize() = Unit
}
\ No newline at end of file
......@@ -36,10 +36,6 @@ internal class SoftwareRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
layer.update(System.nanoTime())
if (layer.prepareDrawContext()) {
......
......@@ -4,12 +4,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.isVideoCardSupported
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
import org.jetbrains.skiko.*
internal class WindowsOpenGLRedrawer(
private val layer: SkiaLayer,
......@@ -45,10 +40,6 @@ internal class WindowsOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
update(System.nanoTime())
......
......@@ -247,6 +247,12 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_setConten
[CATransaction flush];
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_setVSyncEnabled(JNIEnv *env, jobject obj, jlong devicePtr, jboolean enabled)
{
MetalDevice *device = (MetalDevice *) devicePtr;
device.layer.displaySyncEnabled = enabled;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_finishFrame(
JNIEnv *env, jobject redrawer, jlong devicePtr)
{
......
package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.concurrent.Executors
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
class FrameDispatcherTest {
......@@ -197,75 +185,6 @@ class FrameDispatcherTest {
assertEquals(listOf("frame0", "task", "frame1"), history)
}
@Test
fun `await frame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {
frameCount++
}
frameDispatcher.awaitFrame()
assertEquals(1, frameCount)
frameDispatcher.awaitFrame()
assertEquals(2, frameCount)
frameDispatcher.awaitFrame()
assertEquals(3, frameCount)
repeat(100) {
yield()
}
assertEquals(3, frameCount)
}
@Test
fun `await active frame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
val isActive = frameDispatcher.awaitFrame()
assertTrue(isActive)
}
@Test
fun `await failed frame`() = test {
val ignoreExceptionHandler = object :
AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) = Unit
}
val scope = CoroutineScope(coroutineContext + ignoreExceptionHandler)
val frameDispatcher = FrameDispatcher(scope = scope) {
throw RuntimeException()
}
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
@Test
fun `cancel dispatcher before awaitFrame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
frameDispatcher.cancel()
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
@Test
fun `cancel scope before awaitFrame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
scope.cancel()
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
private fun test(
block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest {
......
......@@ -79,7 +79,7 @@ class SkiaWindowTest {
window.layer.renderer = renderer
window.isUndecorated = true
window.pack()
window.layer.awaitRedraw()
window.paint(window.graphics)
window.isVisible = true
delay(1000)
......
package org.jetbrains.skiko.util
import java.awt.Color
import java.awt.image.BufferedImage
import kotlin.math.abs
fun isContentSame(img1: BufferedImage, img2: BufferedImage): Boolean {
fun isContentSame(img1: BufferedImage, img2: BufferedImage, sensitivity: Double): Boolean {
require(sensitivity in 0.0..1.0)
val sensitivity255 = (sensitivity * 255).toInt()
if (img1.width == img2.width && img1.height == img2.height) {
for (x in 0 until img1.width) {
for (y in 0 until img1.height) {
if (img1.getRGB(x, y) != img2.getRGB(x, y)) {
val color1 = Color(img1.getRGB(x, y))
val color2 = Color(img2.getRGB(x, y))
if (abs(color1.red - color2.red) > sensitivity255) {
return false
}
if (abs(color1.green - color2.green) > sensitivity255) {
return false
}
if (abs(color1.blue - color2.blue) > sensitivity255) {
return false
}
if (abs(color1.alpha - color2.alpha) > sensitivity255) {
return false
}
}
......
......@@ -49,7 +49,8 @@ class ScreenshotTestRule : TestRule {
}
if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile)
if (!isContentSame(expected, actual)) {
// macOs screenshots can have different color on different configurations
if (!isContentSame(expected, actual, sensitivity = 0.25)) {
ImageIO.write(actual, "png", actualFile)
throw AssertionError(
"Image mismatch! Expected image ${expectedFile.absolutePath}, actual: ${actualFile.absolutePath}"
......
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