Unverified Commit a2eed5a7 authored by Nikolay Igotti's avatar Nikolay Igotti Committed by GitHub

macos dispatcher (#295)

parent bbd1c31e
......@@ -6,9 +6,11 @@ import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
class BouncingBalls: SkikoView {
class BouncingBalls(private val withFps: Boolean = false): SkikoView {
private data class Circle(var x: Float, var y: Float, var r: Float)
private val fpsCounter = FPSCounter(2.0, true)
companion object {
private fun moveCircle(c: Circle, s: Float, angle: Float, width: Int, height: Int, r: Float) {
c.x = (c.x + s * sin(angle)).coerceAtLeast(r).coerceAtMost(width.toFloat() - r)
......@@ -85,6 +87,8 @@ class BouncingBalls: SkikoView {
)).toMutableList()
override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
if (withFps) fpsCounter.tick()
canvas.clear(-1)
val dtime = (nanoTime - prevTimestamp)
prevTimestamp = nanoTime
......
......@@ -7,7 +7,7 @@ import org.jetbrains.skiko.*
import kotlinx.cinterop.*
import platform.Foundation.NSMakeRect
fun makeApp() = BouncingBalls()
fun makeApp() = BouncingBalls(true)
fun main() {
NSApplication.sharedApplication()
......
......@@ -383,14 +383,17 @@ kotlin {
}
}
if (hostOs == OS.MacOS) {
val macosMain by creating {
val darwinMain by creating {
dependsOn(nativeMain)
}
val macosMain by creating {
dependsOn(darwinMain)
}
val macosTest by creating {
dependsOn(nativeTest)
}
val iosMain by creating {
dependsOn(nativeMain)
dependsOn(darwinMain)
}
val iosTest by creating {
dependsOn(nativeTest)
......
package org.jetbrains.skiko
internal expect inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R
internal expect fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties
\ No newline at end of file
internal expect fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties
internal expect fun currentNanoTime(): Long
\ No newline at end of file
package org.jetbrains.skiko
import java.awt.Component
import kotlin.math.roundToInt
internal class FPSCounter(
class FPSCounter(
private val periodSeconds: Double,
private val showLongFrames: Boolean,
private val getLongFrameMillis: () -> Double
private val getLongFrameMillis: () -> Double = {
1.5 * 1000 / 60
}
) {
private val times = mutableListOf<Long>()
private var lastLogTime = System.nanoTime()
private var lastTime = System.nanoTime()
private var lastLogTime = currentNanoTime()
private var lastTime = currentNanoTime()
fun tick() {
val time = System.nanoTime()
val time = currentNanoTime()
val timestamp = time.nanosToMillis().toLong()
val frameTime = time - lastTime
lastTime = time
......@@ -21,7 +22,7 @@ internal class FPSCounter(
times.add(frameTime)
if (showLongFrames && frameTime > getLongFrameMillis().millisToNanos()) {
println("[%d] Long frame %.2f ms".format(timestamp, frameTime.nanosToMillis()))
println("$timestamp Long frame ${frameTime.nanosToMillis()} ms")
}
if ((time - lastLogTime) > periodSeconds.secondsToNanos()) {
......@@ -39,21 +40,4 @@ internal class FPSCounter(
private fun Long.nanosToMillis(): Double = this / nanosPerMillis
private fun Double.millisToNanos(): Long = (this * nanosPerMillis).toLong()
private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
}
internal fun defaultFPSCounter(
component: Component
): FPSCounter? = with(SkikoProperties) {
if (!SkikoProperties.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 }
FPSCounter(
periodSeconds = fpsPeriodSeconds,
showLongFrames = fpsLongFramesShow,
getLongFrameMillis = {
fpsLongFramesMillis ?: 1.5 * 1000 / refreshRate
}
)
}
\ No newline at end of file
......@@ -23,7 +23,7 @@ actual open class SkiaLayer(
set(value) { throw UnsupportedOperationException() }
actual val contentScale: Float
get() = view!!.contentScaleFactor?.toFloat()
get() = view!!.contentScaleFactor.toFloat()
actual var fullscreen: Boolean
get() = true
......@@ -50,9 +50,11 @@ actual open class SkiaLayer(
internal var view: UIView? = null
// We need to keep reference to controller as Objective-C will only keep weak reference here.
lateinit private var controller: NSObject
actual fun attachTo(container: Any) {
attachTo(container as UIView)
}
fun attachTo(view: UIView) {
this.view = view
contextHandler = MetalContextHandler(this)
......@@ -79,8 +81,9 @@ actual open class SkiaLayer(
// We have ':' in selector to take care of function argument.
view.addGestureRecognizer(UITapGestureRecognizer(controller, NSSelectorFromString("onTap:")))
// TODO: maybe add observer for view.viewDidDisappear() to detach us?
redrawer = MetalRedrawer(this, properties)
redrawer?.redrawImmediately()
redrawer = MetalRedrawer(this, properties).apply {
needRedraw()
}
}
private var isDisposed = false
......
......@@ -36,7 +36,7 @@ internal class MetalRedrawer(
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
if (layer.isShowing()) {
update(getTimeNanos())
layer.update(getTimeNanos())
draw()
}
}
......@@ -49,8 +49,11 @@ internal class MetalRedrawer(
}
override fun dispose() {
frameDispatcher.cancel()
isDisposed = true
if (!isDisposed) {
frameDispatcher.cancel()
metalLayer.dispose()
isDisposed = true
}
}
override fun syncSize() {
......@@ -71,14 +74,10 @@ internal class MetalRedrawer(
override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" }
update(getTimeNanos())
layer.update(getTimeNanos())
draw()
}
private fun update(nanoTime: Long) {
layer.update(nanoTime)
}
private fun draw() {
// TODO: maybe make flush async as in JVM version.
autoreleasepool {
......@@ -127,23 +126,14 @@ class MetalLayer : CAMetalLayer {
}
}
fun draw() {
skiaLayer.update(getTimeNanos())
skiaLayer.draw()
}
fun dispose() {
this.removeFromSuperlayer()
// TODO: anything else to dispose the layer?
}
@Suppress("unused")
private fun performDraw() {
draw()
}
override fun drawInContext(ctx: CGContextRef?) {
draw()
skiaLayer.update(getTimeNanos())
skiaLayer.draw()
super.drawInContext(ctx)
}
}
......@@ -5,4 +5,6 @@ internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block()
\ No newline at end of file
block()
actual fun currentNanoTime(): Long = (kotlinx.browser.window.performance.now() * 1_000_000).toLong()
\ No newline at end of file
......@@ -5,4 +5,6 @@ internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
synchronized(lock, block)
\ No newline at end of file
synchronized(lock, block)
actual fun currentNanoTime(): Long = System.nanoTime()
\ No newline at end of file
......@@ -526,6 +526,20 @@ fun orderEmojiAndSymbolsPopup() {
platformOperations.orderEmojiAndSymbolsPopup()
}
internal fun defaultFPSCounter(
component: Component
): FPSCounter? = with(SkikoProperties) {
if (!SkikoProperties.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 }
FPSCounter(
periodSeconds = fpsPeriodSeconds,
showLongFrames = fpsLongFramesShow,
getLongFrameMillis = { fpsLongFramesMillis ?: 1.5 * 1000 / refreshRate }
)
}
// InputEvent is abstract, so we wrap to match modality.
actual class SkikoPlatformInputEvent(val wrapped: InputEvent)
actual typealias SkikoPlatformKeyboardEvent = KeyEvent
......
......@@ -74,6 +74,9 @@ actual open class SkiaLayer(
}
nsView = object : NSView(NSMakeRect(0.0, 0.0, width, height)) {
private var trackingArea : NSTrackingArea? = null
override fun wantsUpdateLayer(): Boolean {
return true
}
override fun acceptsFirstResponder(): Boolean {
return true
}
......@@ -121,8 +124,9 @@ actual open class SkiaLayer(
center.addObserver(nsView, NSSelectorFromString("onWindowClose:"),
NSWindowWillCloseNotification!!, window)
window.contentView!!.addSubview(nsView)
redrawer = createNativeRedrawer(this, GraphicsApi.OPENGL, properties)
redrawer?.redrawImmediately()
redrawer = createNativeRedrawer(this, GraphicsApi.OPENGL, properties).apply {
needRedraw()
}
}
actual fun detach() {
......
......@@ -2,8 +2,10 @@ package org.jetbrains.skiko.redrawer
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.useContents
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.SkikoDispatchers
import platform.CoreFoundation.CFTimeInterval
import platform.CoreGraphics.CGRectMake
import platform.CoreVideo.CVTimeStamp
......@@ -15,19 +17,23 @@ import platform.QuartzCore.*
import kotlin.system.getTimeNanos
internal class MacOsOpenGLRedrawer(
private val layer: SkiaLayer,
private val skiaLayer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
private val drawLayer = MacosGLLayer(layer, setNeedsDisplayOnBoundsChange = true)
private val glLayer = MacosGLLayer(skiaLayer)
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
redrawImmediately()
}
override fun dispose() {
drawLayer.dispose()
glLayer.dispose()
}
override fun syncSize() {
// TODO: What do we really do here?
layer.nsView.frame.useContents {
drawLayer.setFrame(
skiaLayer.nsView.frame.useContents {
glLayer.setFrame(
origin.x.toInt(),
origin.y.toInt(),
size.width.toInt().coerceAtLeast(0),
......@@ -37,28 +43,24 @@ internal class MacOsOpenGLRedrawer(
}
override fun needRedraw() {
frameDispatcher.scheduleFrame()
}
override fun redrawImmediately() {
layer.update(getTimeNanos())
glLayer.setNeedsDisplay()
skiaLayer.nsView.setNeedsDisplay(true)
}
}
class MacosGLLayer(val layer: SkiaLayer, setNeedsDisplayOnBoundsChange: Boolean) : CAOpenGLLayer() {
internal class MacosGLLayer(val layer: SkiaLayer) : CAOpenGLLayer() {
init {
this.setNeedsDisplayOnBoundsChange(setNeedsDisplayOnBoundsChange)
this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations()
this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable )
this.setAsynchronous(true)
layer.nsView.layer = this
layer.nsView.wantsLayer = true
}
fun draw() {
layer.update(getTimeNanos())
layer.draw()
}
fun setFrame(x: Int, y: Int, width: Int, height: Int) {
val newY = layer.nsView.frame.useContents { size.height } - y - height
......@@ -73,16 +75,6 @@ class MacosGLLayer(val layer: SkiaLayer, setNeedsDisplayOnBoundsChange: Boolean)
// TODO: anything else to dispose the layer?
}
@Suppress("unused")
private fun performDraw() {
try {
draw()
} catch (e: Throwable) {
e.printStackTrace()
}
//display.finish()
}
override fun canDrawInCGLContext(
ctx: CGLContextObj?,
pixelFormat: CGLPixelFormatObj?,
......@@ -99,9 +91,14 @@ class MacosGLLayer(val layer: SkiaLayer, setNeedsDisplayOnBoundsChange: Boolean)
displayTime: CPointer<CVTimeStamp>?
) {
CGLSetCurrentContext(ctx);
performDraw()
try {
layer.update(getTimeNanos())
layer.draw()
} catch (e: Throwable) {
e.printStackTrace()
throw e
}
//context.flush() // TODO: I thought the below should call context.flush().
super.drawInCGLContext(ctx, pixelFormat,forLayerTime, displayTime)
}
}
......
......@@ -5,4 +5,6 @@ internal actual fun makeDefaultSkiaLayerProperties(): SkiaLayerProperties {
}
internal actual inline fun <R> maybeSynchronized(lock: Any, block: () -> R): R =
block()
\ No newline at end of file
block()
actual fun currentNanoTime(): Long = kotlin.system.getTimeNanos()
\ No newline at end of file
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