Unverified Commit 73c0bbc5 authored by Nikolay Igotti's avatar Nikolay Igotti Committed by GitHub

Basic iOS Metal-based renderer (#260)

parent 920a24cf
...@@ -6,7 +6,7 @@ val coroutinesVersion = "1.5.2" ...@@ -6,7 +6,7 @@ val coroutinesVersion = "1.5.2"
repositories { repositories {
mavenLocal() mavenLocal()
jcenter() mavenCentral()
maven("https://dl.bintray.com/kotlin/kotlin-eap") maven("https://dl.bintray.com/kotlin/kotlin-eap")
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
} }
...@@ -56,6 +56,7 @@ kotlin { ...@@ -56,6 +56,7 @@ kotlin {
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-linker-option", "-framework", "-linker-option", "Metal", "-linker-option", "-framework", "-linker-option", "Metal",
"-linker-option", "-framework", "-linker-option", "CoreText", "-linker-option", "-framework", "-linker-option", "CoreText",
"-linker-option", "-framework", "-linker-option", "CoreGraphics"
) )
} }
} }
...@@ -126,6 +127,19 @@ project.tasks.register<Exec>("runIosSim") { ...@@ -126,6 +127,19 @@ project.tasks.register<Exec>("runIosSim") {
} }
} }
project.tasks.register<Exec>("run") {
workingDir = project.buildDir
val binTask = project.tasks.named("linkDebugExecutable${hostOs.capitalize()}${hostArch.capitalize()}")
dependsOn(binTask)
// Hacky approach.
commandLine = listOf("bash", "-c")
argumentProviders.add {
val out = fileTree(binTask.get().outputs.files.files.single()) { include("*.kexe") }
println("Run $out")
listOf(out.single { it.name.endsWith(".kexe") }.absolutePath)
}
}
// Create Xcode integration tasks. // Create Xcode integration tasks.
val sdkName: String? = System.getenv("SDK_NAME") val sdkName: String? = System.getenv("SDK_NAME")
......
kotlin.code.style=official kotlin.code.style=official
kotlin.native.cacheKind.macosX64=none kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
...@@ -3,13 +3,12 @@ package org.jetbrains.skiko.sample ...@@ -3,13 +3,12 @@ package org.jetbrains.skiko.sample
import org.jetbrains.skia.* import org.jetbrains.skia.*
fun displayScene(canvas: Canvas, nanoTime: Long) { fun displayScene(canvas: Canvas, nanoTime: Long) {
val angleDeg = (nanoTime / 5_000_000) % 360
val paint = Paint().apply { color = Color.GREEN } val paint = Paint().apply { color = Color.GREEN }
canvas.clear(Color.RED) canvas.clear(Color.RED)
canvas.save(); canvas.save();
canvas.translate(128.0f, 128.0f) canvas.translate(128.0f, 128.0f)
canvas.rotate(nanoTime.toFloat() / 1e7f) canvas.rotate(angleDeg.toFloat())
val rect = Rect.makeXYWH(-90.5f, -90.5f, 181.0f, 181.0f) val rect = Rect.makeXYWH(-90.5f, -90.5f, 181.0f, 181.0f)
canvas.drawRect(rect, paint) canvas.drawRect(rect, paint)
canvas.restore(); canvas.restore();
......
// Use `xcodegen` first, then `open ./SkikoSample.xcodeproj` and then Run button in XCode. // Use `xcodegen` first, then `open ./SkikoSample.xcodeproj` and then Run button in XCode.
package org.jetbrains.skiko.sample package org.jetbrains.skiko.sample
import org.jetbrains.skia.*
import org.jetbrains.skiko.*
import kotlinx.cinterop.* import kotlinx.cinterop.*
import platform.UIKit.* import platform.UIKit.*
import platform.Foundation.* import platform.Foundation.*
fun main() { fun main() {
val paint = Paint().apply { color = Color.GREEN } val args = emptyArray<String>()
println("Paint is $paint")
runSkikoMain()
}
fun runSkikoMain(args: Array<String> = emptyArray()) {
memScoped { memScoped {
val argc = args.size + 1 val argc = args.size + 1
val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues() val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues()
autoreleasepool { autoreleasepool {
UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate)) UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate))
} }
......
package org.jetbrains.skiko.sample package org.jetbrains.skiko.sample
import kotlinx.cinterop.* import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.* import platform.UIKit.*
class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol { class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta {} companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta
@ObjCObjectBase.OverrideInit @ObjCObjectBase.OverrideInit
constructor() : super() constructor() : super()
......
package org.jetbrains.skiko.sample package org.jetbrains.skiko.sample
import kotlinx.cinterop.* import kotlinx.cinterop.*
import kotlinx.coroutines.runBlocking
import org.jetbrains.skiko.GenericRenderer
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectMake
import platform.Foundation.* import platform.Foundation.*
import platform.UIKit.* import platform.UIKit.*
import platform.Foundation.NSCoder import platform.Foundation.NSCoder
...@@ -15,26 +19,18 @@ class SkikoViewController : UIViewController { ...@@ -15,26 +19,18 @@ class SkikoViewController : UIViewController {
@OverrideInit @OverrideInit
constructor(coder: NSCoder) : super(coder) constructor(coder: NSCoder) : super(coder)
override fun viewDidLoad() { override fun viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
val (width, height) = UIScreen.mainScreen.bounds.useContents { val (width, height) = UIScreen.mainScreen.bounds.useContents {
this.size.width to this.size.height this.size.width to this.size.height
} }
view.setFrame(CGRectMake(0.0, 0.0, width, height))
val layer = SkiaLayer(width.toInt(), height.toInt()) val layer = SkiaLayer(width.toFloat(), height.toFloat())
layer.renderer = GenericRenderer(layer) { canvas, w, h, nanoTime ->
layer.initLayer() displayScene(canvas, nanoTime)
layer.view = UIView().apply {
backgroundColor = UIColor.lightGrayColor
view.addSubview(this)
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
widthAnchor.constraintEqualToAnchor(view.widthAnchor).active = true
heightAnchor.constraintEqualToAnchor(view.heightAnchor).active = true
} }
layer.initLayer(view)
} }
} }
...@@ -320,6 +320,7 @@ kotlin { ...@@ -320,6 +320,7 @@ kotlin {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation(kotlin("stdlib-common")) implementation(kotlin("stdlib-common"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
} }
} }
val commonTest by getting { val commonTest by getting {
......
...@@ -56,7 +56,7 @@ class FrameDispatcher( ...@@ -56,7 +56,7 @@ class FrameDispatcher(
fun scheduleFrame() { fun scheduleFrame() {
if (!frameScheduled) { if (!frameScheduled) {
frameScheduled = true frameScheduled = true
frameChannel.offer(Unit) frameChannel.trySend(Unit).isSuccess
} }
} }
} }
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skiko.context.ContextHandler import org.jetbrains.skiko.context.ContextHandler
import org.jetbrains.skiko.redrawer.OpenGLRedrawer import org.jetbrains.skiko.redrawer.MetalRedrawer
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.redrawer.Redrawer
internal actual fun createNativeContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler { internal actual fun createNativeContextHandler(layer: SkiaLayer, renderApi: GraphicsApi): ContextHandler {
...@@ -13,5 +13,5 @@ internal actual fun createNativeRedrawer( ...@@ -13,5 +13,5 @@ internal actual fun createNativeRedrawer(
renderApi: GraphicsApi, renderApi: GraphicsApi,
properties: SkiaLayerProperties properties: SkiaLayerProperties
): Redrawer { ): Redrawer {
return OpenGLRedrawer(layer, properties) return MetalRedrawer(layer, properties)
} }
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.cinterop.interpretCPointer
import kotlinx.coroutines.*
import platform.CoreFoundation.*
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_global_queue
import platform.darwin.dispatch_get_main_queue
import platform.darwin.dispatch_queue_t
import platform.posix.QOS_CLASS_BACKGROUND
import kotlin.coroutines.CoroutineContext
import kotlin.native.concurrent.AtomicNativePtr
import kotlin.native.concurrent.freeze
import kotlin.native.internal.NativePtr
// This is the only dispatcher that shall be used in Skiko on iOS.
// Current (as of 1.5.2) dispatchers in kotlinx.coroutines are not usable
// for needs of Skiko.
object SkikoDispatchers {
val Main: CoroutineDispatcher = NsQueueDispatcher(dispatch_get_main_queue())
val IO: CoroutineDispatcher = NsQueueDispatcher(
dispatch_get_global_queue(
QOS_CLASS_BACKGROUND.toLong(), 0)
)
}
@OptIn(InternalCoroutinesApi::class)
internal class NsQueueDispatcher(
private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher(), Delay {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue) {
block.run()
}
}
@OptIn(InternalCoroutinesApi::class)
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val timer = Timer()
val timerBlock: TimerBlock = {
timer.dispose()
continuation.resume(Unit) { t -> t.printStackTrace() }
}
timer.start(timeMillis, timerBlock)
continuation.disposeOnCancellation(timer)
}
@OptIn(InternalCoroutinesApi::class)
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
val timer = Timer()
val timerBlock: TimerBlock = {
timer.dispose()
block.run()
}
timer.start(timeMillis, timerBlock)
return timer
}
}
internal typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit
@SharedImmutable
private val TIMER_NEW = NativePtr.NULL
@SharedImmutable
private val TIMER_DISPOSED = NativePtr.NULL.plus(1)
private class Timer : DisposableHandle {
private val ref = AtomicNativePtr(TIMER_NEW)
init { freeze() }
fun start(timeMillis: Long, timerBlock: TimerBlock) {
val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0
@Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock)!!
CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
if (!ref.compareAndSet(TIMER_NEW, timer.rawValue)) {
// dispose was already called concurrently
release(timer)
}
}
override fun dispose() {
while (true) {
val ptr = ref.value
if (ptr == TIMER_DISPOSED) return
if (ref.compareAndSet(ptr, TIMER_DISPOSED)) {
if (ptr != TIMER_NEW) release(interpretCPointer(ptr))
return
}
}
}
private fun release(timer: CFRunLoopTimerRef?) {
CFRunLoopRemoveTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
CFRelease(timer)
}
}
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.cinterop.*
import org.jetbrains.skia.PictureRecorder import org.jetbrains.skia.PictureRecorder
import org.jetbrains.skia.Rect import org.jetbrains.skia.Rect
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skiko.context.MetalContextHandler
import platform.Foundation.* import org.jetbrains.skiko.redrawer.MetalRedrawer
import platform.UIKit.* import platform.UIKit.*
actual open class SkiaLayer actual constructor( actual open class SkiaLayer actual constructor(
properties: SkiaLayerProperties val properties: SkiaLayerProperties
) { ) {
var width: Int = 0 var width: Float = 0f
var height: Int = 0 var height: Float = 0f
constructor(width: Int, height: Int) : this( constructor(width: Float, height: Float) : this(
makeDefaultSkiaLayerProperties()) { makeDefaultSkiaLayerProperties()) {
this.width = width this.width = width
this.height = height this.height = height
} }
fun isShowing(): Boolean {
return true
}
actual var renderApi: GraphicsApi actual var renderApi: GraphicsApi
get() = GraphicsApi.METAL get() = GraphicsApi.METAL
set(value) { throw UnsupportedOperationException() } set(value) { throw UnsupportedOperationException() }
...@@ -40,31 +43,43 @@ actual open class SkiaLayer actual constructor( ...@@ -40,31 +43,43 @@ actual open class SkiaLayer actual constructor(
lateinit var view: UIView lateinit var view: UIView
fun initLayer() { fun initLayer(view: UIView) {
println("SkiaLayer.initLayer") println("SkiaLayer.initLayer")
//redrawer = createNativeRedrawer(this, renderApi, properties) this.view = view
redrawer = MetalRedrawer(this, properties)
redrawer?.redrawImmediately() redrawer?.redrawImmediately()
} }
var renderer: SkiaRenderer? = null var renderer: SkiaRenderer? = null
private var redrawer: Redrawer? = null internal var redrawer: MetalRedrawer? = null
private var picture: PictureHolder? = null private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder() private val pictureRecorder = PictureRecorder()
private val contextHandler = MetalContextHandler(this)
fun update(nanoTime: Long) { fun update(nanoTime: Long) {
println("SkiaLayer.update")
val width = 800f
val height = 600f
val pictureWidth = (width * contentScale).coerceAtLeast(0.0F) val pictureWidth = (width * contentScale).coerceAtLeast(0.0F)
val pictureHeight = (height * contentScale).coerceAtLeast(0.0F) val pictureHeight = (height * contentScale).coerceAtLeast(0.0F)
val bounds = Rect.makeWH(pictureWidth, pictureHeight) val bounds = Rect.makeWH(pictureWidth, pictureHeight)
val canvas = pictureRecorder.beginRecording(bounds) val canvas = pictureRecorder.beginRecording(bounds)
renderer?.onRender(canvas, pictureWidth.toInt(), pictureHeight.toInt(), nanoTime) renderer?.onRender(canvas, pictureWidth.toInt(), pictureHeight.toInt(), nanoTime)
val picture = pictureRecorder.finishRecordingAsPicture() val picture = pictureRecorder.finishRecordingAsPicture()
this.picture = PictureHolder(picture, pictureWidth.toInt(), pictureHeight.toInt()) this.picture = PictureHolder(picture, pictureWidth.toInt(), pictureHeight.toInt())
} }
fun draw() {
contextHandler.apply {
if (!initContext()) {
error("initContext() failure")
return
}
initCanvas()
clearCanvas()
val picture = picture
if (picture != null) {
drawOnCanvas(picture.instance)
}
flush()
}
}
} }
package org.jetbrains.skiko.context
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.MetalRedrawer
internal class MetalContextHandler(layer: SkiaLayer) : ContextHandler(layer) {
val metalRedrawer: MetalRedrawer
get() = layer.redrawer!!
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() {
disposeCanvas()
val scale = layer.contentScale
val w = (layer.width * scale).toInt().coerceAtLeast(0)
val h = (layer.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}"
}
}
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import org.jetbrains.skiko.SkiaLayer import kotlinx.cinterop.addressOf
import org.jetbrains.skiko.SkiaLayerProperties import kotlinx.cinterop.autoreleasepool
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.usePinned
import org.jetbrains.skia.BackendRenderTarget
import org.jetbrains.skia.DirectContext
import org.jetbrains.skiko.*
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.kCAGravityTopLeft
import kotlin.system.getTimeNanos
internal class MetalRedrawer( internal class MetalRedrawer(
private val layer: SkiaLayer, private val layer: SkiaLayer,
private val properties: SkiaLayerProperties private val properties: SkiaLayerProperties
) : Redrawer { ) : Redrawer {
private var isDisposed = false
internal val device = MTLCreateSystemDefaultDevice()!!
private val queue = device.newCommandQueue()!!
private var currentDrawable: CAMetalDrawableProtocol? = null
private val metalLayer = MetalLayer(this.layer, device)
private val frameDispatcher = FrameDispatcher(SkikoDispatchers.Main) {
if (layer.isShowing()) {
update(getTimeNanos())
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() { override fun dispose() {
TODO() frameDispatcher.cancel()
isDisposed = true
} }
override fun syncSize() { override fun syncSize() {
TODO() println("TODO: implement syncSize()")
} }
override fun needRedraw() { override fun needRedraw() {
TODO() check(!isDisposed) { "MetalRedrawer is disposed" }
frameDispatcher.scheduleFrame()
} }
override fun redrawImmediately() { override fun redrawImmediately() {
TODO() check(!isDisposed) { "MetalRedrawer is disposed" }
update(getTimeNanos())
draw()
}
private fun update(nanoTime: Long) {
layer.update(nanoTime)
}
private fun draw() {
// TODO: maybe make flush async as in JVM version.
autoreleasepool {
if (!isDisposed) {
layer.draw()
}
}
}
fun finishFrame() {
autoreleasepool {
currentDrawable?.let {
val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"
commandBuffer.presentDrawable(it)
commandBuffer.commit()
currentDrawable = null
}
}
}
}
class MetalLayer(
private val skiaLayer: SkiaLayer,
theDevice: MTLDeviceProtocol
) : CAMetalLayer() {
init {
this.setNeedsDisplayOnBoundsChange(true)
this.removeAllAnimations()
// TODO: looks like a bug in K/N interop.
this.device = theDevice as objcnames.protocols.MTLDeviceProtocol?
this.pixelFormat = MTLPixelFormatBGRA8Unorm
this.contentsGravity = kCAGravityTopLeft
doubleArrayOf(0.0, 0.0, 0.0, 0.0).usePinned {
this.backgroundColor =
CGColorCreate(CGColorSpaceCreateDeviceRGB(), it.addressOf(0))
}
this.opaque = true
this.frame = skiaLayer.view.frame
skiaLayer.view.layer.addSublayer(this)
}
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()
super.drawInContext(ctx)
} }
} }
package org.jetbrains.skiko.redrawer
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import kotlin.system.getTimeNanos
internal class OpenGLRedrawer(
private val layer: SkiaLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
override fun dispose() {
println("dispose")
}
override fun syncSize() {
println("syncSize")
}
override fun needRedraw() {
println("needRedraw")
}
override fun redrawImmediately() {
layer.update(getTimeNanos())
}
}
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