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

[Metal] Decouple onRender from draw (#1057)

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