Unverified Commit eb4bee60 authored by Roman Sedaikin's avatar Roman Sedaikin Committed by GitHub

Added Metal renderer for macos-native. (#307)

* Added Metal renderer for macos-native.
parent 90f73ca5
...@@ -5,6 +5,8 @@ import org.jetbrains.skia.paragraph.FontCollection ...@@ -5,6 +5,8 @@ import org.jetbrains.skia.paragraph.FontCollection
import org.jetbrains.skia.paragraph.ParagraphBuilder import org.jetbrains.skia.paragraph.ParagraphBuilder
import org.jetbrains.skia.paragraph.ParagraphStyle import org.jetbrains.skia.paragraph.ParagraphStyle
import org.jetbrains.skia.paragraph.TextStyle import org.jetbrains.skia.paragraph.TextStyle
import org.jetbrains.skiko.currentSystemTheme
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkikoView import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.SkikoPointerEvent import org.jetbrains.skiko.SkikoPointerEvent
import org.jetbrains.skiko.SkikoGestureEvent import org.jetbrains.skiko.SkikoGestureEvent
...@@ -16,7 +18,7 @@ import kotlin.math.sin ...@@ -16,7 +18,7 @@ import kotlin.math.sin
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.pow import kotlin.math.pow
class Clocks: SkikoView { class Clocks(private val layer: SkiaLayer): SkikoView {
private var frame = 0 private var frame = 0
private var xpos = 0.0 private var xpos = 0.0
private var ypos = 0.0 private var ypos = 0.0
...@@ -84,6 +86,13 @@ class Clocks: SkikoView { ...@@ -84,6 +86,13 @@ class Clocks: SkikoView {
} }
val style = ParagraphStyle() val style = ParagraphStyle()
val renderInfo = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt()))
.addText("Graphics API: ${layer.renderApi} ✿゚ ${currentSystemTheme}")
.popStyle()
.build()
renderInfo.layout(Float.POSITIVE_INFINITY)
renderInfo.paint(canvas, 5f, 5f)
val frames = ParagraphBuilder(style, fontCollection) val frames = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xff9BC730L.toInt()).setFontSize(20f)) .pushStyle(TextStyle().setColor(0xff9BC730L.toInt()).setFontSize(20f))
.addText("Frames: ${frame++}\nAngle: $rotate") .addText("Frames: ${frame++}\nAngle: $rotate")
......
...@@ -3,12 +3,13 @@ package org.jetbrains.skiko.sample ...@@ -3,12 +3,13 @@ package org.jetbrains.skiko.sample
import kotlinx.cinterop.* import kotlinx.cinterop.*
import org.jetbrains.skiko.GenericSkikoView import org.jetbrains.skiko.GenericSkikoView
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkikoView import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.SkikoViewController import org.jetbrains.skiko.SkikoViewController
import platform.UIKit.* import platform.UIKit.*
import platform.Foundation.* import platform.Foundation.*
fun makeApp(): SkikoView = Clocks() fun makeApp(skiaLayer: SkiaLayer) = Clocks(skiaLayer)
fun main() { fun main() {
val args = emptyArray<String>() val args = emptyArray<String>()
...@@ -37,7 +38,7 @@ class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol { ...@@ -37,7 +38,7 @@ class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
window = UIWindow(frame = UIScreen.mainScreen.bounds) window = UIWindow(frame = UIScreen.mainScreen.bounds)
window!!.rootViewController = SkikoViewController().apply { window!!.rootViewController = SkikoViewController().apply {
setAppFactory { layer -> setAppFactory { layer ->
GenericSkikoView(layer, makeApp()).also { GenericSkikoView(layer, makeApp(layer)).also {
layer.skikoView = it layer.skikoView = it
} }
} }
......
...@@ -7,7 +7,7 @@ import javax.swing.* ...@@ -7,7 +7,7 @@ import javax.swing.*
fun main() { fun main() {
val skiaLayer = SkiaLayer() val skiaLayer = SkiaLayer()
skiaLayer.addView( GenericSkikoView(skiaLayer, Clocks())) skiaLayer.addView( GenericSkikoView(skiaLayer, Clocks(skiaLayer)))
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
val window = JFrame("Skiko example").apply { val window = JFrame("Skiko example").apply {
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
......
...@@ -8,7 +8,7 @@ import kotlinx.cinterop.* ...@@ -8,7 +8,7 @@ import kotlinx.cinterop.*
import platform.Foundation.NSMakeRect import platform.Foundation.NSMakeRect
import platform.darwin.NSObject import platform.darwin.NSObject
fun makeApp() = Clocks() fun makeApp(skiaLayer: SkiaLayer) = Clocks(skiaLayer)
fun main() { fun main() {
val app = NSApplication.sharedApplication() val app = NSApplication.sharedApplication()
...@@ -27,7 +27,7 @@ fun main() { ...@@ -27,7 +27,7 @@ fun main() {
backing = NSBackingStoreBuffered, backing = NSBackingStoreBuffered,
defer = true) defer = true)
val skiaLayer = SkiaLayer() val skiaLayer = SkiaLayer()
skiaLayer.skikoView = GenericSkikoView(skiaLayer, makeApp()) skiaLayer.skikoView = GenericSkikoView(skiaLayer, makeApp(skiaLayer))
skiaLayer.attachTo(window) skiaLayer.attachTo(window)
window.orderFrontRegardless() window.orderFrontRegardless()
app.run() app.run()
......
package org.jetbrains.skiko
val currentSystemTheme: SystemTheme
// TODO: getting actual OS system theme
get() = SystemTheme.LIGHT
\ No newline at end of file
...@@ -9,7 +9,6 @@ import org.jetbrains.skia.BackendRenderTarget ...@@ -9,7 +9,6 @@ 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.context.MetalContextHandler import org.jetbrains.skiko.context.MetalContextHandler
import org.jetbrains.skiko.context.OpenGLContextHandler
import javax.swing.SwingUtilities.* import javax.swing.SwingUtilities.*
internal class MetalRedrawer( internal class MetalRedrawer(
......
package org.jetbrains.skiko
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.MacOsMetalRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
internal fun createNativeRedrawer(
layer: SkiaLayer,
renderApi: GraphicsApi
): Redrawer = when (renderApi) {
GraphicsApi.OPENGL -> MacOsOpenGLRedrawer(layer)
GraphicsApi.METAL -> MacOsMetalRedrawer(layer)
else -> throw IllegalArgumentException("Unsupported API $renderApi")
}
\ No newline at end of file
...@@ -2,7 +2,6 @@ package org.jetbrains.skiko ...@@ -2,7 +2,6 @@ package org.jetbrains.skiko
import kotlinx.cinterop.ObjCAction import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import org.jetbrains.skiko.context.*
import org.jetbrains.skia.* import org.jetbrains.skia.*
import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer import org.jetbrains.skiko.redrawer.MacOsOpenGLRedrawer
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
...@@ -15,7 +14,11 @@ import platform.Foundation.addObserver ...@@ -15,7 +14,11 @@ import platform.Foundation.addObserver
import platform.darwin.NSObject import platform.darwin.NSObject
import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectMake
actual open class SkiaLayer { actual open class SkiaLayer() {
fun isShowing(): Boolean {
return true
}
actual var renderApi: GraphicsApi = GraphicsApi.OPENGL actual var renderApi: GraphicsApi = GraphicsApi.OPENGL
actual val contentScale: Float actual val contentScale: Float
get() = if (this::nsView.isInitialized) nsView.window!!.backingScaleFactor.toFloat() else 1.0f get() = if (this::nsView.isInitialized) nsView.window!!.backingScaleFactor.toFloat() else 1.0f
...@@ -36,7 +39,7 @@ actual open class SkiaLayer { ...@@ -36,7 +39,7 @@ actual open class SkiaLayer {
actual var skikoView: SkikoView? = null actual var skikoView: SkikoView? = null
private var redrawer: Redrawer? = null internal var redrawer: Redrawer? = null
private var picture: PictureHolder? = null private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder() private val pictureRecorder = PictureRecorder()
...@@ -137,7 +140,7 @@ actual open class SkiaLayer { ...@@ -137,7 +140,7 @@ actual open class SkiaLayer {
} }
} }
window.contentView!!.addSubview(nsView) window.contentView!!.addSubview(nsView)
redrawer = MacOsOpenGLRedrawer(this).apply { redrawer = createNativeRedrawer(this, renderApi).apply {
syncSize() syncSize()
needRedraw() needRedraw()
} }
......
package org.jetbrains.skiko
import platform.Foundation.NSUserDefaults
val currentSystemTheme: SystemTheme
get() = when (NSUserDefaults.standardUserDefaults.stringForKey("AppleInterfaceStyle")) {
"Dark" -> SystemTheme.DARK
else -> SystemTheme.LIGHT
}
\ No newline at end of file
package org.jetbrains.skiko.context
import kotlinx.cinterop.useContents
import org.jetbrains.skia.ColorSpace
import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceColorFormat
import org.jetbrains.skia.SurfaceOrigin
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.redrawer.MacOsMetalRedrawer
internal class MacOsMetalContextHandler(layer: SkiaLayer) : ContextHandler(layer, layer::draw) {
val metalRedrawer: MacOsMetalRedrawer
get() = layer.redrawer!! as MacOsMetalRedrawer
override fun initContext(): Boolean {
try {
if (context == null) {
context = metalRedrawer.makeContext()
}
} catch (e: Exception) {
println("${e.message}\nFailed to create Skia Metal context!")
return false
}
return true
}
override fun initCanvas() {
val scale = layer.contentScale
val w = (layer.nsView.frame.useContents { size.width } * scale).toInt().coerceAtLeast(0)
val h = (layer.nsView.frame.useContents { size.height } * scale).toInt().coerceAtLeast(0)
renderTarget = metalRedrawer.makeRenderTarget(w, h)
surface = Surface.makeFromBackendRenderTarget(
context!!,
renderTarget!!,
SurfaceOrigin.TOP_LEFT,
SurfaceColorFormat.BGRA_8888,
ColorSpace.sRGB
)
canvas = surface!!.canvas
}
override fun flush() {
// TODO: maybe make flush async as in JVM version.
super.flush()
surface!!.flushAndSubmit()
metalRedrawer.finishFrame()
}
override fun rendererInfo(): String {
return "Native Metal: device ${metalRedrawer.device.name}"
}
}
...@@ -31,35 +31,41 @@ internal class MacOSOpenGLContextHandler(layer: SkiaLayer) : ContextHandler(laye ...@@ -31,35 +31,41 @@ internal class MacOSOpenGLContextHandler(layer: SkiaLayer) : ContextHandler(laye
return result return result
} }
var initedCanvas = false private var currentWidth = 0
private var currentHeight = 0
private fun isSizeChanged(width: Int, height: Int): Boolean {
if (width != currentWidth || height != currentHeight) {
currentWidth = width
currentHeight = height
return true
}
return false
}
override fun initCanvas() { override fun initCanvas() {
if (!initedCanvas) { val scale = layer.contentScale
val scale = layer.contentScale val w = (layer.nsView.frame.useContents { size.width } * scale).toInt().coerceAtLeast(0)
val w = (layer.nsView.frame.useContents { size.width } * scale).toInt().coerceAtLeast(0) val h = (layer.nsView.frame.useContents { size.height } * scale).toInt().coerceAtLeast(0)
val h = (layer.nsView.frame.useContents { size.height } * scale).toInt().coerceAtLeast(0) if (isSizeChanged(w, h)) {
val fbId = openglGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING.toUInt()) val fbId = openglGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING.toUInt())
renderTarget = BackendRenderTarget.makeGL( renderTarget = BackendRenderTarget.makeGL(
w, w,
h, h,
0, 0,
8, 8,
fbId.toInt(), fbId.toInt(),
FramebufferFormat.GR_GL_RGBA8 FramebufferFormat.GR_GL_RGBA8
) )
surface = Surface.makeFromBackendRenderTarget( surface = Surface.makeFromBackendRenderTarget(
context!!, context!!,
renderTarget!!, renderTarget!!,
SurfaceOrigin.BOTTOM_LEFT, SurfaceOrigin.BOTTOM_LEFT,
SurfaceColorFormat.RGBA_8888, SurfaceColorFormat.RGBA_8888,
ColorSpace.sRGB ColorSpace.sRGB
) )
canvas = surface?.canvas canvas = surface?.canvas
?: error("Could not obtain Canvas from Surface") ?: error("Could not obtain Canvas from Surface")
initedCanvas = true
} }
} }
} }
package org.jetbrains.skiko.redrawer
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.autoreleasepool
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.usePinned
import kotlinx.cinterop.useContents
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkikoDispatchers
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.context.MacOsMetalContextHandler
import platform.CoreGraphics.CGColorCreate
import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB
import platform.CoreGraphics.CGContextRef
import platform.Metal.MTLCreateSystemDefaultDevice
import platform.Metal.MTLDeviceProtocol
import platform.Metal.MTLPixelFormatBGRA8Unorm
import platform.QuartzCore.CAMetalDrawableProtocol
import platform.QuartzCore.CAMetalLayer
import platform.QuartzCore.CATransaction
import platform.QuartzCore.kCAGravityTopLeft
import platform.QuartzCore.kCALayerHeightSizable
import platform.QuartzCore.kCALayerWidthSizable
import kotlin.system.getTimeNanos
import platform.CoreGraphics.CGSizeMake
internal class MacOsMetalRedrawer(
private val skiaLayer: SkiaLayer
) : Redrawer {
private val contextHandler = MacOsMetalContextHandler(skiaLayer)
override val renderInfo: String get() = contextHandler.rendererInfo()
private var isDisposed = false
internal val device = MTLCreateSystemDefaultDevice()!!
private val queue = device.newCommandQueue()!!
private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer()
init {
metalLayer.init(skiaLayer, contextHandler, device)
}
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
if (skiaLayer.isShowing()) {
draw()
}
}
fun makeContext() = DirectContext.makeMetal(device.objcPtr(), queue.objcPtr())
fun makeRenderTarget(width: Int, height: Int): BackendRenderTarget {
currentDrawable = metalLayer.nextDrawable()!!
return BackendRenderTarget.makeMetal(width, height, currentDrawable!!.texture.objcPtr())
}
override fun dispose() {
if (!isDisposed) {
metalLayer.dispose()
isDisposed = true
}
}
override fun syncSize() {
syncContentScale()
val osView = skiaLayer.nsView!!
val (w, h) = osView.frame.useContents {
size.width to size.height
}
CATransaction.begin()
CATransaction.setDisableActions(true)
metalLayer.frame = osView.frame
metalLayer.init(skiaLayer, contextHandler, device)
metalLayer.drawableSize = CGSizeMake(w * metalLayer.contentsScale, h * metalLayer.contentsScale)
CATransaction.commit()
CATransaction.flush()
}
private fun syncContentScale() {
CATransaction.begin()
CATransaction.setDisableActions(true)
metalLayer.contentsScale = skiaLayer.nsView.window!!.backingScaleFactor
CATransaction.commit()
CATransaction.flush()
}
override fun needRedraw() {
check(!isDisposed) { "MetalRedrawer is disposed" }
frameDispatcher.scheduleFrame()
}
override fun redrawImmediately() {
check(!isDisposed) { "MetalRedrawer is disposed" }
draw()
}
private fun draw() {
// TODO: maybe make flush async as in JVM version.
autoreleasepool {
if (!isDisposed) {
skiaLayer.update(getTimeNanos())
contextHandler.draw()
}
}
}
fun finishFrame() {
autoreleasepool {
currentDrawable?.let {
val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"
commandBuffer.presentDrawable(it)
commandBuffer.commit()
currentDrawable = null
}
}
}
}
internal class MetalLayer : CAMetalLayer {
private lateinit var skiaLayer: SkiaLayer
private lateinit var contextHandler: ContextHandler
@OverrideInit
constructor(): super()
@OverrideInit
constructor(layer: Any): super(layer)
fun init(
layer: SkiaLayer,
contextHandler: ContextHandler,
theDevice: MTLDeviceProtocol
) {
this.skiaLayer = layer
this.contextHandler = contextHandler
this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations()
this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable )
this.device = theDevice as objcnames.protocols.MTLDeviceProtocol?
this.pixelFormat = MTLPixelFormatBGRA8Unorm
this.opaque = false
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned {
this.backgroundColor =
CGColorCreate(CGColorSpaceCreateDeviceRGB(), it.addressOf(0))
}
this.displaySyncEnabled = false
skiaLayer.nsView.layer = this
skiaLayer.nsView.wantsLayer = true
this.contentsGravity = kCAGravityTopLeft;
}
fun dispose() {
this.removeFromSuperlayer()
}
override fun drawInContext(ctx: CGContextRef?) {
skiaLayer.update(getTimeNanos())
contextHandler.draw()
super.drawInContext(ctx)
}
}
...@@ -48,7 +48,6 @@ internal class MacOsOpenGLRedrawer( ...@@ -48,7 +48,6 @@ internal class MacOsOpenGLRedrawer(
size.height.toInt().coerceAtLeast(0) size.height.toInt().coerceAtLeast(0)
) )
} }
contextHandler.initedCanvas = false
} }
private fun syncContentScale() { private fun syncContentScale() {
...@@ -70,21 +69,22 @@ internal class MacOsOpenGLRedrawer( ...@@ -70,21 +69,22 @@ internal class MacOsOpenGLRedrawer(
} }
internal class MacosGLLayer : CAOpenGLLayer { internal class MacosGLLayer : CAOpenGLLayer {
private lateinit var layer: SkiaLayer private lateinit var skiaLayer: SkiaLayer
private lateinit var contextHandler: ContextHandler private lateinit var contextHandler: ContextHandler
@OverrideInit @OverrideInit
constructor(): super() constructor(): super()
@OverrideInit @OverrideInit
constructor(layer: Any): super(layer) constructor(layer: Any): super(layer)
fun init(layer: SkiaLayer, contextHandler: ContextHandler) { fun init(layer: SkiaLayer, contextHandler: ContextHandler) {
this.layer = layer skiaLayer = layer
this.contextHandler = contextHandler this.contextHandler = contextHandler
this.setNeedsDisplayOnBoundsChange(true) this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations() this.removeAllAnimations()
this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable ) this.setAutoresizingMask(kCALayerWidthSizable or kCALayerHeightSizable )
layer.nsView.layer = this skiaLayer.nsView.layer = this
layer.nsView.wantsLayer = true skiaLayer.nsView.wantsLayer = true
this.contentsGravity = kCAGravityTopLeft; this.contentsGravity = kCAGravityTopLeft;
} }
...@@ -118,7 +118,7 @@ internal class MacosGLLayer : CAOpenGLLayer { ...@@ -118,7 +118,7 @@ internal class MacosGLLayer : CAOpenGLLayer {
) { ) {
CGLSetCurrentContext(ctx); CGLSetCurrentContext(ctx);
try { try {
layer.update(getTimeNanos()) skiaLayer.update(getTimeNanos())
contextHandler.draw() contextHandler.draw()
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTrace() e.printStackTrace()
......
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