Unverified Commit 8cde16e1 authored by Nikolay Igotti's avatar Nikolay Igotti Committed by GitHub

iOS cross-compilation support (#236)

parent d0a425f4
......@@ -12,7 +12,7 @@ repositories {
}
val osName = System.getProperty("os.name")
val targetOs = when {
val hostOs = when {
osName == "Mac OS X" -> "macos"
osName.startsWith("Win") -> "windows"
osName.startsWith("Linux") -> "linux"
......@@ -20,13 +20,13 @@ val targetOs = when {
}
val osArch = System.getProperty("os.arch")
var targetArch = when (osArch) {
var hostArch = when (osArch) {
"x86_64", "amd64" -> "x64"
"aarch64" -> "arm64"
else -> error("Unsupported arch: $osArch")
}
val target = "${targetOs}-${targetArch}"
val host = "${hostOs}-${hostArch}"
var version = "0.0.0-SNAPSHOT"
if (project.hasProperty("skiko.version")) {
......@@ -34,17 +34,27 @@ if (project.hasProperty("skiko.version")) {
}
kotlin {
val nativeTarget = when (target) {
val targets = mutableListOf<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget>()
val nativeHostTarget = when (host) {
"macos-x64" -> macosX64()
"macos-arm64" -> macosArm64()
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
else -> throw GradleException("Host OS is not supported yet")
}
nativeTarget.apply {
binaries {
executable {
entryPoint = "org.jetbrains.skiko.sample.native.main"
freeCompilerArgs += listOf("-linker-options", "-framework", "-linker-option", "Metal")
targets.add(nativeHostTarget)
if (hostOs == "macos") {
targets.add(iosX64())
targets.add(iosArm64())
}
targets.forEach {
it.apply {
binaries {
executable {
entryPoint = "org.jetbrains.skiko.sample.main"
freeCompilerArgs += listOf("-linker-options", "-framework", "-linker-option", "Metal")
}
}
}
}
......@@ -61,20 +71,54 @@ kotlin {
dependsOn(commonMain)
}
val archTargetMain = when (target) {
val macosMain by creating {
dependsOn(nativeMain)
}
val archTargetMain = when (host) {
"macos-x64" -> {
val macosX64Main by getting {
dependsOn(nativeMain)
dependsOn(macosMain)
}
macosX64Main
}
"macos-arm64" -> {
val macosArm64Main by getting {
dependsOn(nativeMain)
dependsOn(macosMain)
}
macosArm64Main
}
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
if (hostOs == "macos") {
val iosMain by creating {
dependsOn(nativeMain)
}
val iosX64Main by getting {
dependsOn(iosMain)
}
val iosArm64Main by getting {
dependsOn(iosMain)
}
}
}
}
project.tasks.register<Exec>("runIosSim") {
val device = "iPhone 11"
workingDir = project.buildDir
val binTask = project.tasks.named("linkReleaseExecutableIosX64")
dependsOn(binTask)
commandLine = listOf(
"xcrun",
"simctl",
"spawn",
"--standalone",
device
)
argumentProviders.add {
val out = fileTree(binTask.get().outputs.files.files.single()) { include("*.kexe") }
listOf(out.single { it.name.endsWith(".kexe") }.absolutePath)
}
}
package org.jetbrains.skiko.sample
import org.jetbrains.skia.*
fun main() {
val paint = Paint().apply { color = Color.GREEN }
println("HI: $paint")
}
package org.jetbrains.skiko.sample.native
package org.jetbrains.skiko.sample
import platform.AppKit.*
......@@ -25,7 +25,7 @@ fun createWindow(title: String) {
var angle = 0.0f
window.layer.renderer = Renderer(window.layer) {
renderer, w, h, nanoTime -> displayScene(renderer, nanoTime)
renderer, w, h, nanoTime -> displayScene(renderer.canvas!!, nanoTime)
}
window.nsWindow.orderFrontRegardless()
......@@ -46,18 +46,3 @@ class Renderer(
}
}
fun displayScene(renderer: Renderer, nanoTime: Long) {
val canvas = renderer.canvas!!
val paint = Paint().apply { color = Color.GREEN}
canvas.clear(Color.RED)
canvas.save();
canvas.translate(128.0f, 128.0f)
canvas.rotate(nanoTime.toFloat() / 1e7f)
val rect = Rect.makeXYWH(-90.5f, -90.5f, 181.0f, 181.0f)
canvas.drawRect(rect, paint)
canvas.restore();
}
package org.jetbrains.skiko.sample
import org.jetbrains.skia.*
fun displayScene(canvas: Canvas, nanoTime: Long) {
val paint = Paint().apply { color = Color.GREEN }
canvas.clear(Color.RED)
canvas.save();
canvas.translate(128.0f, 128.0f)
canvas.rotate(nanoTime.toFloat() / 1e7f)
val rect = Rect.makeXYWH(-90.5f, -90.5f, 181.0f, 181.0f)
canvas.drawRect(rect, paint)
canvas.restore();
}
......@@ -54,9 +54,19 @@ val skiaZip = run {
}
val crossTargets = listOf(
OS.Wasm to Arch.Wasm
OS.Wasm to Arch.Wasm,
OS.IOS to Arch.X64,
OS.IOS to Arch.Arm64,
OS.MacOS to Arch.X64,
OS.MacOS to Arch.Arm64,
)
class NativeCompilationInfo(val target: org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget) {
var linkTask: Provider<Exec>? = null
}
val allNativeTargets = mutableMapOf<Pair<OS, Arch>, NativeCompilationInfo>()
val skiaCrossDownloadTasks: Map<Pair<OS, Arch>, Provider<File>> = crossTargets.keysToMap { osArch ->
val suffix = targetSuffix(osArch.first, osArch.second)
val out = skiko.dependenciesDir.resolve("skia/skia-$suffix.zip")
......@@ -122,6 +132,76 @@ val wasmCrossCompile = tasks.register<CrossCompileTask>("wasmCrossCompile") {
))
}
fun registerNativeBridgesTask(os: OS, arch: Arch): TaskProvider<CrossCompileTask> {
return tasks.register<CrossCompileTask>("${os.id}_${arch.id}_CrossCompile") {
val osArch = os to arch
val unzipper = skiaDirProviderForCrossTargets[osArch]!!
dependsOn(unzipper)
val unpackedSkia = unzipper.get()
compiler.set("clang++")
when (os) {
OS.IOS -> {
val sdkRoot = "/Applications/Xcode.app/Contents/Developer/Platforms"
val iosFlags = listOf(
"-std=c++17",
"-stdlib=libc++",
"-DSK_SHAPER_CORETEXT_AVAILABLE",
"-DSK_BUILD_FOR_IOS",
"-DSK_METAL",
"-DSK_SUPPORT_GPU=1"
)
when (arch) {
Arch.Arm64 ->
flags.set(
listOf(
"-target", "arm64-apple-ios",
"-isysroot", "$sdkRoot/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"
) +
iosFlags +
skiaPreprocessorFlags()
)
Arch.X64 -> flags.set(
listOf(
"-target", "x86_64-apple-ios-simulator",
"-isysroot", "$sdkRoot/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk")
+ iosFlags
+ skiaPreprocessorFlags()
)
else -> throw GradleException("Unsupported arch: $arch")
}
}
OS.MacOS -> {
flags.set(listOf(
"-std=c++17",
"-DSK_SHAPER_CORETEXT_AVAILABLE",
"-DSK_BUILD_FOR_MAC",
"-DSK_METAL",
"-DSK_SUPPORT_GPU=1",
*skiaPreprocessorFlags()
))
}
}
crossCompileTargetOS.set(osArch.first)
crossCompileTargetArch.set(osArch.second)
buildVariant.set(buildType)
sourceFiles =
project.fileTree("src/nativeMain/cpp") { include("**/*.cc") } +
project.fileTree("src/commonMain/cpp") { include("**/*.cc") }
if (skiko.includeTestHelpers) {
sourceFiles += project.fileTree("src/commonTest/cpp") { include("**/*.cc") }
}
outDir.set(project.layout.buildDirectory.dir("out/compile/${buildType.id}-${osArch.first.id}-${osArch.second.id}"))
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp"))
includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia))
}
}
val linkWasm = tasks.register<LinkWasmTask>("linkWasm") {
val osArch = OS.Wasm to Arch.Wasm
......@@ -186,6 +266,7 @@ val Project.supportNative: Boolean
val Project.supportWasm: Boolean
get() = properties.get("skiko.wasm.enabled") == "true"
kotlin {
jvm {
compilations.all {
......@@ -207,28 +288,64 @@ kotlin {
}
if (supportNative) {
val skiaDir = skiaDir.get().absolutePath
val nativeTarget = when (target) {
"macos-x64" -> macosX64 {}
"macos-arm64" -> macosArm64 {}
else -> null
}
val targetString = target
nativeTarget?.let {
it.compilations.all {
when ("${hostOs.id}-${hostArch.id}") {
"macos-x64" -> allNativeTargets[OS.MacOS to Arch.X64] = NativeCompilationInfo(macosX64())
"macos-arm64" -> allNativeTargets[OS.MacOS to Arch.Arm64] = NativeCompilationInfo(macosArm64())
}
if (hostOs == OS.MacOS) {
allNativeTargets[OS.IOS to Arch.Arm64] = NativeCompilationInfo(iosArm64())
allNativeTargets[OS.IOS to Arch.X64] = NativeCompilationInfo(iosX64())
}
for ((osArch, compilation) in allNativeTargets) {
val targetString = "${osArch.first.id}-${osArch.second.id}"
val unzipper = skiaDirProviderForCrossTargets[osArch]!!
val unpackedSkia = unzipper.get()
val skiaDir = unpackedSkia.absolutePath
val skiaBinSubdir = "out/${buildType.id}-$targetString"
compilation.target.compilations.all {
kotlinOptions {
freeCompilerArgs += listOf(
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskia.a",
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskshaper.a",
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskparagraph.a",
"-include-binary",
"$buildDir/nativeBridges/static/$targetString/skiko-native-bridges-$targetString.a"
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskia.a",
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskshaper.a",
"-include-binary",
"$skiaDir/$skiaBinSubdir/libskparagraph.a",
"-include-binary",
"$buildDir/nativeBridges/static/$targetString/skiko-native-bridges-$targetString.a"
)
}
}
val crossCompileTask = registerNativeBridgesTask(osArch.first, osArch.second)
allNativeTargets[osArch]!!.linkTask = project.tasks.register<Exec>("linkNativeBridges$targetString") {
dependsOn(crossCompileTask)
val objectFilesDir = crossCompileTask.map { it.outDir.get() }
val objectFiles = project.fileTree(objectFilesDir) {
include("**/*.o")
}
inputs.files(objectFiles)
val outDir = "$buildDir/nativeBridges/static/$targetString"
val staticLib = "$outDir/skiko-native-bridges-$targetString.a"
workingDir = File(outDir)
executable = "libtool"
argumentProviders.add {
listOf(
"-static",
"-o",
staticLib
)
}
argumentProviders.add { objectFiles.files.map { it.absolutePath } }
file(outDir).mkdirs()
outputs.dir(outDir)
}
}
}
......@@ -275,10 +392,13 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
}
}
if (targetOs == OS.MacOS) {
if (hostOs == OS.MacOS) {
val macosMain by creating {
dependsOn(nativeMain)
}
val iosMain by creating {
dependsOn(nativeMain)
}
val macosArchMain = when (targetArch) {
Arch.X64 -> {
val macosX64Main by getting {
......@@ -294,6 +414,12 @@ kotlin {
}
else -> throw GradleException("Unsupported arch $targetArch for macOS")
}
val iosX64Main by getting {
dependsOn(iosMain)
}
val iosArm64Main by getting {
dependsOn(iosMain)
}
}
}
}
......@@ -412,7 +538,7 @@ tasks.withType(CppCompile::class.java).configureEach {
)
)
}
OS.Wasm -> throw GradleException("Should not reach here")
OS.Wasm, OS.IOS -> throw GradleException("Should not reach here")
}
}
......@@ -469,60 +595,6 @@ val generateVersion = project.tasks.register("generateVersion") {
}
}
// Very hacky way to compile native bridges and add the
// resulting object files into the final native klib.
val compileNativeBridges = project.tasks.register<CrossCompileTask>("compileNativeBridges") {
dependsOn(skiaDir)
compiler.set("clang++")
crossCompileTargetOS.set(targetOs)
crossCompileTargetArch.set(targetArch)
buildVariant.set(buildType)
sourceFiles =
project.fileTree("src/nativeMain/cpp") { include("**/*.cc") } +
project.fileTree("src/commonMain/cpp") { include("**/*.cc") }
if (skiko.includeTestHelpers) {
sourceFiles += project.fileTree("src/commonTest/cpp") { include("**/*.cc") }
}
outDir.set(project.layout.buildDirectory.dir("nativeBridges/obj/$target"))
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp"))
includeHeadersNonRecursive(skiaHeadersDirs(skiaDir.get()))
flags.set(listOf(
"-DSK_SHAPER_CORETEXT_AVAILABLE",
"-DSK_BUILD_FOR_MAC",
"-DSK_METAL",
*skiaPreprocessorFlags(),
))
}
val linkNativeBridges = project.tasks.register<Exec>("linkNativeBridges") {
dependsOn(compileNativeBridges)
val objectFilesDir = compileNativeBridges.map { it.outDir.get() }
val objectFiles = project.fileTree(objectFilesDir) {
include("**/*.o")
}
inputs.files(objectFiles)
val outDir = "$buildDir/nativeBridges/static/$target"
val staticLib = "$outDir/skiko-native-bridges-$target.a"
workingDir = File(outDir)
executable = "libtool"
argumentProviders.add {
listOf(
"-static",
"-o",
staticLib
)
}
argumentProviders.add { objectFiles.files.map { it.absolutePath } }
file(outDir).mkdirs()
outputs.dir(outDir)
}
fun localSign(signer: String, lib: File): File {
println("Local signing $lib as $signer")
val proc = ProcessBuilder("codesign", "-f", "-s", signer, lib.absolutePath)
......@@ -672,8 +744,8 @@ tasks.withType(LinkSharedLibrary::class.java).configureEach {
)
)
}
OS.Wasm -> {
throw GradleException("This task shalln't be used with WASM")
OS.Wasm, OS.IOS -> {
throw GradleException("This task shalln't be used with $targetOs")
}
}
}
......@@ -952,9 +1024,11 @@ afterEvaluate {
tasks.withType<KotlinNativeCompile>().configureEach {
dependsOn(generateVersion)
source(generatedKotlin)
// TDOD: do we need this 'if'?
if (supportNative) {
dependsOn(linkNativeBridges)
val compilationInfoKey = allNativeTargets.keys.singleOrNull { "${it.first.id}_${it.second.id}" == this.target }
?: throw GradleException("No cross-target for ${this.target}")
dependsOn(allNativeTargets[compilationInfoKey]!!.linkTask)
}
}
}
......
......@@ -8,7 +8,8 @@ enum class OS(
Linux("linux", arrayOf()),
Windows("windows", arrayOf()),
MacOS("macos", arrayOf("-mmacosx-version-min=10.13")),
Wasm("wasm", arrayOf())
Wasm("wasm", arrayOf()),
IOS("ios", arrayOf())
;
val isWindows
......
......@@ -8,5 +8,7 @@ dependencies.skia.macos-x64=m94-fd7c5792ca
dependencies.skia.linux-arm64=m94-fd7c5792ca
dependencies.skia.macos-arm64=m94-fd7c5792ca
dependencies.skia.wasm-wasm=m94-fd7c5792ca
dependencies.skia.ios-x64=m94-fd7c5792ca
dependencies.skia.ios-arm64=m94-fd7c5792ca
org.gradle.jvmargs=-Xmx3G
package org.jetbrains.skiko.native.context
import org.jetbrains.skia.*
import org.jetbrains.skiko.native.*
internal actual fun createContextHandler(layer: HardwareLayer): ContextHandler {
TODO()
}
package org.jetbrains.skiko.native
actual abstract class HardwareLayer
\ No newline at end of file
package org.jetbrains.skiko.native
import org.jetbrains.skiko.GraphicsApi
import org.jetbrains.skiko.redrawer.Redrawer
import org.jetbrains.skiko.native.redrawer.IosOpenGLRedrawer
internal actual fun makePlatformOperations(): PlatformOperations {
return object: PlatformOperations {
override fun createRedrawer(layer: HardwareLayer, properties: SkiaLayerProperties): Redrawer =
when (SkikoProperties.renderApi) {
GraphicsApi.OPENGL -> IosOpenGLRedrawer(layer, properties)
else -> error("No other renderings for native yet")
}
}
}
\ No newline at end of file
package org.jetbrains.skiko.native.redrawer
import org.jetbrains.skiko.native.*
import org.jetbrains.skiko.redrawer.Redrawer
internal class IosOpenGLRedrawer(
private val layer: HardwareLayer,
private val properties: SkiaLayerProperties
) : Redrawer {
override fun dispose() {
TODO()
}
override fun syncSize() {
TODO()
}
override fun needRedraw() {
TODO()
}
override fun redrawImmediately() {
TODO()
}
}
......@@ -3,7 +3,7 @@ package org.jetbrains.skia.impl
import org.jetbrains.skia.ExternalSymbolName
actual abstract class RefCnt : Managed {
actual protected constructor(ptr: NativePointer): super(ptr, _FinalizerHolder.PTR, false)
actual protected constructor(ptr: NativePointer): super(ptr, _FinalizerHolder.PTR, true)
actual protected constructor(ptr: NativePointer, allowClose: Boolean): super(ptr, _FinalizerHolder.PTR, allowClose)
val refCount: Int
......
......@@ -3,7 +3,7 @@ package org.jetbrains.skia.impl
import java.lang.ref.Reference
actual abstract class RefCnt : Managed {
protected actual constructor(ptr: NativePointer) : super(ptr, _FinalizerHolder.PTR)
protected actual constructor(ptr: NativePointer) : super(ptr, _FinalizerHolder.PTR, true)
protected actual constructor(ptr: NativePointer, allowClose: Boolean) : super(ptr, _FinalizerHolder.PTR, allowClose)
val refCount: Int
......
......@@ -4,11 +4,20 @@ import kotlinx.cinterop.*
import org.jetbrains.skia.*
import org.jetbrains.skiko.native.*
import platform.OpenGL.GL_DRAW_FRAMEBUFFER_BINDING
import platform.OpenGL.GL_RGBA8
import platform.OpenGL.glGetIntegerv
import platform.OpenGL.*
import platform.OpenGLCommon.GLenum
internal actual fun createContextHandler(layer: HardwareLayer): ContextHandler {
return OpenGLContextHandler(layer)
//return when (SkikoProperties.renderApi) {
// GraphicsApi.SOFTWARE -> SoftwareContextHandler(layer)
// GraphicsApi.OPENGL -> OpenGLContextHandler(layer)
// else -> TODO("Unsupported yet")
//}
}
internal class OpenGLContextHandler(layer: HardwareLayer) : ContextHandler(layer) {
override fun initContext(): Boolean {
println("OpenGLContextHandler::initContext")
......
package org.jetbrains.skiko.native
import platform.AppKit.*
import platform.Foundation.*
actual abstract class HardwareLayer {
val nsView = NSView(NSMakeRect(0.0, 0.0, 640.0, 480.0))
// getDpiScale is expensive operation on some platforms, so we cache it
private var _contentScale: Float? = null
private var isInit = false
fun checkIsShowing() {
if (!isInit && !nsView.hiddenOrHasHiddenAncestor) {
// _contentScale = getDpiScale()
_contentScale = 1.0f // TODO: what's the proper way here?
init()
isInit = true
}
}
open fun init() {
}
protected open fun nativeInit(platformInfo: Long) {
}
open fun dispose() {
}
protected open fun contentScaleChanged() = Unit
private fun checkContentScale() {
}
internal abstract fun update(nanoTime: Long)
internal abstract fun draw()
val contentScale: Float
get() = _contentScale!!
}
......@@ -7,7 +7,6 @@ import kotlin.native.concurrent.freeze
import kotlin.native.internal.createCleaner
private class FinalizationThunk(private val finalizer: NativePointer, val className: String, obj: NativePointer) {
private var obj = AtomicNativePtr(obj)
fun clean() {
......
......@@ -3,7 +3,7 @@ package org.jetbrains.skia.impl
import org.jetbrains.skia.ExternalSymbolName
actual abstract class RefCnt : Managed {
protected actual constructor(ptr: NativePointer) : super(ptr, _FinalizerHolder.PTR) {}
protected actual constructor(ptr: NativePointer) : super(ptr, _FinalizerHolder.PTR, true) {}
protected actual constructor(ptr: NativePointer, allowClose: Boolean) : super(ptr, _FinalizerHolder.PTR, allowClose)
val refCount: Int
......
package org.jetbrains.skiko.native.context
import kotlinx.cinterop.ptr
import org.jetbrains.skia.*
import org.jetbrains.skiko.native.*
internal fun createContextHandler(layer: HardwareLayer): ContextHandler {
return OpenGLContextHandler(layer)
//return when (SkikoProperties.renderApi) {
// GraphicsApi.SOFTWARE -> SoftwareContextHandler(layer)
// GraphicsApi.OPENGL -> OpenGLContextHandler(layer)
// else -> TODO("Unsupported yet")
//}
}
internal expect fun createContextHandler(layer: HardwareLayer): ContextHandler
internal abstract class ContextHandler(val layer: HardwareLayer) {
......
package org.jetbrains.skiko.native
import kotlinx.cinterop.readValue
import platform.Cocoa.*
import platform.AppKit.*
import platform.Foundation.*
import platform.CoreGraphics.*
abstract class HardwareLayer {
val nsView = NSView(NSMakeRect(0.0, 0.0, 640.0, 480.0))
// getDpiScale is expensive operation on some platforms, so we cache it
private var _contentScale: Float? = null
private var isInit = false
fun checkIsShowing() {
if (!isInit && !nsView.hiddenOrHasHiddenAncestor) {
// _contentScale = getDpiScale()
_contentScale = 1.0f // TODO: what's the proper way here?
init()
isInit = true
}
}
open fun init() {
}
protected open fun nativeInit(platformInfo: Long) {
}
open fun dispose() {
}
protected open fun contentScaleChanged() = Unit
private fun checkContentScale() {
}
internal abstract fun update(nanoTime: Long)
internal abstract fun draw()
val contentScale: Float
get() = _contentScale!!
}
expect abstract class HardwareLayer()
\ 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