Unverified Commit 723f52d3 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Revert "Revert "Pack ANGLE into a separate jar"" (#1115)

Reverts JetBrains/skiko#1114

Fixes https://youtrack.jetbrains.com/issue/SKIKO-1042/Support-ANGLE

The fix is the same as https://github.com/JetBrains/skiko/pull/1082,
except:
- the code is rearranged in new commits after resolving merge conflicts
(the merge is solved by applying their/ours)
- fixed "Task
':publishSkikoJvmRuntimeAngleWindowsX64PublicationToComposeRepoRepository'
uses this output of task ':signSkikoJvmRuntimeWindowsX64Publication'
without declaring an explicit or implicit dependency" (as in
https://github.com/JetBrains/skiko/pull/1127)

WIP: fix CI failure

## Testing
1. `./gradlew publishToMavenLocal`
2. In SkiaAwtSample:
```
./gradlew runWithAngleEnabled
```
3. [CI is
successful](https://buildserver.labs.intellij.net/buildConfiguration/Skiko_PublishSnapshot?branch=revert-1126-revert-1125-igor.demin%2Ffix-skiko-build-awtRuntimeElements&buildTypeTab=overview&mode=builds)
parent 447436f3
...@@ -40,6 +40,9 @@ dependencies { ...@@ -40,6 +40,9 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0")
implementation("org.jetbrains.skiko:skiko-awt-runtime-$target:$version") implementation("org.jetbrains.skiko:skiko-awt-runtime-$target:$version")
implementation("org.jetbrains.runtime:jbr-api:1.5.0") implementation("org.jetbrains.runtime:jbr-api:1.5.0")
if (System.getProperty("os.name").startsWith("Win")) {
implementation("org.jetbrains.skiko:skiko-awt-runtime-angle-$target:$version")
}
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
} }
...@@ -68,17 +71,26 @@ val casualRun = tasks.named<JavaExec>("run") { ...@@ -68,17 +71,26 @@ val casualRun = tasks.named<JavaExec>("run") {
additionalArguments.forEach { systemProperty(it.key, it.value) } additionalArguments.forEach { systemProperty(it.key, it.value) }
} }
tasks.register("runSoftware") { tasks.register("runWithSoftwareRenderer") {
additionalArguments += mapOf("skiko.renderApi" to "DIRECT_SOFTWARE") additionalArguments += mapOf("skiko.renderApi" to "DIRECT_SOFTWARE")
dependsOn(casualRun) dependsOn(casualRun)
} }
// Use Angle as a primary renderer for Windows. Renderers for the other OSes are not changing yet
tasks.register("runWithAngleEnabled") {
group = "application"
additionalArguments += mapOf("skiko.rendering.angle.enabled" to "true")
dependsOn(casualRun)
}
tasks.register("runWithTransparency") { tasks.register("runWithTransparency") {
group = "application"
additionalArguments += mapOf("skiko.transparency" to "true") additionalArguments += mapOf("skiko.transparency" to "true")
dependsOn(casualRun) dependsOn(casualRun)
} }
tasks.register("runInterop") { tasks.register("runInterop") {
group = "application"
additionalArguments += mapOf("skiko.swing.interop" to "true") additionalArguments += mapOf("skiko.swing.interop" to "true")
dependsOn(casualRun) dependsOn(casualRun)
} }
......
...@@ -9,8 +9,14 @@ import kotlin.math.PI ...@@ -9,8 +9,14 @@ import kotlin.math.PI
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
open class ClocksAwt(private val scaleProvider: () -> Float) : SkikoRenderDelegate, MouseMotionListener { open class ClocksAwt(
constructor(layer: SkiaLayer) : this({ layer.contentScale }) private val scaleProvider: () -> Float,
private val renderProvider: () -> GraphicsApi = { GraphicsApi.UNKNOWN }
) : SkikoRenderDelegate, MouseMotionListener {
constructor(layer: SkiaLayer) : this(
{ layer.contentScale },
{ layer.renderApi }
)
private val typeface = FontMgr.default.makeFromFile("fonts/JetBrainsMono-Regular.ttf") private val typeface = FontMgr.default.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
private val font = Font(typeface, 13f).apply { private val font = Font(typeface, 13f).apply {
...@@ -88,7 +94,7 @@ open class ClocksAwt(private val scaleProvider: () -> Float) : SkikoRenderDelega ...@@ -88,7 +94,7 @@ open class ClocksAwt(private val scaleProvider: () -> Float) : SkikoRenderDelega
} }
val paragraph = ParagraphBuilder(style, fontCollection) val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt())) .pushStyle(TextStyle().setColor(0xFF000000.toInt()))
.addText("JRE: ${System.getProperty("java.vendor")}, ${System.getProperty("java.runtime.version")} $currentSystemTheme") .addText("Graphic API: ${renderProvider()}, JRE: ${System.getProperty("java.vendor")}, ${System.getProperty("java.runtime.version")} $currentSystemTheme")
.popStyle() .popStyle()
.build() .build()
paragraph.layout(Float.POSITIVE_INFINITY) paragraph.layout(Float.POSITIVE_INFINITY)
......
...@@ -42,7 +42,8 @@ val skikoProjectContext = SkikoProjectContext( ...@@ -42,7 +42,8 @@ val skikoProjectContext = SkikoProjectContext(
}, },
createChecksumsTask = { targetOs: OS, targetArch: Arch, fileToChecksum: Provider<File> -> createChecksumsTask = { targetOs: OS, targetArch: Arch, fileToChecksum: Provider<File> ->
createChecksumsTask(targetOs, targetArch, fileToChecksum) createChecksumsTask(targetOs, targetArch, fileToChecksum)
} },
additionalRuntimeLibraries = project.registerAdditionalLibraries(targetOs, targetArch, skiko)
) )
allprojects { allprojects {
...@@ -252,6 +253,7 @@ if (supportAndroid) { ...@@ -252,6 +253,7 @@ if (supportAndroid) {
} }
} }
// TODO now it can be moved, move it if you change this
// Can't be moved to buildSrc because of Checksum dependency // Can't be moved to buildSrc because of Checksum dependency
fun createChecksumsTask( fun createChecksumsTask(
targetOs: OS, targetOs: OS,
...@@ -333,6 +335,9 @@ tasks.findByName("publishSkikoWasmRuntimePublicationToComposeRepoRepository") ...@@ -333,6 +335,9 @@ tasks.findByName("publishSkikoWasmRuntimePublicationToComposeRepoRepository")
tasks.findByName("publishSkikoWasmRuntimePublicationToMavenLocal") tasks.findByName("publishSkikoWasmRuntimePublicationToMavenLocal")
?.dependsOn("publishWasmJsPublicationToMavenLocal") ?.dependsOn("publishWasmJsPublicationToMavenLocal")
skikoProjectContext.additionalRuntimeLibraries.forEach {
it.registerRuntimePublishTaskDependency(listOf("MavenLocal", "ComposeRepoRepository"))
}
tasks.withType<KotlinNativeCompile>().configureEach { tasks.withType<KotlinNativeCompile>().configureEach {
// https://youtrack.jetbrains.com/issue/KT-56583 // https://youtrack.jetbrains.com/issue/KT-56583
......
import org.gradle.api.Project
fun Project.registerAdditionalLibraries(
targetOs: OS,
targetArch: Arch,
skikoProperties: SkikoProperties
): List<AdditionalRuntimeLibrary> {
val angleTag = property("dependencies.angle") as String
return listOfNotNull(
if (supportAwt && targetOs == OS.Windows) {
registerAdditionalRuntimeLibrary(
targetOs = targetOs,
targetArch = targetArch,
skikoProperties = skikoProperties,
name = "angle",
archiveUrl = "https://github.com/JetBrains/angle-pack/releases/download/$angleTag/Angle-$angleTag-${targetOs.id}-Release-${targetArch.id}.zip",
filesToInclude = listOf(
"out/Release-${targetOs.id}-${targetArch.id}/libEGL.dll",
"out/Release-${targetOs.id}-${targetArch.id}/libGLESv2.dll"
),
)
} else {
null
}
)
}
import SkikoArtifacts.jvmAdditionalRuntimeArtifactIdFor
import de.undercouch.gradle.tasks.download.Download
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.provider.Provider
import org.gradle.api.publish.PublicationContainer
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.crypto.checksum.Checksum
import org.gradle.kotlin.dsl.register
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.jvm.tasks.Jar
interface AdditionalRuntimeLibrary {
val jarTask: TaskProvider<Jar>
fun registerMavenPublication(
container: PublicationContainer,
emptySourcesJar: Provider<Jar>,
pomNameForPublication: MutableMap<String, String>
)
fun registerRuntimePublishTaskDependency(repos: List<String>)
}
fun Project.registerAdditionalRuntimeLibrary(
targetOs: OS,
targetArch: Arch,
skikoProperties: SkikoProperties,
name: String,
archiveUrl: String,
filesToInclude: List<String>,
): AdditionalRuntimeLibrary {
val visibleName = "${toTitleCase(name)} Runtime"
val targetId = targetId(targetOs, targetArch)
val taskSuffix = "${toTitleCase(name)}${toTitleCase(targetOs.id)}${toTitleCase(targetArch.id)}"
val archiveFileName = archiveUrl.substringAfterLast('/')
val archiveDir = skikoProperties.dependenciesDir.resolve(name).resolve(archiveFileName.substringBefore("."))
val downloadTask = tasks.register<Download>("download$taskSuffix") {
group = visibleName
description = "Downloads $archiveUrl"
onlyIfModified(true)
src(archiveUrl)
dest(archiveDir.resolve(archiveFileName))
}
val unzipTask = tasks.register<Copy>("unzip$taskSuffix") {
group = visibleName
dependsOn(downloadTask)
from(zipTree(downloadTask.get().dest)) {
filesToInclude.forEach { include(it) }
duplicatesStrategy = DuplicatesStrategy.FAIL
eachFile { path = file.name }
includeEmptyDirs = false
}
into(archiveDir.resolve("extracted"))
}
val checksumTask = tasks.register<Checksum>("createChecksums$taskSuffix") {
group = visibleName
inputFiles.setFrom(unzipTask)
checksumAlgorithm.set(Checksum.Algorithm.SHA256)
outputDirectory.set(layout.buildDirectory.dir("$name/checksums"))
dependsOn(unzipTask)
}
val jarTask = tasks.register<Jar>("skikoRuntimeJar$taskSuffix") {
group = visibleName
dependsOn(unzipTask)
dependsOn(checksumTask)
archiveBaseName.set("skiko-$name")
archiveClassifier.set(targetId)
from(unzipTask)
from(checksumTask)
}
return object : AdditionalRuntimeLibrary {
override val jarTask = jarTask
override fun registerMavenPublication(
container: PublicationContainer,
emptySourcesJar: Provider<Jar>,
pomNameForPublication: MutableMap<String, String>
) {
container.create("skikoJvmRuntime$taskSuffix", MavenPublication::class.java) {
pomNameForPublication[this.name] = "Skiko $visibleName for ${targetOs.id} ${targetArch.id}"
artifactId = jvmAdditionalRuntimeArtifactIdFor(name, targetOs, targetArch)
afterEvaluate {
artifact(jarTask.map { it.archiveFile.get() })
artifact(emptySourcesJar)
}
}
}
override fun registerRuntimePublishTaskDependency(repos: List<String>) {
repos.forEach { repo ->
val mainTaskSuffix = "${toTitleCase(targetOs.id)}${toTitleCase(targetArch.id)}"
project.tasks.findByName("publishSkikoJvmRuntime${mainTaskSuffix}PublicationTo${repo}")
?.dependsOn("publishSkikoJvmRuntime${taskSuffix}PublicationTo${repo}")
}
}
}
}
...@@ -15,7 +15,8 @@ class SkikoProjectContext( ...@@ -15,7 +15,8 @@ class SkikoProjectContext(
val skiko: SkikoProperties, val skiko: SkikoProperties,
val kotlin: KotlinMultiplatformExtension, val kotlin: KotlinMultiplatformExtension,
val windowsSdkPathProvider: () -> WindowsSdkPaths, val windowsSdkPathProvider: () -> WindowsSdkPaths,
val createChecksumsTask: (OS, Arch, Provider<File>) -> TaskProvider<*> val createChecksumsTask: (OS, Arch, Provider<File>) -> TaskProvider<*>,
val additionalRuntimeLibraries: List<AdditionalRuntimeLibrary>,
) { ) {
val buildType = skiko.buildType val buildType = skiko.buildType
......
...@@ -207,6 +207,8 @@ object SkikoArtifacts { ...@@ -207,6 +207,8 @@ object SkikoArtifacts {
"skiko-android-runtime-${arch.id}" "skiko-android-runtime-${arch.id}"
else else
"skiko-awt-runtime-${targetId(os, arch)}" "skiko-awt-runtime-${targetId(os, arch)}"
fun jvmAdditionalRuntimeArtifactIdFor(name: String, os: OS, arch: Arch) =
"skiko-awt-runtime-$name-${os.id}-${arch.id}"
// Using custom name like skiko-<Os>-<Arch> (with a dash) // Using custom name like skiko-<Os>-<Arch> (with a dash)
// does not seem possible (at least without adding a dash to a target's tasks), // does not seem possible (at least without adding a dash to a target's tasks),
// so we're using the default naming pattern instead. // so we're using the default naming pattern instead.
......
...@@ -24,6 +24,7 @@ private class SkikoPublishingContext( ...@@ -24,6 +24,7 @@ private class SkikoPublishingContext(
val project = projectContext.project val project = projectContext.project
val kotlin = projectContext.kotlin val kotlin = projectContext.kotlin
val skiko = projectContext.skiko val skiko = projectContext.skiko
val additionalRuntimeLibraries = projectContext.additionalRuntimeLibraries
val pomNameForPublication: MutableMap<String, String> = HashMap() val pomNameForPublication: MutableMap<String, String> = HashMap()
...@@ -42,6 +43,7 @@ fun SkikoProjectContext.declarePublications() { ...@@ -42,6 +43,7 @@ fun SkikoProjectContext.declarePublications() {
ctx.configurePublicationDefaults() ctx.configurePublicationDefaults()
ctx.configureAllJvmRuntimeJarPublications() ctx.configureAllJvmRuntimeJarPublications()
ctx.configureAwtRuntimeJarPublication() ctx.configureAwtRuntimeJarPublication()
ctx.configureAdditionalRuntimeLibrariesPublication()
ctx.configureWebPublication() ctx.configureWebPublication()
ctx.configureAndroidPublication() ctx.configureAndroidPublication()
...@@ -276,6 +278,12 @@ private fun SkikoPublishingContext.configureAwtRuntimeJarPublication() { ...@@ -276,6 +278,12 @@ private fun SkikoPublishingContext.configureAwtRuntimeJarPublication() {
} }
} }
private fun SkikoPublishingContext.configureAdditionalRuntimeLibrariesPublication() = publications {
additionalRuntimeLibraries.forEach {
it.registerMavenPublication(this, emptySourcesJar, pomNameForPublication)
}
}
private fun SkikoPublishingContext.configureWebPublication() = publications { private fun SkikoPublishingContext.configureWebPublication() = publications {
if (!project.supportWeb) return@publications if (!project.supportWeb) return@publications
create("skikoWasmRuntime", MavenPublication::class.java) { create("skikoWasmRuntime", MavenPublication::class.java) {
......
...@@ -265,6 +265,24 @@ fun Project.configureSignAndPublishDependencies() { ...@@ -265,6 +265,24 @@ fun Project.configureSignAndPublishDependencies() {
} }
} }
if (supportAwt) {
val publishJvmRuntimeAngleX64 = "publishSkikoJvmRuntimeAngleWindowsX64PublicationToComposeRepoRepository"
val publishJvmRuntimeAngleArm64 = "publishSkikoJvmRuntimeAngleWindowsArm64PublicationToComposeRepoRepository"
val signJvmRuntimeX64 = "signSkikoJvmRuntimeWindowsX64Publication"
val signJvmRuntimeArm64 = "signSkikoJvmRuntimeWindowsArm64Publication"
tasks.configureEach {
when {
name.startsWith(publishJvmRuntimeAngleX64) -> {
dependsOn(signJvmRuntimeX64)
}
name.startsWith(publishJvmRuntimeAngleArm64) -> {
dependsOn(signJvmRuntimeArm64)
}
}
}
}
// Cross-publication pairs due to shared javadoc: KotlinMultiplatform <-> AWT // Cross-publication pairs due to shared javadoc: KotlinMultiplatform <-> AWT
tasks.configureEach { tasks.configureEach {
val publishKmp = "publishKotlinMultiplatformPublicationTo" val publishKmp = "publishKotlinMultiplatformPublicationTo"
......
package tasks.configuration package tasks.configuration
import AdditionalRuntimeLibrary
import Arch import Arch
import CompileSkikoCppTask import CompileSkikoCppTask
import CompileSkikoObjCTask import CompileSkikoObjCTask
...@@ -465,11 +466,16 @@ fun SkikoProjectContext.skikoRuntimeDirForTestsTask( ...@@ -465,11 +466,16 @@ fun SkikoProjectContext.skikoRuntimeDirForTestsTask(
targetOs: OS, targetOs: OS,
targetArch: Arch, targetArch: Arch,
skikoJvmJar: Provider<Jar>, skikoJvmJar: Provider<Jar>,
skikoJvmRuntimeJar: Provider<Jar> skikoJvmRuntimeJar: Provider<Jar>,
additionalRuntimeLibraries: List<AdditionalRuntimeLibrary>,
) = project.registerSkikoTask<Copy>("skikoRuntimeDirForTests", targetOs, targetArch) { ) = project.registerSkikoTask<Copy>("skikoRuntimeDirForTests", targetOs, targetArch) {
dependsOn(skikoJvmJar, skikoJvmRuntimeJar) dependsOn(skikoJvmJar, skikoJvmRuntimeJar)
from(project.zipTree(skikoJvmJar.flatMap { it.archiveFile })) from(project.zipTree(skikoJvmJar.flatMap { it.archiveFile }))
from(project.zipTree(skikoJvmRuntimeJar.flatMap { it.archiveFile })) from(project.zipTree(skikoJvmRuntimeJar.flatMap { it.archiveFile }))
additionalRuntimeLibraries.forEach { lib ->
from(project.zipTree(lib.jarTask.flatMap { it.archiveFile }))
}
duplicatesStrategy = DuplicatesStrategy.WARN duplicatesStrategy = DuplicatesStrategy.WARN
destinationDir = project.layout.buildDirectory.dir("skiko-runtime-for-tests").get().asFile destinationDir = project.layout.buildDirectory.dir("skiko-runtime-for-tests").get().asFile
} }
...@@ -482,9 +488,13 @@ fun SkikoProjectContext.skikoJarForTestsTask( ...@@ -482,9 +488,13 @@ fun SkikoProjectContext.skikoJarForTestsTask(
archiveFileName.set("skiko-runtime-for-tests.jar") archiveFileName.set("skiko-runtime-for-tests.jar")
} }
fun SkikoProjectContext.setupJvmTestTask(skikoAwtJarForTests: TaskProvider<Jar>, targetOs: OS, targetArch: Arch) = with(project) { fun SkikoProjectContext.setupJvmTestTask(
skikoAwtJarForTests: TaskProvider<Jar>,
targetOs: OS,
targetArch: Arch
) = with(project) {
val skikoAwtRuntimeJarForTests = createSkikoJvmJarTask(targetOs, targetArch, skikoAwtJarForTests) val skikoAwtRuntimeJarForTests = createSkikoJvmJarTask(targetOs, targetArch, skikoAwtJarForTests)
val skikoRuntimeDirForTests = skikoRuntimeDirForTestsTask(targetOs, targetArch, skikoAwtJarForTests, skikoAwtRuntimeJarForTests) val skikoRuntimeDirForTests = skikoRuntimeDirForTestsTask(targetOs, targetArch, skikoAwtJarForTests, skikoAwtRuntimeJarForTests, additionalRuntimeLibraries)
val skikoJarForTests = skikoJarForTestsTask(skikoRuntimeDirForTests) val skikoJarForTests = skikoJarForTestsTask(skikoRuntimeDirForTests)
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
......
...@@ -15,6 +15,8 @@ val skikoArtifactIds: List<String> = ...@@ -15,6 +15,8 @@ val skikoArtifactIds: List<String> =
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.Linux, Arch.Arm64), SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.Linux, Arch.Arm64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.X64), SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.X64),
SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.Arm64), SkikoArtifacts.jvmRuntimeArtifactIdFor(OS.MacOS, Arch.Arm64),
SkikoArtifacts.jvmAdditionalRuntimeArtifactIdFor("angle", OS.Windows, Arch.X64),
SkikoArtifacts.jvmAdditionalRuntimeArtifactIdFor("angle", OS.Windows, Arch.Arm64),
SkikoArtifacts.jsWasmArtifactId, SkikoArtifacts.jsWasmArtifactId,
SkikoArtifacts.jsArtifactId, SkikoArtifacts.jsArtifactId,
SkikoArtifacts.wasmArtifactId, SkikoArtifacts.wasmArtifactId,
......
...@@ -6,9 +6,13 @@ kotlin.mpp.enableCInteropCommonization=true ...@@ -6,9 +6,13 @@ kotlin.mpp.enableCInteropCommonization=true
deploy.version=0.0.0 deploy.version=0.0.0
# a tag from https://github.com/JetBrains/skia-pack
dependencies.skia=m138-80d088a-1 dependencies.skia=m138-80d088a-1
# you can override general skia dependencies by passing platform-specific property: # a tag from https://github.com/JetBrains/angle-pack
dependencies.angle=ec4d8f8e4d
# you can override general skia dependencies by passing platform-specific property:
# dependencies.skia.android-arm64 # dependencies.skia.android-arm64
# dependencies.skia.android-x64 # dependencies.skia.android-x64
# dependencies.skia.ios-arm64 # dependencies.skia.ios-arm64
......
...@@ -2,22 +2,13 @@ ...@@ -2,22 +2,13 @@
#include <windows.h> #include <windows.h>
#include <shlwapi.h> #include <shlwapi.h>
#include <string>
#define GL_GLES_PROTOTYPES 0 #define GL_GLES_PROTOTYPES 0
#define EGL_EGL_PROTOTYPES 0 #define EGL_EGL_PROTOTYPES 0
#include <GLES/gl.h> #include <GLES/gl.h>
#include <EGL/egl.h> #include <EGL/egl.h>
#include "exceptions_handler.h" #include "exceptions_handler.h"
#define THROW_IF_NULL(action) \
do { \
auto __result { action }; \
if (0 == __result) { \
auto __code = GetLastError(); \
throwJavaRenderExceptionByErrorCode(env, __FUNCTION__, __code); \
return; \
} \
} while ((void)0, 0)
static HINSTANCE AngleEGLLibrary = nullptr; static HINSTANCE AngleEGLLibrary = nullptr;
extern "C" { extern "C" {
...@@ -76,20 +67,11 @@ extern "C" { ...@@ -76,20 +67,11 @@ extern "C" {
return eglGetProcAddress(procname); return eglGetProcAddress(procname);
} }
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_AngleSupport_1jvmKt_loadAngleLibraryWindows(JNIEnv *env, jobject obj) { JNIEXPORT jboolean JNICALL Java_org_jetbrains_skiko_AngleSupport_1jvmKt_initAngleLibraryWindows(JNIEnv *env, jobject, jstring jlibraryName) {
TCHAR basePath[MAX_PATH] = TEXT(""); const char* libraryName = env->GetStringUTFChars(jlibraryName, nullptr);
TCHAR libEGL[MAX_PATH] = TEXT(""); AngleEGLLibrary = GetModuleHandleA(libraryName);
TCHAR libGLESv2[MAX_PATH] = TEXT(""); env->ReleaseStringUTFChars(jlibraryName, libraryName);
HMODULE hmodule = nullptr; return AngleEGLLibrary != nullptr;
THROW_IF_NULL(GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPTSTR) &Java_org_jetbrains_skiko_AngleSupport_1jvmKt_loadAngleLibraryWindows,
&hmodule));
THROW_IF_NULL(GetModuleFileName(hmodule, basePath, sizeof(basePath)));
THROW_IF_NULL(PathRemoveFileSpec(basePath));
THROW_IF_NULL(PathCombine(libEGL, basePath, TEXT("libEGL.dll")));
THROW_IF_NULL(PathCombine(libGLESv2, basePath, TEXT("libGLESv2.dll")));
THROW_IF_NULL(AngleEGLLibrary = LoadLibrary(libEGL));
THROW_IF_NULL(LoadLibrary(libGLESv2));
} }
} }
......
...@@ -11,7 +11,11 @@ internal class AngleRedrawer( ...@@ -11,7 +11,11 @@ internal class AngleRedrawer(
private val properties: SkiaLayerProperties private val properties: SkiaLayerProperties
) : AWTRedrawer(layer, analytics, GraphicsApi.ANGLE) { ) : AWTRedrawer(layer, analytics, GraphicsApi.ANGLE) {
init { init {
loadAngleLibrary() try {
loadAngleLibrary()
} catch (e: Exception) {
throw RenderException("Failed to load ANGLE library", cause = e)
}
} }
private val contextHandler = AngleContextHandler(layer) private val contextHandler = AngleContextHandler(layer)
......
...@@ -21,7 +21,7 @@ internal fun uiTest( ...@@ -21,7 +21,7 @@ internal fun uiTest(
runBlocking(MainUIDispatcher) { runBlocking(MainUIDispatcher) {
if (renderApiProperty == "all") { if (renderApiProperty == "all") {
for (renderApi in SkikoProperties.fallbackRenderApiQueue(SkikoProperties.renderApi)) { for (renderApi in SkikoProperties.fallbackRenderApiQueue(initialApi = null)) {
if (renderApi in excludeRenderApis) { if (renderApi in excludeRenderApis) {
println("Skipping $renderApi renderApi") println("Skipping $renderApi renderApi")
continue continue
......
package org.jetbrains.skiko
/**
* An exception related to inability of loading an optional rendering API
* (e.g. ANGLE on Windows)
*/
internal class OptionalRenderApiException(
message: String? = null,
cause: Throwable? = null
) : RenderException(message, cause)
...@@ -2,25 +2,25 @@ package org.jetbrains.skiko ...@@ -2,25 +2,25 @@ package org.jetbrains.skiko
import org.jetbrains.skia.impl.Library import org.jetbrains.skia.impl.Library
private external fun loadAngleLibraryWindows() private external fun initAngleLibraryWindows(libraryName: String): Boolean
private var isLoaded = false private const val libEGLName = "libEGL"
@Synchronized private var loader = LibraryLoader(
internal actual fun loadAngleLibrary() { libEGLName,
if (!isLoaded) { // libGLESv2 is not loaded explicitly in Skiko, it is loaded by libEGL
when { additionalFile = System.mapLibraryName("libGLESv2"),
hostOs.isWindows -> { init = {
Library.staticLoad() Library.staticLoad()
try { if (!initAngleLibraryWindows(libEGLName)) {
loadAngleLibraryWindows() throw LibraryLoadException("Failed to load ANGLE library $libEGLName")
}
catch (e: Exception) {
throw OptionalRenderApiException("Failed to load ANGLE library: ${e}")
}
}
else -> Unit
} }
isLoaded = true
} }
} )
\ No newline at end of file
internal actual fun loadAngleLibrary() {
when {
hostOs.isWindows -> loader.loadOnce()
else -> Unit
}
}
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skia.Bitmap import org.jetbrains.skia.Bitmap
import java.io.File import java.util.concurrent.atomic.AtomicBoolean
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption.*
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.bufferedReader
import kotlin.io.path.createParentDirectories
import kotlin.io.resolve
import kotlin.use
object Library { object Library {
private var copyDir: File? = null private var loader = LibraryLoader(
name = "skiko-$hostId",
// A native library cannot be loaded in several classloaders, so we have to clone additionalFile = if (hostOs.isWindows) "icudtl.dat" else null,
// the native library to allow Skiko loading to work properly in complex cases, i.e., init = {
// several IDEA plugins.
private fun loadLibraryOrCopy(library: File) {
try {
System.load(library.absolutePath)
} catch (e: UnsatisfiedLinkError) {
if (e.message?.contains("already loaded in another classloader") == true) {
copyDir = Files.createTempDirectory("skiko").toFile()
val tempFile = copyDir!!.resolve(library.name)
Files.copy(library.toPath(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
tempFile.deleteOnExit()
System.load(tempFile.absolutePath)
} else {
throw e
}
}
}
private fun unpackIfNeeded(dest: File, resourceName: String, deleteOnExit: Boolean): File {
val file = File(dest, resourceName)
if (!file.exists()) {
withFileLock(dest.resolve(".lock").toPath()) {
if (file.exists()) return file
val tempFile = File.createTempFile("skiko", "", dest)
if (deleteOnExit)
file.deleteOnExit()
Library::class.java.getResourceAsStream("/$resourceName").use { input ->
Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
}
}
return file
}
/**
* Holds a reference to the lock which has to be acquired when loading the native library,
* or to wait for the loading to finish.
* The reference will resolve to `null` if loading is done and callers do not need to wait anymore.
*/
private val loadingLock = AtomicReference(ReentrantLock())
// This function does the following: on request to load given resource,
// it checks if resource with given name is found in content-derived directory
// in Skiko's home, and if not - unpacks it. It could also load additional
// localization resources, on platforms where it is needed.
fun load() {
/**
* If there is no more loading lock available, then the loading has finished
* and we can just return as normal, assuming that the library was successfully loaded.
*/
val lock = loadingLock.get() ?: return
/**
* If the lock is held by the current thread, then this indicates a recursive call to load.
* Methods like `_nAfterLoad()` might trigger additional Class loading, where .clinit (static init methods)
* trigger further calls to Library.staticLoad() -> Library.load() while holding the current lock.
*
* It is fine, in such cases, to return eagerly and assume that the Library is successfully loaded and
* the recursion is a result of callbacks indicating the successful load.
*/
if (lock.isHeldByCurrentThread) return
lock.withLock {
// We entered the critical section, but another thread might have already entered and finished
if (loadingLock.get() !== lock) return
// Find/unpack a usable copy of the native library.
findAndLoad()
// TODO move properties to SkikoProperties
Setup.init() Setup.init()
try { try {
...@@ -97,64 +15,16 @@ object Library { ...@@ -97,64 +15,16 @@ object Library {
org.jetbrains.skia.impl.Library._nAfterLoad() org.jetbrains.skia.impl.Library._nAfterLoad()
} catch (t: Throwable) { } catch (t: Throwable) {
t.printStackTrace() t.printStackTrace()
} finally {
loadingLock.compareAndSet(lock, null)
} }
} }
} )
private fun findAndLoad() {
val name = "skiko-$hostId"
val platformName = System.mapLibraryName(name)
val icu = if (hostOs.isWindows) "icudtl.dat" else null
if (hostOs == OS.Android) {
System.loadLibrary("skiko-$hostId")
return
}
// First try: system property is set.
val skikoLibraryPath = SkikoProperties.libraryPath
if (skikoLibraryPath != null) {
val library = File(File(skikoLibraryPath), platformName)
loadLibraryOrCopy(library)
if (icu != null && copyDir != null)
unpackIfNeeded(copyDir!!, icu, true)
return
}
// Second try: load it from the bin/ or lib/ directory relative to the JVM home. // This function does the following: on request to load given resource,
// The user might have placed the native files alongside the other JVM libraries // it checks if resource with given name is found in content-derived directory
// for signing purposes, so we'll find it here if so. // in Skiko's home, and if not - unpacks it. It could also load additional
val jvmFiles = File(System.getProperty("java.home"), if (hostOs.isWindows) "bin" else "lib") // localization resources, on platforms where it is needed.
val pathInJvm = jvmFiles.resolve(platformName) fun load() {
if (pathInJvm.exists() && icu?.let { (jvmFiles.resolve(it)).exists() } != false) { loader.loadOnce()
loadLibraryOrCopy(pathInJvm)
return
}
// Third try: look up in or extract to a local cache directory.
// Key the cache by the hash of the library.
val hashResourceStream = Library::class.java.getResourceAsStream(
"/$platformName.sha256"
) ?: throw LibraryLoadException(
"Cannot find $platformName.sha256, proper native dependency missing."
)
val hash = hashResourceStream.use { it.bufferedReader().readLine() }
val dataDir = File(File(SkikoProperties.dataPath), hash)
dataDir.mkdirs()
val library = unpackIfNeeded(dataDir, platformName, false)
loadLibraryOrCopy(library)
if (icu != null) {
if (copyDir != null) {
// We made a duplicate to resolve classloader conflicts.
unpackIfNeeded(copyDir!!, icu, true)
} else {
// Normal path where Skiko is loaded only once.
unpackIfNeeded(dataDir, icu, false)
}
}
} }
} }
...@@ -165,19 +35,3 @@ internal class LibraryTestImpl() { ...@@ -165,19 +35,3 @@ internal class LibraryTestImpl() {
return bitmap._ptr return bitmap._ptr
} }
} }
/**
* Simple lockfile utility which ensures that the lockfile at the given [path] exists and is locked properly.
* Note: This method cannot be re-entered recusrively
* Note: The same process can only take a given lock once
*/
internal inline fun <T> withFileLock(path: Path, action: () -> T): T {
path.createParentDirectories()
return FileChannel.open(path, READ, WRITE, CREATE).use { channel ->
val lock = channel.lock()
lock.use {
action()
}
}
}
...@@ -2,4 +2,4 @@ package org.jetbrains.skiko ...@@ -2,4 +2,4 @@ package org.jetbrains.skiko
import java.lang.RuntimeException import java.lang.RuntimeException
class LibraryLoadException(message: String) : RuntimeException(message) class LibraryLoadException(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
\ No newline at end of file \ No newline at end of file
package org.jetbrains.skiko
import java.io.File
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption.*
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.path.createParentDirectories
internal class LibraryLoader(
/**
* Short library name without platform suffix and extension. For example "skiko" or "skiko-angle-libEGL"
*/
private val name: String,
/**
* Additional file to check or unpack after loading the library. For example, "icudtl.dat".
*
* Currently only one file is supported, but it can be extended to support multiple ones.
*/
private val additionalFile: String? = null,
/**
* Additional code that is called after successfully loading
*/
private val init: () -> Unit = {}
) {
// A native library cannot be loaded in several classloaders, so we have to clone
// the native library to allow Skiko loading to work properly in complex cases, i.e.,
// several IDEA plugins.
private fun loadLibraryOrCopy(library: File): File? {
try {
System.load(library.absolutePath)
return null
} catch (e: UnsatisfiedLinkError) {
if (e.message?.contains("already loaded in another classloader") == true) {
val copyDir = Files.createTempDirectory("skiko").toFile()
val tempFile = copyDir.resolve(library.name)
Files.copy(library.toPath(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
tempFile.deleteOnExit()
System.load(tempFile.absolutePath)
return copyDir
} else {
throw LibraryLoadException("Failed to loade library $library", cause = e)
}
}
}
private fun unpackIfNeeded(dest: File, resourceName: String, deleteOnExit: Boolean): File {
val file = File(dest, resourceName)
if (!file.exists()) {
withFileLock(dest.resolve(".lock").toPath()) {
if (file.exists()) return file
val tempFile = File.createTempFile("skiko", "", dest)
if (deleteOnExit)
file.deleteOnExit()
Library::class.java.getResourceAsStream("/$resourceName").use { input ->
Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
}
}
return file
}
/**
* Holds a reference to the lock which has to be acquired when loading the native library,
* or to wait for the loading to finish.
* The reference will resolve to `null` if loading is done and callers do not need to wait anymore.
*/
private val loadingLock = AtomicReference(ReentrantLock())
/**
* Load a native library finding it in multiple sources:
* - from SkikoProperties.libraryPath
* - java.home
* - jar resources
*
* @throws LibraryLoadException if library wasn't loaded successfully.
* Calling this function again retries the loading.
*/
fun loadOnce() {
/**
* If there is no more loading lock available, then the loading has finished
* and we can just return as normal, assuming that the library was successfully loaded.
*/
val lock = loadingLock.get() ?: return
/**
* If the lock is held by the current thread, then this indicates a recursive call to load.
* Methods like `_nAfterLoad()` might trigger additional Class loading, where .clinit (static init methods)
* trigger further calls to Library.staticLoad() -> Library.load() while holding the current lock.
*
* It is fine, in such cases, to return eagerly and assume that the Library is successfully loaded and
* the recursion is a result of callbacks indicating the successful load.
*/
if (lock.isHeldByCurrentThread) return
lock.withLock {
// We entered the critical section, but another thread might have already entered and finished
if (loadingLock.get() !== lock) return
try {
findAndLoadLibrary(name, additionalFile)
init()
} finally {
loadingLock.compareAndSet(lock, null)
}
}
}
private fun findAndLoadLibrary(name: String, additionalFile: String? = null) {
val platformName = System.mapLibraryName(name)
if (hostOs == OS.Android) {
System.loadLibrary(name)
return
}
// First try: system property is set.
val skikoLibraryPath = SkikoProperties.libraryPath
if (skikoLibraryPath != null) {
val library = File(File(skikoLibraryPath), platformName)
val copyDir = loadLibraryOrCopy(library)
if (additionalFile != null && copyDir != null)
unpackIfNeeded(copyDir, additionalFile, true)
return
}
// Second try: load it from the bin/ or lib/ directory relative to the JVM home.
// The user might have placed the native files alongside the other JVM libraries
// for signing purposes, so we'll find it here if so.
val jvmFiles = File(System.getProperty("java.home"), if (hostOs.isWindows) "bin" else "lib")
val pathInJvm = jvmFiles.resolve(platformName)
if (pathInJvm.exists() && additionalFile?.let { (jvmFiles.resolve(it)).exists() } != false) {
loadLibraryOrCopy(pathInJvm)
return
}
// Third try: look up in or extract to a local cache directory.
// Key the cache by the hash of the library.
val hashResourceStream = Library::class.java.getResourceAsStream(
"/$platformName.sha256"
) ?: throw LibraryLoadException(
"Cannot find $platformName.sha256, proper native dependency missing."
)
val hash = hashResourceStream.use { it.bufferedReader().readLine() }
val dataDir = File(File(SkikoProperties.dataPath), hash)
dataDir.mkdirs()
val library = unpackIfNeeded(dataDir, platformName, false)
val copyDir = loadLibraryOrCopy(library)
if (additionalFile != null) {
if (copyDir != null) {
// We made a duplicate to resolve classloader conflicts.
unpackIfNeeded(copyDir, additionalFile, true)
} else {
// Normal path where Skiko is loaded only once.
unpackIfNeeded(dataDir, additionalFile, false)
}
}
}
}
/**
* Simple lockfile utility which ensures that the lockfile at the given [path] exists and is locked properly.
* Note: This method cannot be re-entered recusrively
* Note: The same process can only take a given lock once
*/
internal inline fun <T> withFileLock(path: Path, action: () -> T): T {
path.createParentDirectories()
return FileChannel.open(path, READ, WRITE, CREATE).use { channel ->
val lock = channel.lock()
lock.use {
action()
}
}
}
...@@ -54,6 +54,18 @@ object SkikoProperties { ...@@ -54,6 +54,18 @@ object SkikoProperties {
return getProperty("skiko.rendering.linux.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false return getProperty("skiko.rendering.linux.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false
} }
/**
* Is experimental ANGLE renderer API enabled (https://skia.org/docs/user/special/angle/).
*
* If enabled, Windows uses it as a primary render API and fallbacks to the default APIs.
*
* Other OSes are not supported yet.
*
* If it is enabled, make sure that either:
* - `org.jetbrains.skiko:skiko-awt-runtime-angle-$target:$version` added as a dependency
* - The `skiko.library.path` property is defined and the directory has libEGL, libGLESv2 from
* https://github.com/JetBrains/angle-pack/releases
*/
val renderingAngleEnabled: Boolean get() = getProperty("skiko.rendering.angle.enabled")?.toBoolean() ?: false val renderingAngleEnabled: Boolean get() = getProperty("skiko.rendering.angle.enabled")?.toBoolean() ?: false
/** /**
...@@ -160,7 +172,7 @@ object SkikoProperties { ...@@ -160,7 +172,7 @@ object SkikoProperties {
} }
} }
internal fun fallbackRenderApiQueue(initialApi: GraphicsApi): List<GraphicsApi> { internal fun fallbackRenderApiQueue(initialApi: GraphicsApi?): List<GraphicsApi> {
var fallbackApis = when (hostOs) { var fallbackApis = when (hostOs) {
OS.Linux -> listOf(GraphicsApi.OPENGL, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT) OS.Linux -> listOf(GraphicsApi.OPENGL, GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT)
OS.MacOS -> listOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE_COMPAT) OS.MacOS -> listOf(GraphicsApi.METAL, GraphicsApi.SOFTWARE_COMPAT)
...@@ -186,12 +198,16 @@ object SkikoProperties { ...@@ -186,12 +198,16 @@ object SkikoProperties {
else -> return listOf(GraphicsApi.UNKNOWN) else -> return listOf(GraphicsApi.UNKNOWN)
} }
val indexOfInitialApi = fallbackApis.indexOf(initialApi) return if (initialApi != null) {
require(indexOfInitialApi >= 0) { val indexOfInitialApi = fallbackApis.indexOf(initialApi)
"$hostOs does not support $initialApi rendering API." require(indexOfInitialApi >= 0) {
} "$hostOs does not support $initialApi rendering API."
fallbackApis = fallbackApis.drop(indexOfInitialApi + 1) }
fallbackApis = fallbackApis.drop(indexOfInitialApi + 1)
return listOf(initialApi) + fallbackApis listOf(initialApi) + fallbackApis
} else {
fallbackApis
}
} }
} }
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