Unverified Commit 3e870638 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Pack ANGLE into a separate jar (#1082)

An addition to [the feature
PR](https://github.com/JetBrains/skiko/pull/1017) that allows including
ANGLE the same way as Skiko native libraries included:
- unpack it from an additional jar
`org.jetbrains.skiko:skiko-awt-runtime-angle-$target:$version`
- find them in `skiko.library.path` directoty (a system property)
- find them in `java.home`

It reuses the same logic that is used to load `Skiko.dll`.

Currently only Windows supported, the same way as in the original PR.

No CI changes needed, as building of this jar is added as a dependency
for
`publishSkikoJvmRuntimeWindowsX64PublicationToComposeRepoRepository`/`publishSkikoJvmRuntimeWindowsArm64PublicationToComposeRepoRepository`

## Testing

Manually with `skiko.rendering.angle.enabled` true/false on
SkikoAwtSample.

## Release Notes (Skiko)
### Features - Desktop
A new experimental renderer is introduced for Windows. It uses [the
ANGLE library](https://github.com/google/angle) which has proven to be
stable because it is used inside Chromium, compared to the `DIRECT3D`
renderer that has issues on a few machines. Note that `ANGLE` still uses
`DIRECT3D` API under the hood.

1. Add this to the code to enable it:
```
System.setProperty("skiko.rendering.angle.enabled", "true")
```

2. Add the ANGLE library into the dependencies:
  - If you use Gradle:
  ```
  if (System.getProperty("os.name").startsWith("Win")) {

implementation("org.jetbrains.skiko:skiko-awt-runtime-angle-$target:$version")
  }
  ```
- If you set `skiko.library.path`. Extract `libEGL.dll`, `libGLESv2.dll`
from https://github.com/JetBrains/angle-pack/releases into this
directory.

## Release Notes (Compose)
### Features - Desktop
N/A

Until we specify how to match Skiko and Compose versions, or include it
in Compose, it is a Skiko-only experimental feature. Some projects can
include Skiko directly, it should work, but not supported as a Compose
feature.
parent 814b6e46
...@@ -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)
......
...@@ -16,7 +16,6 @@ plugins { ...@@ -16,7 +16,6 @@ plugins {
id("org.jetbrains.dokka") version "1.9.10" id("org.jetbrains.dokka") version "1.9.10"
`maven-publish` `maven-publish`
signing signing
id("org.gradle.crypto.checksum") version "1.4.0"
} }
if (supportAndroid) { if (supportAndroid) {
...@@ -455,6 +454,7 @@ if (supportAndroid) { ...@@ -455,6 +454,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,
...@@ -467,13 +467,19 @@ fun createChecksumsTask( ...@@ -467,13 +467,19 @@ fun createChecksumsTask(
outputDirectory = layout.buildDirectory.dir("checksums-${targetId(targetOs, targetArch)}") outputDirectory = layout.buildDirectory.dir("checksums-${targetId(targetOs, targetArch)}")
} }
val additionalRuntimeLibraries = project.registerAdditionalLibraries(targetOs, targetArch, skiko)
if (supportAwt) { if (supportAwt) {
val skikoAwtJarForTests by project.tasks.registering(Jar::class) { val skikoAwtJarForTests by project.tasks.registering(Jar::class) {
archiveBaseName.set("skiko-awt-test") archiveBaseName.set("skiko-awt-test")
from(kotlin.jvm("awt").compilations["main"].output.allOutputs) from(kotlin.jvm("awt").compilations["main"].output.allOutputs)
} }
skikoProjectContext.setupJvmTestTask(skikoAwtJarForTests, targetOs, targetArch) skikoProjectContext.setupJvmTestTask(
skikoAwtJarForTests,
additionalRuntimeLibraries,
targetOs,
targetArch,
)
} }
afterEvaluate { afterEvaluate {
...@@ -590,6 +596,10 @@ publishing { ...@@ -590,6 +596,10 @@ publishing {
} }
} }
additionalRuntimeLibraries.forEach {
it.registerMavenPublication(this, emptySourcesJar, pomNameForPublication)
}
if (supportWeb) { if (supportWeb) {
create<MavenPublication>("skikoWasmRuntime") { create<MavenPublication>("skikoWasmRuntime") {
pomNameForPublication[name] = "Skiko WASM Runtime" pomNameForPublication[name] = "Skiko WASM Runtime"
...@@ -649,6 +659,9 @@ tasks.findByName("publishSkikoWasmRuntimePublicationToComposeRepoRepository") ...@@ -649,6 +659,9 @@ tasks.findByName("publishSkikoWasmRuntimePublicationToComposeRepoRepository")
tasks.findByName("publishSkikoWasmRuntimePublicationToMavenLocal") tasks.findByName("publishSkikoWasmRuntimePublicationToMavenLocal")
?.dependsOn("publishWasmJsPublicationToMavenLocal") ?.dependsOn("publishWasmJsPublicationToMavenLocal")
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
......
...@@ -13,4 +13,5 @@ dependencies { ...@@ -13,4 +13,5 @@ dependencies {
val kotlinVersion = project.properties["kotlin.version"] as String val kotlinVersion = project.properties["kotlin.version"] as String
implementation(kotlin("gradle-plugin", kotlinVersion)) implementation(kotlin("gradle-plugin", kotlinVersion))
implementation("de.undercouch:gradle-download-task:5.5.0") implementation("de.undercouch:gradle-download-task:5.5.0")
implementation("org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:1.4.0")
} }
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.publish.PublicationContainer
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.crypto.checksum.Checksum
import org.gradle.kotlin.dsl.register
import org.gradle.api.publish.maven.MavenPublication
interface AdditionalRuntimeLibrary {
val jarTask: TaskProvider<Jar>
fun registerMavenPublication(
container: PublicationContainer,
emptySourcesJar: TaskProvider<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: TaskProvider<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}")
}
}
}
}
...@@ -211,6 +211,8 @@ object SkikoArtifacts { ...@@ -211,6 +211,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.
......
package tasks.configuration package tasks.configuration
import AdditionalRuntimeLibrary
import Arch import Arch
import CompileSkikoCppTask import CompileSkikoCppTask
import CompileSkikoObjCTask import CompileSkikoObjCTask
...@@ -24,7 +25,6 @@ import org.gradle.api.tasks.TaskProvider ...@@ -24,7 +25,6 @@ import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import org.gradle.util.internal.VersionNumber
import projectDirs import projectDirs
import registerOrGetSkiaDirProvider import registerOrGetSkiaDirProvider
import registerSkikoTask import registerSkikoTask
...@@ -461,11 +461,16 @@ fun SkikoProjectContext.skikoRuntimeDirForTestsTask( ...@@ -461,11 +461,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
} }
...@@ -478,9 +483,14 @@ fun SkikoProjectContext.skikoJarForTestsTask( ...@@ -478,9 +483,14 @@ 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>,
additionalRuntimeLibraries: List<AdditionalRuntimeLibrary>,
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,8 +6,12 @@ kotlin.mpp.enableCInteropCommonization=true ...@@ -6,8 +6,12 @@ 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
# a tag from https://github.com/JetBrains/angle-pack
dependencies.angle=7fea539cc9
# you can override general skia dependencies by passing platform-specific property: # 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
......
...@@ -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 {
try {
loadAngleLibrary() 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}")
} }
} }
)
internal actual fun loadAngleLibrary() {
when {
hostOs.isWindows -> loader.loadOnce()
else -> Unit else -> Unit
} }
isLoaded = true
}
} }
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skia.Bitmap import org.jetbrains.skia.Bitmap
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
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()) {
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
}
private var loaded = AtomicBoolean(false)
// 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.
@Synchronized
fun load() {
if (!loaded.compareAndSet(false, true)) return
// Find/unpack a usable copy of the native library.
findAndLoad()
// TODO move properties to SkikoProperties
Setup.init() Setup.init()
try { try {
...@@ -65,59 +17,14 @@ object Library { ...@@ -65,59 +17,14 @@ object Library {
t.printStackTrace() t.printStackTrace()
} }
} }
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.
// 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() && icu?.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) // This function does the following: on request to load given resource,
dataDir.mkdirs() // it checks if resource with given name is found in content-derived directory
val library = unpackIfNeeded(dataDir, platformName, false) // in Skiko's home, and if not - unpacks it. It could also load additional
loadLibraryOrCopy(library) // localization resources, on platforms where it is needed.
if (icu != null) { fun load() {
if (copyDir != null) { loader.loadOnce()
// 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)
}
}
} }
} }
......
...@@ -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.file.Files
import java.nio.file.StandardCopyOption
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()) {
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
}
private var isLoaded = false
/**
* 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.
*/
@Synchronized
fun loadOnce() {
if (!isLoaded) {
findAndLoadLibrary(name, additionalFile)
init()
isLoaded = true
}
}
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)
}
}
}
}
...@@ -50,6 +50,18 @@ object SkikoProperties { ...@@ -50,6 +50,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
/** /**
...@@ -156,7 +168,7 @@ object SkikoProperties { ...@@ -156,7 +168,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)
...@@ -182,12 +194,16 @@ object SkikoProperties { ...@@ -182,12 +194,16 @@ object SkikoProperties {
else -> return listOf(GraphicsApi.UNKNOWN) else -> return listOf(GraphicsApi.UNKNOWN)
} }
return if (initialApi != null) {
val indexOfInitialApi = fallbackApis.indexOf(initialApi) val indexOfInitialApi = fallbackApis.indexOf(initialApi)
require(indexOfInitialApi >= 0) { require(indexOfInitialApi >= 0) {
"$hostOs does not support $initialApi rendering API." "$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