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() { ...@@ -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 mouseX = 0
var mouseY = 0 var mouseY = 0
...@@ -113,7 +113,7 @@ fun createWindow(title: String, exitOnClose: Boolean) = runBlocking(Dispatchers. ...@@ -113,7 +113,7 @@ fun createWindow(title: String, exitOnClose: Boolean) = runBlocking(Dispatchers.
// MANDATORY: set window preferred size before calling pack() // MANDATORY: set window preferred size before calling pack()
window.preferredSize = Dimension(800, 600) window.preferredSize = Dimension(800, 600)
window.pack() window.pack()
window.layer.awaitRedraw() window.layer.paint(window.graphics)
window.isVisible = true 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" ...@@ -418,17 +418,10 @@ extern "C"
GR_D3D_CALL_ERRCHECK(d3dDevice->swapChain->ResizeBuffers(BuffersCount, width, height, DXGI_FORMAT_R8G8B8A8_UNORM, 0)); 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( JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_swap(
JNIEnv *env, jobject redrawer, jlong devicePtr, jlong contextPtr, jlong surfacePtr, jboolean isVsyncEnabled) JNIEnv *env, jobject redrawer, jlong devicePtr, jboolean isVsyncEnabled)
{ {
DirectXDevice *d3dDevice = fromJavaPointer<DirectXDevice *>(devicePtr); 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. // 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]; const UINT64 fenceValue = d3dDevice->fenceValues[d3dDevice->bufferIndex];
......
...@@ -3,11 +3,8 @@ package org.jetbrains.skiko ...@@ -3,11 +3,8 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
/** /**
* Dispatch frame after call of [scheduleFrame]. * Dispatch frame after call of [scheduleFrame].
...@@ -34,7 +31,6 @@ class FrameDispatcher( ...@@ -34,7 +31,6 @@ class FrameDispatcher(
frameChannel.receive() frameChannel.receive()
frameScheduled = false frameScheduled = false
onFrame() onFrame()
resumeFrameAwaiters(isActive = true)
// As per `yield()` documentation: // As per `yield()` documentation:
// //
// For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and // For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and
...@@ -45,12 +41,6 @@ class FrameDispatcher( ...@@ -45,12 +41,6 @@ class FrameDispatcher(
} }
} }
init {
job.invokeOnCompletion {
resumeFrameAwaiters(isActive = false)
}
}
fun cancel() { fun cancel() {
job.cancel() job.cancel()
} }
...@@ -69,35 +59,4 @@ class FrameDispatcher( ...@@ -69,35 +59,4 @@ class FrameDispatcher(
frameChannel.offer(Unit) 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 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.skia.*
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.createContextHandler import org.jetbrains.skiko.context.createContextHandler
...@@ -67,8 +63,7 @@ open class SkiaLayer( ...@@ -67,8 +63,7 @@ open class SkiaLayer(
} }
} }
private val onInit = CompletableDeferred<Unit>() private var isInited = false
private val isInited get() = onInit.isCompleted
private var isRendering = false private var isRendering = false
private fun checkInit() { private fun checkInit() {
...@@ -138,7 +133,7 @@ open class SkiaLayer( ...@@ -138,7 +133,7 @@ open class SkiaLayer(
open fun init() { open fun init() {
backedLayer.init() backedLayer.init()
findNextWorkingRenderApi(false) findNextWorkingRenderApi(false)
onInit.complete(Unit) isInited = true
} }
private val stateHandlers = private val stateHandlers =
...@@ -281,20 +276,6 @@ open class SkiaLayer( ...@@ -281,20 +276,6 @@ open class SkiaLayer(
redrawer?.needRedraw() 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") @Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this) private val fpsCounter = defaultFPSCounter(this)
......
...@@ -10,18 +10,13 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer) ...@@ -10,18 +10,13 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
private val bufferCount = 2 private val bufferCount = 2
private var surfaces: Array<Surface?> = arrayOfNulls(bufferCount) private var surfaces: Array<Surface?> = arrayOfNulls(bufferCount)
val directXRedrawer: Direct3DRedrawer private val directXRedrawer: Direct3DRedrawer
get() = layer.redrawer!! as Direct3DRedrawer get() = layer.redrawer!! as Direct3DRedrawer
var device: Long = 0
override fun initContext(): Boolean { override fun initContext(): Boolean {
try { try {
if (context == null) { if (context == null) {
device = directXRedrawer.createDevice() context = directXRedrawer.makeContext()
if (device == 0L) {
throw Exception("Failed to create DirectX12 device.")
}
context = directXRedrawer.makeContext(device)
if (System.getProperty("skiko.hardwareInfo.enabled") == "true") { if (System.getProperty("skiko.hardwareInfo.enabled") == "true") {
println(rendererInfo()) println(rendererInfo())
} }
...@@ -55,14 +50,14 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer) ...@@ -55,14 +50,14 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
context?.flush() context?.flush()
if (!isD3DInited) { if (!isD3DInited) {
directXRedrawer.initSwapChain(device) directXRedrawer.initSwapChain()
} else { } else {
directXRedrawer.resizeBuffers(device, w, h) directXRedrawer.resizeBuffers(w, h)
} }
try { try {
for (bufferIndex in 0..bufferCount - 1) { 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 { } finally {
Reference.reachabilityFence(context!!) Reference.reachabilityFence(context!!)
...@@ -70,17 +65,16 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer) ...@@ -70,17 +65,16 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
if (!isD3DInited) { if (!isD3DInited) {
isD3DInited = true isD3DInited = true
directXRedrawer.initFence(device) directXRedrawer.initFence()
} }
} }
surface = surfaces[directXRedrawer.getBufferIndex(device)] surface = surfaces[directXRedrawer.getBufferIndex()]
canvas = surface!!.canvas canvas = surface!!.canvas
} }
override fun flush() { override fun flush() {
try { try {
directXRedrawer.finishFrame( flush(
device,
Native.getPtr(context!!), Native.getPtr(context!!),
Native.getPtr(surface!!) Native.getPtr(surface!!)
) )
...@@ -91,19 +85,20 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer) ...@@ -91,19 +85,20 @@ internal class Direct3DContextHandler(layer: SkiaLayer) : ContextHandler(layer)
} }
override fun destroyContext() { override fun destroyContext() {
directXRedrawer.disposeDevice(device)
context?.close() context?.close()
} }
override fun disposeCanvas() { override fun disposeCanvas() {
for (bufferIndex in 0..bufferCount - 1) { for (bufferIndex in 0 until bufferCount) {
surfaces[bufferIndex]?.close() surfaces[bufferIndex]?.close()
} }
} }
override fun rendererInfo(): String { override fun rendererInfo(): String {
return super.rendererInfo() + return super.rendererInfo() +
"Video card: ${directXRedrawer.getAdapterName(device)}\n" + "Video card: ${directXRedrawer.adapterName}\n" +
"Total VRAM: ${directXRedrawer.getAdapterMemorySize(device) / 1024 / 1024} MB\n" "Total VRAM: ${directXRedrawer.adapterMemorySize / 1024 / 1024} MB\n"
} }
private external fun flush(context: Long, surface: Long)
} }
...@@ -32,10 +32,6 @@ internal class AngleRedrawer( ...@@ -32,10 +32,6 @@ internal class AngleRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "AngleRedrawer is disposed" } check(!isDisposed) { "AngleRedrawer is disposed" }
update(System.nanoTime()) update(System.nanoTime())
......
...@@ -16,14 +16,21 @@ internal class Direct3DRedrawer( ...@@ -16,14 +16,21 @@ internal class Direct3DRedrawer(
) : Redrawer { ) : Redrawer {
private var isDisposed = false 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) { private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
update(System.nanoTime()) update(System.nanoTime())
draw() draw()
} }
override fun dispose() = synchronized(disposeLock) { override fun dispose() = synchronized(drawLock) {
disposeDevice(device)
frameDispatcher.cancel() frameDispatcher.cancel()
isDisposed = true isDisposed = true
} }
...@@ -33,16 +40,12 @@ internal class Direct3DRedrawer( ...@@ -33,16 +40,12 @@ internal class Direct3DRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "Direct3DRedrawer is disposed" } check(!isDisposed) { "Direct3DRedrawer is disposed" }
// TODO now we wait until previous layer.draw is finished. it ends only on the next vsync. layer.update(System.nanoTime())
// because of that we lose one frame on resize and can theoretically see very small white bars on the sides of the window if (prepareDrawContext()) {
// to avoid this we should be able to draw in two modes: with vsync and without. drawAndSwap(withVsync = false)
frameDispatcher.scheduleFrame() }
} }
private fun update(nanoTime: Long) { private fun update(nanoTime: Long) {
...@@ -50,32 +53,33 @@ internal class Direct3DRedrawer( ...@@ -50,32 +53,33 @@ internal class Direct3DRedrawer(
} }
private suspend fun draw() { private suspend fun draw() {
if (layer.prepareDrawContext()) { if (prepareDrawContext()) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
synchronized(disposeLock) { drawAndSwap(withVsync = properties.isVsyncEnabled)
if (!isDisposed) {
layer.draw()
} }
} }
} }
private fun prepareDrawContext() = synchronized(drawLock) {
layer.prepareDrawContext()
}
private fun drawAndSwap(withVsync: Boolean) = synchronized(drawLock) {
if (!isDisposed) {
layer.draw()
swap(device, withVsync)
} }
} }
fun makeContext(device: Long) = DirectContext( fun makeContext() = DirectContext(
makeDirectXContext(device) 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) makeDirectXSurface(device, context, width, height, index)
) )
fun createDevice(): Long = createDirectXDevice(getAdapterPriority(), layer.contentHandle) private fun getAdapterPriority(): Int {
fun finishFrame(device: Long, context: Long, surface: Long) {
finishFrame(device, context, surface, properties.isVsyncEnabled)
}
fun getAdapterPriority(): Int {
val adapterPriority = GpuPriority.parse(System.getProperty("skiko.directx.gpu.priority")) val adapterPriority = GpuPriority.parse(System.getProperty("skiko.directx.gpu.priority"))
return when (adapterPriority) { return when (adapterPriority) {
GpuPriority.Auto -> 0 GpuPriority.Auto -> 0
...@@ -85,15 +89,23 @@ internal class Direct3DRedrawer( ...@@ -85,15 +89,23 @@ internal class Direct3DRedrawer(
} }
} }
external fun createDirectXDevice(adapterPriority: Int, contentHandle: Long): Long fun resizeBuffers(width: Int, height: Int) = resizeBuffers(device, width, height)
external fun makeDirectXContext(device: Long): Long
external fun makeDirectXSurface(device: Long, context: Long, width: Int, height: Int, index: Int): Long fun getBufferIndex() = getBufferIndex(device)
external fun resizeBuffers(device: Long, width: Int, height: Int) fun initSwapChain() = initSwapChain(device)
private external fun finishFrame(device: Long, context: Long, surface: Long, isVsyncEnabled: Boolean) fun initFence() = initFence(device)
external fun disposeDevice(device: Long) val adapterName get() = getAdapterName(device)
external fun getBufferIndex(device: Long): Int val adapterMemorySize get() = getAdapterMemorySize(device)
external fun initSwapChain(device: Long)
external fun initFence(device: Long) private external fun createDirectXDevice(adapterPriority: Int, contentHandle: Long): Long
external fun getAdapterName(device: Long): String private external fun makeDirectXContext(device: Long): Long
external fun getAdapterMemorySize(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( ...@@ -59,10 +59,6 @@ internal class LinuxOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface { override fun redrawImmediately() = layer.backedLayer.lockLinuxDrawingSurface {
check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" } check(!isDisposed) { "LinuxOpenGLRedrawer is disposed" }
update(System.nanoTime()) update(System.nanoTime())
......
...@@ -2,12 +2,7 @@ package org.jetbrains.skiko.redrawer ...@@ -2,12 +2,7 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.*
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 javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane import javax.swing.SwingUtilities.getRootPane
...@@ -121,10 +116,6 @@ internal class MacOsOpenGLRedrawer( ...@@ -121,10 +116,6 @@ internal class MacOsOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
......
...@@ -7,13 +7,13 @@ import kotlinx.coroutines.withContext ...@@ -7,13 +7,13 @@ 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 org.jetbrains.skiko.useDrawingSurfacePlatformInfo
import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane import javax.swing.SwingUtilities.getRootPane
import kotlin.time.ExperimentalTime
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
properties: SkiaLayerProperties private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
companion object { companion object {
init { init {
...@@ -21,18 +21,22 @@ internal class MetalRedrawer( ...@@ -21,18 +21,22 @@ internal class MetalRedrawer(
} }
} }
private var isDisposed = false private var isDisposed = false
private var disposeLock = Any() private var drawLock = Any()
private val device = layer.backedLayer.useDrawingSurfacePlatformInfo { private val device = layer.backedLayer.useDrawingSurfacePlatformInfo {
createMetalDevice(getAdapterPriority(), it) createMetalDevice(getAdapterPriority(), it)
} }
private val windowHandle = layer.windowHandle private val windowHandle = layer.windowHandle
init {
setVSyncEnabled(device, properties.isVsyncEnabled)
}
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) { private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
update(System.nanoTime()) update(System.nanoTime())
draw() draw()
} }
override fun dispose() = synchronized(disposeLock) { override fun dispose() = synchronized(drawLock) {
frameDispatcher.cancel() frameDispatcher.cancel()
disposeDevice(device) disposeDevice(device)
isDisposed = true isDisposed = true
...@@ -43,16 +47,12 @@ internal class MetalRedrawer( ...@@ -43,16 +47,12 @@ internal class MetalRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" } check(!isDisposed) { "MetalRedrawer is disposed" }
// TODO: now we wait until previous `layer.draw` is finished. it ends only on the next vsync. setVSyncEnabled(device, enabled = false)
// Because of that we lose one frame on resize and can theoretically see very small white bars on the sides update(System.nanoTime())
// of the window to avoid this we should be able to draw in two modes: with vsync and without. performDraw()
frameDispatcher.scheduleFrame() setVSyncEnabled(device, properties.isVsyncEnabled)
} }
private fun update(nanoTime: Long) { private fun update(nanoTime: Long) {
...@@ -60,11 +60,6 @@ internal class MetalRedrawer( ...@@ -60,11 +60,6 @@ internal class MetalRedrawer(
} }
private suspend fun draw() { private suspend fun draw() {
withContext(Dispatchers.IO) {
val handle = startRendering()
synchronized(disposeLock) {
if (!isDisposed)
if (layer.prepareDrawContext()) {
// 2,3 GHz 8-Core Intel Core i9 // 2,3 GHz 8-Core Intel Core i9
// //
// Test1. 8 windows, multiple clocks, 800x600 // Test1. 8 windows, multiple clocks, 800x600
...@@ -76,9 +71,9 @@ internal class MetalRedrawer( ...@@ -76,9 +71,9 @@ 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
layer.draw() withContext(Dispatchers.IO) {
} val handle = startRendering()
} performDraw()
endRendering(handle) 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.
...@@ -88,6 +83,14 @@ internal class MetalRedrawer( ...@@ -88,6 +83,14 @@ internal class MetalRedrawer(
delay(300) delay(300)
} }
private fun performDraw() = synchronized(drawLock) {
if (!isDisposed) {
if (layer.prepareDrawContext()) {
layer.draw()
}
}
}
override fun syncSize() { override fun syncSize() {
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)
...@@ -131,6 +134,7 @@ internal class MetalRedrawer( ...@@ -131,6 +134,7 @@ internal class MetalRedrawer(
private external fun finishFrame(device: Long) private external fun finishFrame(device: Long)
private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int) 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 setContentScale(device: Long, contentScale: Float)
private external fun setVSyncEnabled(device: Long, enabled: Boolean)
private external fun isOccluded(window: Long): Boolean private external fun isOccluded(window: Long): Boolean
private external fun getAdapterName(device: Long): String private external fun getAdapterName(device: Long): String
private external fun getAdapterMemorySize(device: Long): Long private external fun getAdapterMemorySize(device: Long): Long
......
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
interface Redrawer { internal interface Redrawer {
fun dispose() fun dispose()
fun needRedraw() fun needRedraw()
fun redrawImmediately() fun redrawImmediately()
suspend fun awaitRedraw(): Boolean
fun syncSize() = Unit fun syncSize() = Unit
} }
\ No newline at end of file
...@@ -36,10 +36,6 @@ internal class SoftwareRedrawer( ...@@ -36,10 +36,6 @@ internal class SoftwareRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
if (layer.prepareDrawContext()) { if (layer.prepareDrawContext()) {
......
...@@ -4,12 +4,7 @@ import kotlinx.coroutines.CancellationException ...@@ -4,12 +4,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.*
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
internal class WindowsOpenGLRedrawer( internal class WindowsOpenGLRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
...@@ -45,10 +40,6 @@ internal class WindowsOpenGLRedrawer( ...@@ -45,10 +40,6 @@ internal class WindowsOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" } check(!isDisposed) { "WindowsOpenGLRedrawer is disposed" }
update(System.nanoTime()) update(System.nanoTime())
......
...@@ -247,6 +247,12 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_setConten ...@@ -247,6 +247,12 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_setConten
[CATransaction flush]; [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( JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_finishFrame(
JNIEnv *env, jobject redrawer, jlong devicePtr) JNIEnv *env, jobject redrawer, jlong devicePtr)
{ {
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.*
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.test.TestCoroutineScope import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class FrameDispatcherTest { class FrameDispatcherTest {
...@@ -197,75 +185,6 @@ class FrameDispatcherTest { ...@@ -197,75 +185,6 @@ class FrameDispatcherTest {
assertEquals(listOf("frame0", "task", "frame1"), history) 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( private fun test(
block: suspend TestCoroutineScope.() -> Unit block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest { ) = runBlockingTest {
......
...@@ -79,7 +79,7 @@ class SkiaWindowTest { ...@@ -79,7 +79,7 @@ class SkiaWindowTest {
window.layer.renderer = renderer window.layer.renderer = renderer
window.isUndecorated = true window.isUndecorated = true
window.pack() window.pack()
window.layer.awaitRedraw() window.paint(window.graphics)
window.isVisible = true window.isVisible = true
delay(1000) delay(1000)
......
package org.jetbrains.skiko.util package org.jetbrains.skiko.util
import java.awt.Color
import java.awt.image.BufferedImage 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) { if (img1.width == img2.width && img1.height == img2.height) {
for (x in 0 until img1.width) { for (x in 0 until img1.width) {
for (y in 0 until img1.height) { 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 return false
} }
} }
......
...@@ -49,7 +49,8 @@ class ScreenshotTestRule : TestRule { ...@@ -49,7 +49,8 @@ class ScreenshotTestRule : TestRule {
} }
if (expectedFile.exists()) { if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile) 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) ImageIO.write(actual, "png", actualFile)
throw AssertionError( throw AssertionError(
"Image mismatch! Expected image ${expectedFile.absolutePath}, actual: ${actualFile.absolutePath}" "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