Commit 572f0e08 authored by liuqiang's avatar liuqiang

feat(ohos): Adapt 0.150.1 and skia m150

Signed-off-by: liuqiang's avatarqiangliu <liuqiang@eazytec.com>
parent 3956e988
...@@ -13,3 +13,5 @@ skiko/src/jvmMain/java ...@@ -13,3 +13,5 @@ skiko/src/jvmMain/java
.cache .cache
.clangd .clangd
**/kotlin-js-store/ **/kotlin-js-store/
*.log
/dependencies/
[versions] [versions]
kotlin = "2.3.20" kotlin = "2.4.0-ST.0.4.0-0812"
kotlinxBrowser = "0.5.0" kotlinxBrowser = "0.5.0"
coroutines = "1.8.0" coroutines = "1.10.2-0.4.0"
jetbrainsRuntime-api = "1.5.0" jetbrainsRuntime-api = "1.5.0"
androidGradlePlugin = "9.0.0" androidGradlePlugin = "9.0.0"
...@@ -15,6 +15,9 @@ cryptoChecksumPlugin = "1.4.0" ...@@ -15,6 +15,9 @@ cryptoChecksumPlugin = "1.4.0"
kotlinxBenchmark = "0.4.14" kotlinxBenchmark = "0.4.14"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
[libraries] [libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" } coroutines-core-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" }
...@@ -33,4 +36,4 @@ githubApi = { module = "org.kohsuke:github-api", version.ref = "githubApi" } ...@@ -33,4 +36,4 @@ githubApi = { module = "org.kohsuke:github-api", version.ref = "githubApi" }
buildHelpers-publishing-gradlePlugin = { module = "org.jetbrains.compose.internal.build-helpers:publishing", version.ref = "buildHelpers-publishing" } buildHelpers-publishing-gradlePlugin = { module = "org.jetbrains.compose.internal.build-helpers:publishing", version.ref = "buildHelpers-publishing" }
crypto-checksum-gradlePlugin = { module = "gradle.plugin.org.gradle.crypto:checksum", version.ref = "cryptoChecksumPlugin" } crypto-checksum-gradlePlugin = { module = "gradle.plugin.org.gradle.crypto:checksum", version.ref = "cryptoChecksumPlugin" }
kotlinx-benchmark-gradlePlugin = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-plugin", version.ref = "kotlinxBenchmark" } kotlinx-benchmark-gradlePlugin = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-plugin", version.ref = "kotlinxBenchmark" }
kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinxBenchmark"} kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinxBenchmark"}
\ No newline at end of file
#!/usr/bin/env bash
# Copyright (c) 2026 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -euo pipefail
RETRY_MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-3}"
RETRY_DELAY_SECONDS="${RETRY_DELAY_SECONDS:-10}"
DEPLOY_VERSION="${DEPLOY_VERSION:-0.150.1-0.1.0-01}"
DEPLOY_RELEASE="${DEPLOY_RELEASE:-true}"
PUBLISH_REPOSITORY="${PUBLISH_REPOSITORY:-MavenLocal}"
PUBLISH_MODE="default"
# region Ohos
SKIA_TAG="m150-1f14f1166a"
# endregion
case "$PUBLISH_REPOSITORY" in
MavenLocal)
PUBLISH_TASK_SUFFIX="ToMavenLocal"
;;
ComposeRepo)
PUBLISH_TASK_SUFFIX="ToComposeRepoRepository"
;;
*)
echo "[build-ohos] ERROR: unsupported PUBLISH_REPOSITORY: $PUBLISH_REPOSITORY"
echo "[build-ohos] Supported repositories: MavenLocal, ComposeRepo"
exit 1
;;
esac
PUBLISH_OHOS_ARM64_TASK=":skiko:publishOhosArm64Publication${PUBLISH_TASK_SUFFIX}"
PUBLISH_OHOS_X64_TASK=":skiko:publishOhosX64Publication${PUBLISH_TASK_SUFFIX}"
PUBLISH_KMP_TASK=":skiko:publishKotlinMultiplatformPublication${PUBLISH_TASK_SUFFIX}"
if [ $# -gt 1 ]; then
echo "[build-ohos] ERROR: too many arguments."
echo "[build-ohos] Usage: $0 [--skia|--fusionRenderer]"
exit 1
fi
if [ $# -eq 1 ]; then
case "$1" in
--skia)
PUBLISH_MODE="skia"
;;
--fusionRenderer)
PUBLISH_MODE="fusionRenderer"
;;
*)
echo "[build-ohos] ERROR: unsupported argument: $1"
echo "[build-ohos] Usage: $0 [--skia|--fusionRenderer]"
exit 1
;;
esac
fi
if [ -z "${JAVA_HOME:-}" ] || [ ! -x "$JAVA_HOME/bin/java" ]; then
echo "[build-ohos] ERROR: JAVA_HOME is not set to a valid JDK."
echo "[build-ohos] Configure JAVA_HOME in the shell or pass it when invoking this script."
exit 1
fi
export JAVA_HOME
export PATH="$JAVA_HOME/bin:$PATH"
echo "[build-ohos] Using JAVA_HOME: $JAVA_HOME"
java -version 2>&1 | sed -n '1p'
# 切换到项目根目录,支持从任意路径调用(如 CI 流水线)
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
OHOS_NATIVE_DIR="${OHOS_NATIVE_DIR:-/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/native}"
export OHOS_LLVM="${OHOS_LLVM:-$OHOS_NATIVE_DIR/llvm}"
export OHOS_SYSROOT="${OHOS_SYSROOT:-$OHOS_NATIVE_DIR/sysroot}"
if [ ! -d "$OHOS_LLVM" ] || [ ! -d "$OHOS_SYSROOT" ]; then
echo "[build-ohos] ERROR: OHOS native toolchain not found: $OHOS_NATIVE_DIR"
exit 1
fi
echo "[build-ohos] OHOS native SDK: $OHOS_NATIVE_DIR"
# region Ohos
# Prepares the patched m150 checkout when no external checkout is supplied.
# Building is delegated to the upstream Gradle task.
prepare_ohos_skia() {
local managed_skia_dir arm64_archive x64_archive
if [ -n "${SKIA_DIR:-}" ]; then
if [ ! -d "$SKIA_DIR/.git" ]; then
echo "[build-ohos] ERROR: SKIA_DIR is not a Git checkout: $SKIA_DIR"
return 1
fi
managed_skia_dir="$SKIA_DIR"
else
managed_skia_dir="$PROJECT_ROOT/dependencies/skia/$SKIA_TAG"
if [ ! -d "$managed_skia_dir/.git" ]; then
if [ -e "$managed_skia_dir" ]; then
echo "[build-ohos] ERROR: managed Skia path exists but is not a Git checkout: $managed_skia_dir"
return 1
fi
mkdir -p "$(dirname "$managed_skia_dir")"
echo "[build-ohos] Cloning official Skia $SKIA_TAG"
git clone --depth 1 --branch "$SKIA_TAG" https://github.com/JetBrains/skia.git "$managed_skia_dir"
fi
fi
"$PROJECT_ROOT/skia/apply_patch.sh" "$managed_skia_dir"
export OHOS_SDK_HOME="${OHOS_SDK_HOME:-$(dirname "$OHOS_NATIVE_DIR")}"
arm64_archive="$managed_skia_dir/Skia-$SKIA_TAG-ohos-Release-arm64.zip"
x64_archive="$managed_skia_dir/Skia-$SKIA_TAG-ohos-Release-x64.zip"
if [ ! -f "$arm64_archive" ] || [ ! -f "$x64_archive" ]; then
echo "[build-ohos] Building Skia $SKIA_TAG through prepareLocalSkiaBuild"
SKIA_TARGET=ohos SKIA_VERSION="$SKIA_TAG" \
./gradlew :skiko:prepareLocalSkiaBuild \
-Pskia.repo.dir="$managed_skia_dir" \
-Pskiko.awt.enabled=false \
-Pskiko.native.ohos.enabled=true
fi
SKIA_DIR="$managed_skia_dir"
export SKIA_DIR
echo "[build-ohos] Skia source: $SKIA_DIR"
}
# endregion
# fusionRenderer requires the fusion-render repository root (the directory containing OHRender/).
# Keep the legacy lookup order: OHRENDER_DIR, then the sibling fusion-render checkout.
if [ -z "${OHRENDER_DIR:-}" ]; then
OHRENDER_DIR="$(cd "$(dirname "$PROJECT_ROOT")" && pwd)/fusion-render"
fi
export OHRENDER_DIR
echo "[build-ohos] OHRENDER_DIR: $OHRENDER_DIR"
echo "[build-ohos] Publish repository: $PUBLISH_REPOSITORY"
# 避免 CI 未注入 BUILD_LOG 时导致 tee 空路径报错
BUILD_LOG="${BUILD_LOG:-$PROJECT_ROOT/build-ohos.log}"
mkdir -p "$(dirname "$BUILD_LOG")"
resolve_android_sdk() {
local default_sdk
if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}" ]; then
echo "${ANDROID_HOME}"
return 0
fi
if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "${ANDROID_SDK_ROOT}" ]; then
echo "${ANDROID_SDK_ROOT}"
return 0
fi
default_sdk="${HOME:-}/Library/Android/sdk"
if [ -n "${HOME:-}" ] && [ -d "$default_sdk" ]; then
echo "$default_sdk"
return 0
fi
return 1
}
ensure_android_local_properties() {
local sdk_path local_props tmp_file sdk_escaped input_file
if ! sdk_path="$(resolve_android_sdk)"; then
echo "[build-ohos] ERROR: Android SDK not found."
echo "[build-ohos] Please install Android SDK to: ${HOME:-<your_home>}/Library/Android/sdk"
echo "[build-ohos] Or set ANDROID_HOME / ANDROID_SDK_ROOT manually."
return 1
fi
export ANDROID_HOME="$sdk_path"
export ANDROID_SDK_ROOT="$sdk_path"
local_props="$PROJECT_ROOT/skiko/local.properties"
mkdir -p "$(dirname "$local_props")"
tmp_file="$(mktemp)"
sdk_escaped="$(printf '%s' "$sdk_path" | sed 's/:/\\:/g')"
input_file="$local_props"
[ -f "$local_props" ] || input_file="/dev/null"
awk -v sdk="$sdk_escaped" '
BEGIN { updated = 0 }
/^sdk\.dir=/ {
print "sdk.dir=" sdk
updated = 1
next
}
{ print }
END {
if (!updated) {
print "sdk.dir=" sdk
}
}
' "$input_file" > "$tmp_file"
mv "$tmp_file" "$local_props"
echo "[build-ohos] Android SDK: $sdk_path"
echo "[build-ohos] Ensured local.properties: $local_props"
}
# 默认沿用用户本机 Gradle 缓存(通常是 ~/.gradle),避免每次冷启动拉依赖。
# 仅在外部显式传入 GRADLE_USER_HOME 时使用自定义目录。
if [ -n "${GRADLE_USER_HOME:-}" ]; then
mkdir -p "$GRADLE_USER_HOME"
fi
# skiko/settings.gradle.kts 的 buildscript 默认会访问 JetBrains Space;内网或 Space 503 时改为跳过,
# 依赖需来自 Nexus、~/.m2 或 skiko/gradle/local-buildscript-m2(见 skiko/gradle.properties 注释)。
export SKIKO_BUILDSCRIPT_SKIP_JETBRAINS_SPACE="${SKIKO_BUILDSCRIPT_SKIP_JETBRAINS_SPACE:-true}"
# 增大 Gradle 访问仓库超时,降低网络抖动导致的误失败
GRADLE_NETWORK_OPTS="-Dorg.gradle.internal.http.connectionTimeout=${GRADLE_CONNECTION_TIMEOUT_MS:-60000} -Dorg.gradle.internal.http.socketTimeout=${GRADLE_SOCKET_TIMEOUT_MS:-120000}"
export GRADLE_OPTS="${GRADLE_OPTS:-} $GRADLE_NETWORK_OPTS"
is_dependency_download_error() {
local log_file="$1"
grep -Eiq \
'Could not resolve|Could not GET|Could not HEAD|Read timed out|Connection reset|Connection timed out|Temporary failure|503 Service Unavailable|502 Bad Gateway|Failed to fetch|ArtifactResolveException|Could not download' \
"$log_file"
}
run_publish_scheme() {
echo "[build-ohos] Pass 1/2: OHOS Skia"
prepare_ohos_skia || return 1
ensure_android_local_properties || return 1
if ! ./gradlew \
"$PUBLISH_OHOS_ARM64_TASK" \
"$PUBLISH_OHOS_X64_TASK" \
"$PUBLISH_KMP_TASK" \
-x :skiko:compileCommonMainKotlinMetadata \
-x :skiko:compileNativeJsMainKotlinMetadata \
-x :skiko:compileNativeMainKotlinMetadata \
-Pskiko.awt.enabled=false \
-Pskiko.android.enabled=true \
-Pskiko.js.enabled=false \
-Pskiko.wasm.enabled=false \
-Pskiko.native.enabled=false \
-Pskiko.native.mac.enabled=false \
-Pskiko.native.ios.enabled=false \
-Pskiko.native.ios.arm64.enabled=true \
-Pskiko.native.ios.simulatorArm64.enabled=true \
-Pskiko.native.ios.x64.enabled=false \
-Pskiko.native.ohos.enabled=true \
-Pskiko.native.ohos.arm64.enabled=true \
-Pskiko.native.ohos.x64.enabled=true \
-Pskiko.upstream.redirect.enabled=true \
-Pkotlin.stdlib.default.dependency=false \
-Pskia.dir="$SKIA_DIR" \
-Pdeploy.version="$DEPLOY_VERSION" \
-Pdeploy.release="$DEPLOY_RELEASE" \
--no-build-cache --rerun-tasks; then
return 1
fi
echo "[build-ohos] Pass 2/2: OHOS fusionRenderer"
if ! ./gradlew \
"$PUBLISH_OHOS_ARM64_TASK" \
"$PUBLISH_OHOS_X64_TASK" \
-Pskiko.awt.enabled=false \
-Pskiko.android.enabled=false \
-Pskiko.js.enabled=false \
-Pskiko.wasm.enabled=false \
-Pskiko.native.enabled=false \
-Pskiko.native.mac.enabled=false \
-Pskiko.native.ios.enabled=false \
-Pskiko.native.ohos.enabled=true \
-Pskiko.native.ohos.arm64.enabled=true \
-Pskiko.native.ohos.x64.enabled=true \
-Pskiko.native.ohos.fusionRenderer.enabled=true \
-Pohrender.dir="$OHRENDER_DIR" \
-Pdeploy.version="$DEPLOY_VERSION" \
-Pdeploy.release="$DEPLOY_RELEASE" \
--no-build-cache --rerun-tasks; then
return 1
fi
}
run_publish_skia_only() {
echo "[build-ohos] Mode: --skia (OHOS Skia)"
prepare_ohos_skia || return 1
ensure_android_local_properties || return 1
./gradlew \
"$PUBLISH_OHOS_ARM64_TASK" \
"$PUBLISH_OHOS_X64_TASK" \
"$PUBLISH_KMP_TASK" \
-x :skiko:compileCommonMainKotlinMetadata \
-x :skiko:compileNativeJsMainKotlinMetadata \
-x :skiko:compileNativeMainKotlinMetadata \
-Pskiko.awt.enabled=false \
-Pskiko.android.enabled=true \
-Pskiko.js.enabled=false \
-Pskiko.wasm.enabled=false \
-Pskiko.native.enabled=false \
-Pskiko.native.mac.enabled=false \
-Pskiko.native.ios.enabled=false \
-Pskiko.native.ios.arm64.enabled=true \
-Pskiko.native.ios.simulatorArm64.enabled=true \
-Pskiko.native.ios.x64.enabled=false \
-Pskiko.native.ohos.enabled=true \
-Pskiko.native.ohos.arm64.enabled=true \
-Pskiko.native.ohos.x64.enabled=true \
-Pskiko.upstream.redirect.enabled=true \
-Pkotlin.stdlib.default.dependency=false \
-Pskia.dir="$SKIA_DIR" \
-Pdeploy.version="$DEPLOY_VERSION" \
-Pdeploy.release="$DEPLOY_RELEASE" \
--no-build-cache --rerun-tasks
}
run_publish_fusion_renderer_only() {
echo "[build-ohos] Mode: --fusionRenderer (OHOS fusionRenderer)"
./gradlew \
"$PUBLISH_OHOS_ARM64_TASK" \
"$PUBLISH_OHOS_X64_TASK" \
-Pskiko.awt.enabled=false \
-Pskiko.android.enabled=false \
-Pskiko.js.enabled=false \
-Pskiko.wasm.enabled=false \
-Pskiko.native.enabled=false \
-Pskiko.native.mac.enabled=false \
-Pskiko.native.ios.enabled=false \
-Pskiko.native.ohos.enabled=true \
-Pskiko.native.ohos.arm64.enabled=true \
-Pskiko.native.ohos.x64.enabled=true \
-Pskiko.native.ohos.fusionRenderer.enabled=true \
-Pohrender.dir="$OHRENDER_DIR" \
-Pdeploy.version="$DEPLOY_VERSION" \
-Pdeploy.release="$DEPLOY_RELEASE" \
--no-build-cache --rerun-tasks
}
run_with_dependency_retry() {
local attempt=1
local max_attempts delay attempt_log
max_attempts="$RETRY_MAX_ATTEMPTS"
delay="$RETRY_DELAY_SECONDS"
while [ "$attempt" -le "$max_attempts" ]; do
attempt_log="$(mktemp)"
echo "[build-ohos] publish attempt ${attempt}/${max_attempts} (mode=${PUBLISH_MODE})"
if (
case "$PUBLISH_MODE" in
skia)
run_publish_skia_only
;;
fusionRenderer)
run_publish_fusion_renderer_only
;;
*)
run_publish_scheme
;;
esac
) 2>&1 | tee "$BUILD_LOG" "$attempt_log"; then
rm -f "$attempt_log"
return 0
fi
if [ "$attempt" -lt "$max_attempts" ] && is_dependency_download_error "$attempt_log"; then
echo "[build-ohos] 依赖下载疑似失败,${delay}s 后重试..."
rm -f "$attempt_log"
sleep "$delay"
attempt=$((attempt + 1))
continue
fi
rm -f "$attempt_log"
return 1
done
}
if run_with_dependency_retry; then
exit 0
fi
echo "[build-ohos] 构建失败。"
exit 1
# OHOS Skia m150
The OHOS Skia backend is based on JetBrains Skia tag
`m150-1f14f1166a` (`1f14f1166a847cb0e148f981600bf1e3e6ca6e46`).
The Skia checkout and build outputs are not part of this repository. The
complete source delta is stored in `patches/skia-m150-ohos.patch`.
## Build from a clean Skiko checkout
Run the OHOS build normally:
```bash
DEPLOY_VERSION=0.150.1-0.1.0-01 \
DEPLOY_RELEASE=true \
./scripts/build-ohos.sh
```
When `SKIA_DIR` is not set, the script automatically:
1. clones the official `m150-1f14f1166a` tag into the ignored
`dependencies/skia/m150-1f14f1166a` directory;
2. applies `patches/skia-m150-ohos.patch`;
3. delegates the OHOS arm64/x64 build to the upstream
`prepareLocalSkiaBuild` task; and
4. publishes Skiko using that checkout.
Subsequent builds reuse the checkout and compiled libraries.
## Optional: use an external checkout
Clone Skia in any external working directory, check out the exact m150 tag,
and apply the patch:
```bash
git clone https://github.com/JetBrains/skia.git /path/to/skia-m150
git -C /path/to/skia-m150 checkout m150-1f14f1166a
./skia/apply_patch.sh /path/to/skia-m150
```
Then point the normal build at it:
```bash
SKIA_DIR=/path/to/skia-m150 \
DEPLOY_VERSION=0.150.1-0.1.0-01 \
DEPLOY_RELEASE=true \
./scripts/build-ohos.sh
```
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
REPOSITORY_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SKIA_DIR="${1:-$REPOSITORY_ROOT/dependencies/skia/m150-1f14f1166a}"
PATCH_FILE="$SCRIPT_DIR/patches/skia-m150-ohos.patch"
EXPECTED_COMMIT="1f14f1166a847cb0e148f981600bf1e3e6ca6e46"
if [ ! -d "$SKIA_DIR/.git" ]; then
echo "[skia] ERROR: Skia checkout not found: $SKIA_DIR"
exit 1
fi
ACTUAL_COMMIT="$(git -C "$SKIA_DIR" rev-parse HEAD)"
if [ "$ACTUAL_COMMIT" != "$EXPECTED_COMMIT" ]; then
echo "[skia] ERROR: expected m150 commit $EXPECTED_COMMIT, found $ACTUAL_COMMIT"
exit 1
fi
if git -C "$SKIA_DIR" apply --reverse --check "$PATCH_FILE" >/dev/null 2>&1; then
echo "[skia] OHOS m150 patch is already applied."
exit 0
fi
git -C "$SKIA_DIR" apply --check "$PATCH_FILE"
git -C "$SKIA_DIR" apply "$PATCH_FILE"
echo "[skia] Applied OHOS m150 patch to $SKIA_DIR"
This diff is collapsed.
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class) @file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class)
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.Usage
import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.crypto.checksum.Checksum import org.gradle.crypto.checksum.Checksum
...@@ -8,13 +10,16 @@ import org.jetbrains.compose.internal.publishing.MavenCentralProperties ...@@ -8,13 +10,16 @@ import org.jetbrains.compose.internal.publishing.MavenCentralProperties
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import tasks.configuration.* import tasks.configuration.*
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinNativeCompilerOptions
import dsl.SkikoDependencyScope import dsl.SkikoDependencyScope
import java.util.Properties
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
...@@ -38,6 +43,9 @@ val buildType = skiko.buildType ...@@ -38,6 +43,9 @@ val buildType = skiko.buildType
val targetOs = hostOs val targetOs = hostOs
val targetArch = skiko.targetArch val targetArch = skiko.targetArch
val skikoArtifacts = SkikoArtifacts() val skikoArtifacts = SkikoArtifacts()
val upstreamRedirectEnabled = findProperty("skiko.upstream.redirect.enabled") == "true"
val upstreamKotlinVersion = providers.gradleProperty("skiko.upstream.kotlin.version").orElse("2.3.20")
val upstreamCoroutinesVersion = providers.gradleProperty("skiko.upstream.coroutines.version").orElse("1.8.0")
val coreDependencies: SkikoDependencyScope.() -> Unit = { val coreDependencies: SkikoDependencyScope.() -> Unit = {
targets { targets {
...@@ -201,6 +209,22 @@ allprojects { ...@@ -201,6 +209,22 @@ allprojects {
} }
repositories { repositories {
val composeRepoProperties = Properties().apply {
val propertiesFile = rootDir.parentFile.resolve("local.properties")
if (propertiesFile.isFile) propertiesFile.inputStream().use { load(it) }
}
composeRepoProperties.getProperty("COMPOSE_REPO_URL")
?.takeIf(String::isNotBlank)
?.let { repoUrl ->
maven(repoUrl) {
credentials {
username = composeRepoProperties.getProperty("COMPOSE_REPO_USERNAME")
password = composeRepoProperties.getProperty("COMPOSE_REPO_KEY")
}
}
}
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-inner/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral { mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central") url = uri("https://cache-redirector.jetbrains.com/maven-central")
} }
...@@ -302,6 +326,50 @@ kotlin { ...@@ -302,6 +326,50 @@ kotlin {
skikoProjectContext.configureNativeTarget(OS.Linux, Arch.X64, linuxX64()) skikoProjectContext.configureNativeTarget(OS.Linux, Arch.X64, linuxX64())
skikoProjectContext.configureNativeTarget(OS.Linux, Arch.Arm64, linuxArm64()) skikoProjectContext.configureNativeTarget(OS.Linux, Arch.Arm64, linuxArm64())
} }
if (supportNativeOhosArm64) {
skikoProjectContext.configureNativeTarget(OS.OHOS, Arch.Arm64, ohosArm64())
}
if (supportNativeOhosX64) {
skikoProjectContext.configureNativeTarget(OS.OHOS, Arch.X64, ohosX64())
}
if (findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true") {
val fusionModuleName = "org.jetbrains.skiko.fusion:skiko"
listOf("ohosArm64", "ohosX64").forEach { targetName ->
targets.findByName(targetName)?.compilations?.getByName("main")?.compileTaskProvider?.configure {
(compilerOptions as KotlinNativeCompilerOptions).moduleName.set(fusionModuleName)
}
}
}
val ohosSourceVariant = if (
findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true"
) "fusionRenderer" else "skia"
if (supportNativeOhosArm64) {
sourceSets.named("ohosArm64Main") {
kotlin.srcDir("src/ohosMain/kotlin")
kotlin.srcDir("src/ohosMain/$ohosSourceVariant/kotlin")
if (upstreamRedirectEnabled) {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}")
implementation(libs.coroutines.core)
}
}
}
}
if (supportNativeOhosX64) {
sourceSets.named("ohosX64Main") {
kotlin.srcDir("src/ohosMain/kotlin")
kotlin.srcDir("src/ohosMain/$ohosSourceVariant/kotlin")
if (upstreamRedirectEnabled) {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}")
implementation(libs.coroutines.core)
}
}
}
}
if (supportNativeIosArm64) { if (supportNativeIosArm64) {
skikoProjectContext.configureNativeTarget(OS.IOS, Arch.Arm64, iosArm64()) skikoProjectContext.configureNativeTarget(OS.IOS, Arch.Arm64, iosArm64())
} }
...@@ -322,8 +390,13 @@ kotlin { ...@@ -322,8 +390,13 @@ kotlin {
} }
sourceSets.commonMain.dependencies { sourceSets.commonMain.dependencies {
implementation(kotlin("stdlib")) if (upstreamRedirectEnabled) {
implementation(libs.coroutines.core) implementation("org.jetbrains.kotlin:kotlin-stdlib:${upstreamKotlinVersion.get()}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${upstreamCoroutinesVersion.get()}")
} else {
implementation(kotlin("stdlib"))
implementation(libs.coroutines.core)
}
} }
sourceSets.commonTest.dependencies { sourceSets.commonTest.dependencies {
...@@ -359,6 +432,17 @@ kotlin { ...@@ -359,6 +432,17 @@ kotlin {
} }
} }
//region Ohos
// AGP 9 creates Android as an external KMP target, which is not selected by
// this project's `withAndroidTarget()` hierarchy group. Connect the proxy
// target explicitly so commonMain has a JVM consumer during redirect builds.
if (supportAndroid && upstreamRedirectEnabled) {
sourceSets.named("androidMain") {
dependsOn(sourceSets.getByName("commonMain"))
}
}
//endregion
skikoProjectContext.jvmTestSourceSet?.dependencies { skikoProjectContext.jvmTestSourceSet?.dependencies {
implementation(libs.coroutines.test) implementation(libs.coroutines.test)
implementation(kotlin("test-junit")) implementation(kotlin("test-junit"))
...@@ -499,6 +583,60 @@ if (supportAndroid) { ...@@ -499,6 +583,60 @@ if (supportAndroid) {
skikoProjectContext.declarePublications() skikoProjectContext.declarePublications()
//region Ohos
// Android/iOS variants in the fork root metadata redirect to official m150
// target artifacts. Keep their shared metadata contract official as well; the
// locally compiled OHOS leaf klibs remain free to expose OHOS-only additions.
if (upstreamRedirectEnabled) {
val upstreamSkikoMetadataVersion = providers
.gradleProperty("skiko.upstream.redirect.version")
.orElse("0.150.1")
val upstreamSkikoMetadata by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
isTransitive = false
resolutionStrategy.useGlobalDependencySubstitutionRules = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-metadata"))
attribute(KotlinPlatformType.attribute, KotlinPlatformType.common)
}
}
val upstreamSkikoSources by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
isTransitive = false
resolutionStrategy.useGlobalDependencySubstitutionRules = false
}
dependencies {
upstreamSkikoMetadata(
"org.jetbrains.skiko:skiko:${upstreamSkikoMetadataVersion.get()}"
)
upstreamSkikoSources(
"org.jetbrains.skiko:skiko:${upstreamSkikoMetadataVersion.get()}:sources@jar"
)
}
tasks.named<Jar>("allMetadataJar") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from({ upstreamSkikoMetadata.files.map(::zipTree) }) {
include("commonMain/**")
include("nativeJsMain/**")
include("nativeMain/**")
}
}
val upstreamSkikoSourcesFile = upstreamSkikoSources.elements.map { it.single().asFile }
tasks.named<org.gradle.jvm.tasks.Jar>("sourcesJar") {
inputs.file(upstreamSkikoSourcesFile).withPropertyName("upstreamSkikoSources")
// The root publication redirects Android/iOS to official artifacts, so its
// source contract must match the official root as well. Keep this task and
// output path intact because KGP wires metadataSourcesElements to them.
doLast {
upstreamSkikoSourcesFile.get().copyTo(archiveFile.get().asFile, overwrite = true)
}
}
}
//endregion
val mavenCentral = MavenCentralProperties(project) val mavenCentral = MavenCentralProperties(project)
if (skiko.isTeamcityCIBuild || mavenCentral.signArtifacts) { if (skiko.isTeamcityCIBuild || mavenCentral.signArtifacts) {
signing { signing {
......
plugins { plugins {
alias(libs.plugins.kotlin.jvm)
`kotlin-dsl` `kotlin-dsl`
} }
...@@ -15,3 +16,10 @@ dependencies { ...@@ -15,3 +16,10 @@ dependencies {
implementation(libs.crypto.checksum.gradlePlugin) implementation(libs.crypto.checksum.gradlePlugin)
implementation(libs.kotlinx.benchmark.gradlePlugin) implementation(libs.kotlinx.benchmark.gradlePlugin)
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2)
apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2)
}
}
kotlin.version=2.3.20 kotlin.version=2.4.0-ST.0.4.0-0812
pluginManagement { pluginManagement {
repositories { repositories {
val composeRepoProperties = java.util.Properties().apply {
val propertiesFile = rootDir.parentFile.parentFile.resolve("local.properties")
if (propertiesFile.isFile) propertiesFile.inputStream().use(::load)
}
composeRepoProperties.getProperty("COMPOSE_REPO_URL")
?.takeIf(String::isNotBlank)
?.let { repoUrl ->
maven(repoUrl) {
credentials {
username = composeRepoProperties.getProperty("COMPOSE_REPO_USERNAME")
password = composeRepoProperties.getProperty("COMPOSE_REPO_KEY")
}
}
}
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-inner/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral { mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central") url = uri("https://cache-redirector.jetbrains.com/maven-central")
} }
...@@ -9,6 +25,22 @@ pluginManagement { ...@@ -9,6 +25,22 @@ pluginManagement {
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
val composeRepoProperties = java.util.Properties().apply {
val propertiesFile = rootDir.parentFile.parentFile.resolve("local.properties")
if (propertiesFile.isFile) propertiesFile.inputStream().use(::load)
}
composeRepoProperties.getProperty("COMPOSE_REPO_URL")
?.takeIf(String::isNotBlank)
?.let { repoUrl ->
maven(repoUrl) {
credentials {
username = composeRepoProperties.getProperty("COMPOSE_REPO_USERNAME")
password = composeRepoProperties.getProperty("COMPOSE_REPO_KEY")
}
}
}
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-inner/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://packages.jetbrains.team/maven/p/cmp/dev") { maven("https://packages.jetbrains.team/maven/p/cmp/dev") {
content { content {
includeGroupByRegex("org.jetbrains.compose.internal.*") includeGroupByRegex("org.jetbrains.compose.internal.*")
......
...@@ -30,6 +30,9 @@ abstract class CompileSkikoCppTask() : AbstractSkikoNativeToolTask() { ...@@ -30,6 +30,9 @@ abstract class CompileSkikoCppTask() : AbstractSkikoNativeToolTask() {
@get:Internal @get:Internal
abstract val sourceRoots: ListProperty<Directory> abstract val sourceRoots: ListProperty<Directory>
@get:Input
abstract val excludePatterns: ListProperty<String>
@get:Input @get:Input
abstract val compiler: Property<String> abstract val compiler: Property<String>
...@@ -43,7 +46,10 @@ abstract class CompileSkikoCppTask() : AbstractSkikoNativeToolTask() { ...@@ -43,7 +46,10 @@ abstract class CompileSkikoCppTask() : AbstractSkikoNativeToolTask() {
val sources = project.objects.fileCollection() val sources = project.objects.fileCollection()
for (sourceRoot in sourceRoots.get()) { for (sourceRoot in sourceRoots.get()) {
for (srcExtension in srcExtensions) { for (srcExtension in srcExtensions) {
sources.from(project.fileTree(sourceRoot) { include("**/*.$srcExtension") }) sources.from(project.fileTree(sourceRoot) {
include("**/*.$srcExtension")
exclude(excludePatterns.getOrElse(emptyList()))
})
} }
} }
sources sources
......
...@@ -7,6 +7,9 @@ enum class SkiaTarget( ...@@ -7,6 +7,9 @@ enum class SkiaTarget(
MACOS("macos", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")), MACOS("macos", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")),
WINDOWS("windows", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")), WINDOWS("windows", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")),
LINUX("linux", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")), LINUX("linux", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=true")),
// region Ohos
OHOS("ohos", listOf("-P${SkikoGradleProperties.AWT_ENABLED}=false")),
// endregion
WASM("wasm", listOf("-P${SkikoGradleProperties.WASM_ENABLED}=true", "-P${SkikoGradleProperties.AWT_ENABLED}=false")); WASM("wasm", listOf("-P${SkikoGradleProperties.WASM_ENABLED}=true", "-P${SkikoGradleProperties.AWT_ENABLED}=false"));
private val Arch.gradleProperty: String private val Arch.gradleProperty: String
...@@ -29,6 +32,9 @@ enum class SkiaTarget( ...@@ -29,6 +32,9 @@ enum class SkiaTarget(
MACOS -> if (hostArch == Arch.Arm64) listOf(Arch.Arm64, Arch.X64) else listOf(Arch.X64) MACOS -> if (hostArch == Arch.Arm64) listOf(Arch.Arm64, Arch.X64) else listOf(Arch.X64)
WINDOWS -> listOf(hostArch) WINDOWS -> listOf(hostArch)
LINUX -> listOf(hostArch) LINUX -> listOf(hostArch)
// region Ohos
OHOS -> listOf(Arch.Arm64, Arch.X64)
// endregion
WASM -> listOf(hostArch) WASM -> listOf(hostArch)
} }
...@@ -48,9 +54,12 @@ enum class SkiaTarget( ...@@ -48,9 +54,12 @@ enum class SkiaTarget(
"macos" -> MACOS "macos" -> MACOS
"windows" -> WINDOWS "windows" -> WINDOWS
"linux" -> LINUX "linux" -> LINUX
// region Ohos
"ohos" -> OHOS
// endregion
"wasm" -> WASM "wasm" -> WASM
else -> throw IllegalArgumentException( else -> throw IllegalArgumentException(
"Unknown SKIA_TARGET: $target. Valid targets: ios, iosSim, macos, windows, linux, wasm" "Unknown SKIA_TARGET: $target. Valid targets: ios, iosSim, macos, windows, linux, ohos, wasm"
) )
} }
} }
......
...@@ -95,6 +95,7 @@ fun SkikoProjectContext.declareSkiaTasks() { ...@@ -95,6 +95,7 @@ fun SkikoProjectContext.declareSkiaTasks() {
"tvosSim" to listOf("arm64", "x64"), "tvosSim" to listOf("arm64", "x64"),
"wasm" to listOf("wasm"), "wasm" to listOf("wasm"),
"windows" to listOf("arm64", "x64"), "windows" to listOf("arm64", "x64"),
"ohos" to listOf("arm64", "x64"),
).forEach { (config, architectures) -> ).forEach { (config, architectures) ->
architectures.forEach { arch -> architectures.forEach { arch ->
val taskNameSuffix = joinToTitleCamelCase(config, arch) val taskNameSuffix = joinToTitleCamelCase(config, arch)
...@@ -221,8 +222,17 @@ val Project.supportNativeMac: Boolean ...@@ -221,8 +222,17 @@ val Project.supportNativeMac: Boolean
val Project.supportNativeLinux: Boolean val Project.supportNativeLinux: Boolean
get() = supportAllNative || findProperty(SkikoGradleProperties.NATIVE_LINUX) == "true" || isInIdea get() = supportAllNative || findProperty(SkikoGradleProperties.NATIVE_LINUX) == "true" || isInIdea
val Project.supportNativeOhos: Boolean
get() = findProperty(SkikoGradleProperties.NATIVE_OHOS) == "true"
val Project.supportNativeOhosArm64: Boolean
get() = supportNativeOhos || findProperty(SkikoGradleProperties.NATIVE_OHOS_ARM64) == "true"
val Project.supportNativeOhosX64: Boolean
get() = supportNativeOhos || findProperty(SkikoGradleProperties.NATIVE_OHOS_X64) == "true"
val Project.supportAnyNative: Boolean val Project.supportAnyNative: Boolean
get() = supportAllNative || supportAnyNativeIos || supportNativeMac || supportNativeLinux get() = supportAllNative || supportAnyNativeIos || supportNativeMac || supportNativeLinux || supportNativeOhosArm64 || supportNativeOhosX64
val Project.supportWeb: Boolean val Project.supportWeb: Boolean
get() = findProperty(SkikoGradleProperties.WASM_ENABLED) == "true" || isInIdea get() = findProperty(SkikoGradleProperties.WASM_ENABLED) == "true" || isInIdea
......
...@@ -25,6 +25,8 @@ val OS.validEnvs: List<TargetEnv> ...@@ -25,6 +25,8 @@ val OS.validEnvs: List<TargetEnv>
OS.Windows, OS.Android -> listOf(TargetEnv.JVM) OS.Windows, OS.Android -> listOf(TargetEnv.JVM)
OS.IOS, OS.TVOS -> listOf(TargetEnv.NATIVE) OS.IOS, OS.TVOS -> listOf(TargetEnv.NATIVE)
OS.Wasm -> listOf(TargetEnv.WASM) OS.Wasm -> listOf(TargetEnv.WASM)
// OHOS uses its locally-built Skia archive list instead of the upstream binary DSL.
OS.OHOS -> emptyList()
} }
class BinaryRegistry { class BinaryRegistry {
......
...@@ -37,7 +37,12 @@ internal abstract class AbstractArgBuilder : ArgBuilder { ...@@ -37,7 +37,12 @@ internal abstract class AbstractArgBuilder : ArgBuilder {
} }
override fun createArgFile(file: File): String { override fun createArgFile(file: File): String {
file.writeLines(args) // Clang response files split unquoted whitespace even when the response
// file itself was passed as one Gradle process argument. Quote paths
// here so paths such as "Android Studio.app" stay a single argument.
file.writeLines(args.map { arg ->
if (" " in arg && !arg.startsWith('"')) "\"$arg\"" else arg
})
return "@${escapePathIfNeeded(file)}" return "@${escapePathIfNeeded(file)}"
} }
...@@ -143,4 +148,4 @@ internal class VisualCppLinkerArgBuilder : BaseVisualStudioBuildToolsArgBuilder( ...@@ -143,4 +148,4 @@ internal class VisualCppLinkerArgBuilder : BaseVisualStudioBuildToolsArgBuilder(
super.addTransformedArgs(argName, value) super.addTransformedArgs(argName, value)
} }
} }
} }
\ No newline at end of file
...@@ -13,7 +13,8 @@ enum class OS( ...@@ -13,7 +13,8 @@ enum class OS(
MacOS("macos", arrayOf("-mmacosx-version-min=11.0")), MacOS("macos", arrayOf("-mmacosx-version-min=11.0")),
Wasm("wasm", arrayOf()), Wasm("wasm", arrayOf()),
IOS("ios", arrayOf()), IOS("ios", arrayOf()),
TVOS("tvos", arrayOf()) TVOS("tvos", arrayOf()),
OHOS("ohos", arrayOf())
; ;
val isWindows val isWindows
...@@ -36,7 +37,7 @@ val OS.isCompatibleWithHost: Boolean ...@@ -36,7 +37,7 @@ val OS.isCompatibleWithHost: Boolean
OS.Windows -> hostOs == OS.Windows OS.Windows -> hostOs == OS.Windows
OS.MacOS, OS.IOS, OS.TVOS -> hostOs == OS.MacOS OS.MacOS, OS.IOS, OS.TVOS -> hostOs == OS.MacOS
OS.Wasm -> true OS.Wasm -> true
OS.Android -> true OS.Android, OS.OHOS -> true
} }
fun compilerForTarget(os: OS, arch: Arch): String = fun compilerForTarget(os: OS, arch: Arch): String =
...@@ -49,7 +50,7 @@ fun compilerForTarget(os: OS, arch: Arch): String = ...@@ -49,7 +50,7 @@ fun compilerForTarget(os: OS, arch: Arch): String =
} }
OS.Android -> "clang++" OS.Android -> "clang++"
OS.Windows -> "clang-cl.exe" OS.Windows -> "clang-cl.exe"
OS.MacOS, OS.IOS, OS.TVOS -> "clang++" OS.MacOS, OS.IOS, OS.TVOS, OS.OHOS -> "clang++"
OS.Wasm -> if (Os.isFamily(Os.FAMILY_WINDOWS)) "emcc.bat" else "emcc" OS.Wasm -> if (Os.isFamily(Os.FAMILY_WINDOWS)) "emcc.bat" else "emcc"
} }
...@@ -58,7 +59,7 @@ fun linkerForTarget(os: OS, arch: Arch): String = ...@@ -58,7 +59,7 @@ fun linkerForTarget(os: OS, arch: Arch): String =
val OS.dynamicLibExt: String val OS.dynamicLibExt: String
get() = when (this) { get() = when (this) {
OS.Linux, OS.Android -> ".so" OS.Linux, OS.Android, OS.OHOS -> ".so"
OS.Windows -> ".dll" OS.Windows -> ".dll"
OS.MacOS, OS.IOS, OS.TVOS -> ".dylib" OS.MacOS, OS.IOS, OS.TVOS -> ".dylib"
OS.Wasm -> ".wasm" OS.Wasm -> ".wasm"
...@@ -271,6 +272,9 @@ object SkikoGradleProperties { ...@@ -271,6 +272,9 @@ object SkikoGradleProperties {
const val NATIVE_TVOS_X64 = "skiko.native.tvos.x64.enabled" const val NATIVE_TVOS_X64 = "skiko.native.tvos.x64.enabled"
const val NATIVE_MAC = "skiko.native.mac.enabled" const val NATIVE_MAC = "skiko.native.mac.enabled"
const val NATIVE_LINUX = "skiko.native.linux.enabled" const val NATIVE_LINUX = "skiko.native.linux.enabled"
const val NATIVE_OHOS = "skiko.native.ohos.enabled"
const val NATIVE_OHOS_ARM64 = "skiko.native.ohos.arm64.enabled"
const val NATIVE_OHOS_X64 = "skiko.native.ohos.x64.enabled"
} }
class SkikoArtifacts( class SkikoArtifacts(
......
...@@ -56,7 +56,65 @@ fun SkikoProjectContext.declarePublications() { ...@@ -56,7 +56,65 @@ fun SkikoProjectContext.declarePublications() {
ctx.configureAndroidPublication() ctx.configureAndroidPublication()
ctx.configurePomNames() ctx.configurePomNames()
//region Ohos
// Keep Android and the supported iOS targets in the root Gradle module metadata,
// but route those variants to the official m150 artifacts. Only the OHOS target
// artifacts are built and published by this fork.
ctx.configureUpstreamTargetRedirects()
//endregion
if (project.findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true") {
val ohosArtifacts = artifacts
project.afterEvaluate {
publishing.publications.withType(MavenPublication::class.java).configureEach {
artifactId = when (name) {
"ohosArm64" -> ohosArtifacts.nativeArtifactIdFor(OS.OHOS, Arch.Arm64) + "-fusionrenderer"
"ohosX64" -> ohosArtifacts.nativeArtifactIdFor(OS.OHOS, Arch.X64) + "-fusionrenderer"
else -> artifactId
}
}
}
}
}
//region Ohos
private fun SkikoPublishingContext.configureUpstreamTargetRedirects() {
if (project.findProperty("skiko.upstream.redirect.enabled") != "true") return
val upstreamVersion = project.providers
.gradleProperty("skiko.upstream.redirect.version")
.orElse("0.150.1")
.get()
val redirectedPublicationNames = setOf(
"android",
"iosArm64",
"iosSimulatorArm64",
)
val redirectedMavenLocalTaskNames = setOf(
"publishAndroidPublicationToMavenLocal",
"publishIosArm64PublicationToMavenLocal",
"publishIosSimulatorArm64PublicationToMavenLocal",
)
publications {
withType(MavenPublication::class.java).configureEach {
if (name in redirectedPublicationNames) {
version = upstreamVersion
}
}
}
// These publications only provide coordinates for root-module `available-at`
// entries. Never publish local leaf artifacts under the official version.
project.tasks.configureEach {
if (name in redirectedMavenLocalTaskNames) {
enabled = false
}
}
} }
//endregion
private val SkikoPublishingContext.emptySourcesJar private val SkikoPublishingContext.emptySourcesJar
get() = project.tasks.registerOrGetTask<Jar>("emptySourcesJar") { get() = project.tasks.registerOrGetTask<Jar>("emptySourcesJar") {
......
...@@ -32,6 +32,10 @@ val skikoSourceSetHierarchyTemplate = KotlinHierarchyTemplate { ...@@ -32,6 +32,10 @@ val skikoSourceSetHierarchyTemplate = KotlinHierarchyTemplate {
} }
group("native") { group("native") {
//region Ohos
withOhos()
//endregion
group("linux") { group("linux") {
withLinux() withLinux()
} }
......
...@@ -17,7 +17,7 @@ internal class SymbolExtractor( ...@@ -17,7 +17,7 @@ internal class SymbolExtractor(
"Symbol extraction does not support ${type.name} symbols" "Symbol extraction does not support ${type.name} symbols"
} }
return when (os) { return when (os) {
OS.Linux, OS.Android, OS.MacOS, OS.IOS, OS.TVOS -> { OS.Linux, OS.Android, OS.OHOS, OS.MacOS, OS.IOS, OS.TVOS -> {
val nmCommand = nmCommand(type) val nmCommand = nmCommand(type)
val output = run(nmCommand.command, nmCommand.args + actualFiles.map { it.absolutePath }) val output = run(nmCommand.command, nmCommand.args + actualFiles.map { it.absolutePath })
parseNmPosix(output) parseNmPosix(output)
...@@ -44,7 +44,7 @@ internal class SymbolExtractor( ...@@ -44,7 +44,7 @@ internal class SymbolExtractor(
SymbolType.Undefined -> listOf("-P", "-u") SymbolType.Undefined -> listOf("-P", "-u")
SymbolType.DefinedGlobal -> when (os) { SymbolType.DefinedGlobal -> when (os) {
OS.MacOS, OS.IOS, OS.TVOS -> listOf("-P", "-g", "-U") OS.MacOS, OS.IOS, OS.TVOS -> listOf("-P", "-g", "-U")
OS.Linux, OS.Android -> listOf("-P", "-g", "--defined-only") OS.Linux, OS.Android, OS.OHOS -> listOf("-P", "-g", "--defined-only")
else -> error("nm symbol extraction does not support ${os.name} target") else -> error("nm symbol extraction does not support ${os.name} target")
} }
} }
......
...@@ -127,6 +127,9 @@ fun skiaPreprocessorFlags(os: OS, buildType: SkiaBuildType): Array<String> { ...@@ -127,6 +127,9 @@ fun skiaPreprocessorFlags(os: OS, buildType: SkiaBuildType): Array<String> {
OS.Android -> listOf( OS.Android -> listOf(
"-DSK_BUILD_FOR_ANDROID" "-DSK_BUILD_FOR_ANDROID"
) )
OS.OHOS -> listOf(
"-DSK_BUILD_FOR_OHOS"
)
} }
return (base + perOs).toTypedArray() return (base + perOs).toTypedArray()
......
...@@ -133,7 +133,7 @@ fun SkikoProjectContext.createCompileJvmBindingsTask( ...@@ -133,7 +133,7 @@ fun SkikoProjectContext.createCompileJvmBindingsTask(
"-fPIC" "-fPIC"
) )
} }
OS.Wasm, OS.IOS, OS.TVOS -> error("Should not reach here") OS.Wasm, OS.IOS, OS.TVOS, OS.OHOS -> error("Should not reach here")
} }
flags.set( flags.set(
...@@ -392,7 +392,7 @@ fun SkikoProjectContext.createLinkJvmBindings( ...@@ -392,7 +392,7 @@ fun SkikoProjectContext.createLinkJvmBindings(
osFlags = androidFlags.toTypedArray() osFlags = androidFlags.toTypedArray()
linker.set(project.androidClangFor(targetArch)) linker.set(project.androidClangFor(targetArch))
} }
OS.Wasm, OS.IOS, OS.TVOS -> { OS.Wasm, OS.IOS, OS.TVOS, OS.OHOS -> {
throw GradleException("This task shalln't be used with $targetOs") throw GradleException("This task shalln't be used with $targetOs")
} }
} }
...@@ -570,7 +570,7 @@ fun SkikoProjectContext.skikoRuntimeDirForTestsTask( ...@@ -570,7 +570,7 @@ fun SkikoProjectContext.skikoRuntimeDirForTestsTask(
additionalRuntimeLibraries.forEach { lib -> additionalRuntimeLibraries.forEach { lib ->
from(project.zipTree(lib.jarTask.flatMap { it.archiveFile })) 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
} }
......
...@@ -9,6 +9,7 @@ import SkikoProjectContext ...@@ -9,6 +9,7 @@ import SkikoProjectContext
import WriteCInteropDefFile import WriteCInteropDefFile
import compilerForTarget import compilerForTarget
import dsl.TargetEnv import dsl.TargetEnv
import dsl.ResolvedBinaryConfiguration
import hostArch import hostArch
import isCompatibleWithHost import isCompatibleWithHost
import joinToTitleCamelCase import joinToTitleCamelCase
...@@ -16,8 +17,10 @@ import mutableListOfLinkerOptions ...@@ -16,8 +17,10 @@ import mutableListOfLinkerOptions
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.getByName import org.gradle.kotlin.dsl.getByName
import org.gradle.kotlin.dsl.named
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
...@@ -29,6 +32,177 @@ import symbols.HideSkiaSymbolsTask ...@@ -29,6 +32,177 @@ import symbols.HideSkiaSymbolsTask
import java.io.File import java.io.File
import kotlin.collections.plus import kotlin.collections.plus
private fun ohosClangTarget(arch: Arch): String = when (arch) {
Arch.Arm64 -> "aarch64-linux-ohos"
Arch.X64 -> "x86_64-linux-ohos"
Arch.Wasm -> error("OHOS does not support Wasm")
}
private fun Project.ohosToolchainRoot(): File? =
(findProperty("ohos.llvm.dir")?.toString() ?: System.getenv("OHOS_LLVM"))
?.let(::File)
?.takeIf(File::isDirectory)
private fun Project.ohosSysroot(): File? =
(findProperty("ohos.sysroot")?.toString() ?: System.getenv("OHOS_SYSROOT"))
?.let(::File)
?.takeIf(File::isDirectory)
private fun Project.ohosCompiler(): String =
ohosToolchainRoot()?.resolve("bin/clang++")?.takeIf(File::canExecute)?.absolutePath ?: "clang++"
private fun Project.ohosToolchainFlags(): List<String> {
val toolchain = ohosToolchainRoot() ?: return emptyList()
val sysroot = ohosSysroot() ?: return emptyList()
val resourceDir = toolchain.resolve("lib/clang").listFiles()
?.filter(File::isDirectory)
?.maxByOrNull(File::getName)
?: error("Clang resource directory not found under ${toolchain.resolve("lib/clang")}")
return listOf(
"--sysroot=${sysroot.absolutePath}",
"-resource-dir=${resourceDir.absolutePath}",
"-isystem", toolchain.resolve("include/libcxx-ohos/include/c++/v1").absolutePath,
"-isystem", resourceDir.resolve("include").absolutePath,
"-DOHOS",
"-D__MUSL__",
"-D_LIBCPP_HAS_MUSL_LIBC",
"-D__BUILD_LINUX_WITH_CLANG",
"-D__GNU_SOURCE",
"-D_GNU_SOURCE",
)
}
private fun Project.isOhosFusionRendererEnabled(): Boolean =
findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true"
private fun Project.ohRenderRoot(): File {
val configured = findProperty("ohrender.dir")?.toString()?.takeIf(String::isNotBlank)
?: System.getenv("OHRENDER_DIR")?.takeIf(String::isNotBlank)
?: projectDir.resolve("../../fusion-render").absolutePath
val configuredDir = File(configured)
val root = if (configuredDir.name == "OHRender") configuredDir else configuredDir.resolve("OHRender")
require(root.isDirectory) {
"OHRender directory not found: $root. Set -Pohrender.dir or OHRENDER_DIR to the OHRender repository."
}
return root
}
private fun ohRenderHeadersDirs(root: File): List<File> = listOf(
root,
root.resolve("include"),
root.resolve("include/codec"),
root.resolve("include/core"),
root.resolve("include/effects"),
root.resolve("include/pathops"),
root.resolve("include/utils"),
root.resolve("modules/skcms"),
root.resolve("modules/skparagraph/include"),
root.resolve("modules/skresources/include"),
root.resolve("modules/skshaper/include"),
root.resolve("modules/skunicode/include"),
root.resolve("modules/sksg/include"),
root.resolve("modules/svg/include"),
root.resolve("src"),
root.resolve("third_party/icu"),
root.resolve("third_party/expat/expat/lib"),
root.resolve("third_party/include"),
)
private fun SkikoProjectContext.prepareFusionBridgeSourcesTask(): TaskProvider<Exec> = with(project) {
val output = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
val syncTask = if ("syncFusionBridgeSources" in tasks.names) {
tasks.named<Sync>("syncFusionBridgeSources")
} else {
tasks.register("syncFusionBridgeSources", Sync::class.java) {
from(projectDir) {
include("src/commonMain/cpp/common/**")
include("src/nativeJsMain/cpp/**")
}
into(output)
}
}
return if ("prepareFusionBridgeSources" in tasks.names) {
tasks.named<Exec>("prepareFusionBridgeSources")
} else {
tasks.register("prepareFusionBridgeSources", Exec::class.java) {
dependsOn(syncTask)
//region ohos
// The bridge sources and exported ABI come from Skiko m150. This patch only
// translates m150 implementation details to the older OHRender C++ API.
val patch = projectDir.resolve("src/ohosMain/fusionRenderer/patches/m150-to-ohrender.patch")
//endregion
inputs.file(patch)
outputs.dir(output)
workingDir(output)
commandLine("/usr/bin/patch", "-p1", "--forward", "--input=${patch.absolutePath}")
}
}
}
fun SkikoProjectContext.compileOhRenderTask(os: OS, arch: Arch): TaskProvider<CompileSkikoCppTask> =
with(project) {
require(os == OS.OHOS)
val root = ohRenderRoot()
registerSkikoTask<CompileSkikoCppTask>("compileOhRender", os, arch) {
compiler.set(ohosCompiler())
buildTargetOS.set(os)
buildTargetArch.set(arch)
buildVariant.set(buildType)
buildSuffix.set("fusion-ohrender")
flags.set(listOf(
"-target", ohosClangTarget(arch),
*ohosToolchainFlags().toTypedArray(),
*buildType.clangFlags,
//region ohos
// Keep SkRefCnt's inline ABI consistent with the Fusion bridge. In particular,
// Release must define NDEBUG; otherwise OHRender emits debug destructors while
// the bridge emits release-mode internal_dispose(), causing a ref-count abort.
*buildType.flags,
//endregion
"-fPIC", "-frtti", "-fno-exceptions",
"-DSK_BUILD_FOR_OHOS", "-DSK_TRACE",
))
sourceRoots.set(listOf(
root.resolve("src"),
root.resolve("modules/skcms"),
root.resolve("modules/skparagraph"),
root.resolve("modules/skresources"),
root.resolve("modules/svg/src"),
root.resolve("modules/skshaper/src"),
).map { layout.dir(provider { it }).get() })
excludePatterns.set(listOf(
"**/SkOHAvifCodec.cpp",
"**/SkShaper_harfbuzz.cpp",
))
includeHeadersNonRecursive(ohRenderHeadersDirs(root))
}
}
fun SkikoProjectContext.archiveOhRenderTask(
os: OS,
arch: Arch,
compileTask: TaskProvider<CompileSkikoCppTask>,
): TaskProvider<Exec> = with(project) {
require(os == OS.OHOS)
val targetString = "${os.id}-${arch.id}"
registerSkikoTask<Exec>("archiveOhRender", os, arch) {
dependsOn(compileTask)
val objectFiles = fileTree(compileTask.flatMap { it.outDir }) { include("**/*.o") }
inputs.files(objectFiles)
val outDir = layout.buildDirectory.dir("nativeFusion/static/$targetString")
val output = outDir.map { it.file("ohrender-$targetString.a") }
outputs.file(output)
doFirst { outDir.get().asFile.mkdirs() }
workingDir = outDir.get().asFile
executable = System.getenv("OHOS_LLVM_AR")
?: ohosToolchainRoot()?.resolve("bin/llvm-ar")?.absolutePath
?: "llvm-ar"
argumentProviders.add {
listOf("-crs", output.get().asFile.absolutePath) + objectFiles.files.map(File::getAbsolutePath)
}
}
}
fun String.withSuffix(isUikitSim: Boolean = false) = fun String.withSuffix(isUikitSim: Boolean = false) =
this + if (isUikitSim) "Sim" else "" this + if (isUikitSim) "Sim" else ""
...@@ -53,19 +227,26 @@ fun Project.findXcodeSdkRoot(): String { ...@@ -53,19 +227,26 @@ fun Project.findXcodeSdkRoot(): String {
fun SkikoProjectContext.compileNativeBridgesTask( fun SkikoProjectContext.compileNativeBridgesTask(
os: OS, arch: Arch, isUikitSim: Boolean os: OS, arch: Arch, isUikitSim: Boolean
): TaskProvider<CompileSkikoCppTask> = with (this.project) { ): TaskProvider<CompileSkikoCppTask> = with (this.project) {
val skiaNativeDir = registerOrGetSkiaDirProvider(os, arch, isUikitSim = isUikitSim) val isFusion = os == OS.OHOS && isOhosFusionRendererEnabled()
val skiaNativeDir = if (isFusion) null else registerOrGetSkiaDirProvider(os, arch, isUikitSim = isUikitSim)
val actionName = "compileNativeBridges".withSuffix(isUikitSim = isUikitSim) val actionName = (if (isFusion) "compileFusionNativeBridges" else "compileNativeBridges")
.withSuffix(isUikitSim = isUikitSim)
val fusionBridgeSources = if (isFusion) prepareFusionBridgeSourcesTask() else null
return project.registerSkikoTask<CompileSkikoCppTask>(actionName, os, arch) { return project.registerSkikoTask<CompileSkikoCppTask>(actionName, os, arch) {
dependsOn(skiaNativeDir) skiaNativeDir?.let { dependsOn(it) }
val unpackedSkia = skiaNativeDir.get() fusionBridgeSources?.let { dependsOn(it) }
val nativeHeadersRoot = if (isFusion) ohRenderRoot() else skiaNativeDir!!.get()
compiler.set(compilerForTarget(os, arch)) compiler.set(if (os == OS.OHOS) ohosCompiler() else compilerForTarget(os, arch))
buildTargetOS.set(os) buildTargetOS.set(os)
if (isUikitSim) { if (isUikitSim) {
buildSuffix.set("sim") buildSuffix.set("sim")
} }
if (isFusion) {
buildSuffix.set("fusion")
}
buildTargetArch.set(arch) buildTargetArch.set(arch)
buildVariant.set(buildType) buildVariant.set(buildType)
...@@ -120,15 +301,16 @@ fun SkikoProjectContext.compileNativeBridgesTask( ...@@ -120,15 +301,16 @@ fun SkikoProjectContext.compileNativeBridgesTask(
} }
OS.MacOS -> { OS.MacOS -> {
compiler.set(project.appleToolchainExecutableOrDefault("clang++", compiler.get())) compiler.set(project.appleToolchainExecutableOrDefault("clang++", compiler.get()))
val archFlags = when (arch) {
Arch.Arm64 -> arrayOf("-arch", "arm64")
Arch.X64 -> arrayOf("-arch", "x86_64")
else -> error("Unexpected arch: $arch for $os")
}
flags.set(listOf( flags.set(listOf(
*project.appleMacOsSdkFlags().toTypedArray(), *project.appleMacOsSdkFlags().toTypedArray(),
*buildType.clangFlags, *buildType.clangFlags,
*skiaPreprocessorFlags(OS.MacOS, buildType), *skiaPreprocessorFlags(OS.MacOS, buildType),
when(arch) { *archFlags
Arch.Arm64 -> "-arch arm64"
Arch.X64 -> "-arch x86_64"
else -> error("Unexpected arch: $arch for $os")
}
)) ))
} }
OS.Linux -> { OS.Linux -> {
...@@ -152,16 +334,72 @@ fun SkikoProjectContext.compileNativeBridgesTask( ...@@ -152,16 +334,72 @@ fun SkikoProjectContext.compileNativeBridgesTask(
} }
flags.set(linuxFlags) flags.set(linuxFlags)
} }
OS.OHOS -> flags.set(listOf(
"-target", ohosClangTarget(arch),
*ohosToolchainFlags().toTypedArray(),
*buildType.clangFlags,
"-fPIC", if (isFusion) "-frtti" else "-fno-rtti", "-fno-exceptions",
"-DSK_BUILD_FOR_OHOS",
*skiaPreprocessorFlags(OS.OHOS, buildType),
*(if (isFusion) arrayOf(
"-DUSE_OHRENDER", "-USK_GANESH", "-USK_GL", "-USK_UNICODE_AVAILABLE"
) else emptyArray())
))
else -> throw GradleException("$os not yet supported") else -> throw GradleException("$os not yet supported")
} }
val srcDirs = projectDirs("src/commonMain/cpp/common", "src/nativeNativeJs/cpp", "src/nativeJsMain/cpp") + val sharedSrcDirs = if (isFusion) {
if (skiko.includeTestHelpers) projectDirs("src/nativeJsTest/cpp") else emptyList() val compatRoot = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
listOf(
compatRoot.resolve("src/commonMain/cpp/common"),
compatRoot.resolve("src/nativeJsMain/cpp"),
).map { layout.dir(provider { it }).get() }
} else {
projectDirs("src/commonMain/cpp/common", "src/nativeNativeJs/cpp", "src/nativeJsMain/cpp")
}
val srcDirs = sharedSrcDirs +
(if (os == OS.OHOS) projectDirs("src/ohosMain/cpp") else emptyList()) +
(if (os == OS.OHOS) projectDirs(
"src/ohosMain/" +
(if (project.findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true")
"fusionRenderer" else "skia") + "/cpp"
) else emptyList()) +
(if (skiko.includeTestHelpers) projectDirs("src/nativeJsTest/cpp") else emptyList())
sourceRoots.set(srcDirs) sourceRoots.set(srcDirs)
includeHeadersNonRecursive(projectDir.resolve("src/nativeJsMain/cpp")) if (isFusion) {
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp/common/include")) excludePatterns.set(listOf(
includeHeadersNonRecursive(skiaHeadersDirs(unpackedSkia)) "**/DirectContext.cc",
"**/BackendRenderTarget.cc",
"**/BackendTexture.cc",
"**/RuntimeEffect.cc",
"**/RuntimeShaderBuilder.cc",
"**/render.cc",
"**/skottie/**",
"**/sksg/**",
"**/SVGCanvas.cc",
"**/GrGLMakeNativeInterface_ohos.cpp",
))
}
if (isFusion) {
val compatRoot = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
includeHeadersNonRecursive(compatRoot.resolve("src/nativeJsMain/cpp"))
includeHeadersNonRecursive(compatRoot.resolve("src/commonMain/cpp/common/include"))
} else {
includeHeadersNonRecursive(projectDir.resolve("src/nativeJsMain/cpp"))
includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp/common/include"))
}
if (os == OS.OHOS) includeHeadersNonRecursive(
projectDir.resolve(
"src/ohosMain/" +
(if (project.findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true")
"fusionRenderer" else "skia") + "/cpp"
)
)
includeHeadersNonRecursive(
if (isFusion) ohRenderHeadersDirs(nativeHeadersRoot) else skiaHeadersDirs(nativeHeadersRoot)
)
} }
} }
...@@ -192,19 +430,67 @@ fun configureCinterop( ...@@ -192,19 +430,67 @@ fun configureCinterop(
} }
} }
private fun ohosSkiaStaticLibraries(
skiaDir: String,
targetString: String,
buildType: SkiaBuildType,
): List<String> {
val skiaBinSubdir = "$skiaDir/out/${buildType.id}-$targetString"
val libraries = if (targetString.startsWith("ohos-")) listOf(
"libskresources.a",
"libskparagraph.a",
"libskia.a",
"libjsonreader.a",
"libskottie.a",
"libsksg.a",
"libskunicode_core.a",
"libskunicode_icu.a",
"libskshaper.a",
"libskcms.a",
"libwuffs.a",
) else listOf(
"libskresources.a",
"libskparagraph.a",
"libskia.a",
"libskia_ganesh_ext.a",
"libicu.a",
"libjsonreader.a",
"libskottie.a",
"libsvg.a",
"libpng.a",
"libwebp_sse41.a",
"libsksg.a",
"libskunicode_core.a",
"libskunicode_icu.a",
"libwebp.a",
"libdng_sdk.a",
"libpiex.a",
"libharfbuzz.a",
"libexpat.a",
"libzlib.a",
"libjpeg.a",
"libskshaper.a"
)
return libraries.map {
"$skiaBinSubdir/$it"
}
}
fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) = with(this.project) { fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) = with(this.project) {
if (!os.isCompatibleWithHost) return if (!os.isCompatibleWithHost) return
target.generateVersion(os, arch, skiko) target.generateVersion(os, arch, skiko)
val isUikitSim = target.isUikitSimulator() val isUikitSim = target.isUikitSimulator()
val isFusion = os == OS.OHOS && isOhosFusionRendererEnabled()
val targetString = "${os.idWithSuffix(isUikitSim = isUikitSim)}-${arch.id}" val targetString = "${os.idWithSuffix(isUikitSim = isUikitSim)}-${arch.id}"
val bridgeArtifactKey = if (isFusion) "fusion-$targetString" else targetString
val unzipper = if (isFusion) null else registerOrGetSkiaDirProvider(os, arch, isUikitSim)
val skiaDir = unzipper?.get()?.absolutePath
val unzipper = registerOrGetSkiaDirProvider(os, arch, isUikitSim) val bridgesLibrary = layout.buildDirectory.file(
val unpackedSkia = unzipper.get() "nativeBridges/static/$bridgeArtifactKey/$nativeBridgesLibPrefix-$bridgeArtifactKey.a"
val skiaDir = unpackedSkia.absolutePath )
val bridgesLibrary = layout.buildDirectory.file("nativeBridges/static/$targetString/$nativeBridgesLibPrefix-$targetString.a")
val bridgesLibraryPath = bridgesLibrary.get().asFile.absolutePath val bridgesLibraryPath = bridgesLibrary.get().asFile.absolutePath
// For iOS/tvOS we patch every library so that public Skia symbols are // For iOS/tvOS we patch every library so that public Skia symbols are
...@@ -215,10 +501,29 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -215,10 +501,29 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
val requiresSymbolPatching = os == OS.IOS || os == OS.TVOS val requiresSymbolPatching = os == OS.IOS || os == OS.TVOS
val patchedLibsDir = layout.buildDirectory.dir("nativeBridges/patched/$targetString").get().asFile val patchedLibsDir = layout.buildDirectory.dir("nativeBridges/patched/$targetString").get().asFile
val skiaBinDir = "$skiaDir/out/${buildType.id}-$targetString" val ohRenderCompile = if (isFusion) compileOhRenderTask(os, arch) else null
val resolvedBinaryInputs = resolveBinaryInputs(os, arch, TargetEnv.NATIVE, skiaBinDir) val ohRenderArchive = ohRenderCompile?.let { archiveOhRenderTask(os, arch, it) }
val nativeArchives = resolvedBinaryInputs.staticArchivePaths.distinct() val skiaBinDir = skiaDir?.let { "$it/out/${buildType.id}-$targetString" }
val allLibraries = if (requiresSymbolPatching) { val resolvedBinaryInputs = if (isFusion || os == OS.OHOS) {
ResolvedBinaryConfiguration()
} else {
resolveBinaryInputs(os, arch, TargetEnv.NATIVE, skiaBinDir!!)
}
val nativeArchives = if (os == OS.OHOS && !isFusion) {
ohosSkiaStaticLibraries(skiaDir!!, targetString, buildType)
} else {
resolvedBinaryInputs.staticArchivePaths.distinct()
}
val allLibraries = if (isFusion) {
val ohRenderRoot = ohRenderRoot()
val archPrefix = if (arch == Arch.Arm64) "aarch64" else "x86_64"
listOf(
bridgesLibraryPath,
layout.buildDirectory.file("nativeFusion/static/$targetString/ohrender-$targetString.a")
.get().asFile.absolutePath,
ohRenderRoot.resolve("third_party/libs/${archPrefix}_libexpat.a").absolutePath,
)
} else if (requiresSymbolPatching) {
nativeArchives.map { lib -> nativeArchives.map { lib ->
"${patchedLibsDir.absolutePath}/${File(lib).name}" "${patchedLibsDir.absolutePath}/${File(lib).name}"
} + "${patchedLibsDir.absolutePath}/$nativeBridgesLibPrefix-$targetString.a" } + "${patchedLibsDir.absolutePath}/$nativeBridgesLibPrefix-$targetString.a"
...@@ -290,6 +595,19 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -290,6 +595,19 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
options.add("--version-script=${hiddenSymbolsFile.get().asFile.absolutePath}") options.add("--version-script=${hiddenSymbolsFile.get().asFile.absolutePath}")
mutableListOfLinkerOptions(options) mutableListOfLinkerOptions(options)
} }
OS.OHOS -> mutableListOfLinkerOptions(if (isFusion) listOf(
"-target", ohosClangTarget(arch),
"-lace_napi.z", "-lhilog_ndk.z", "-lnative_drawing",
"-lpixelmap", "-lpixelmap_ndk.z", "-limage_source",
"-lhitrace_ndk.z", "-luv", "-limage_packer",
"-ldeviceinfo_ndk.z", "-lace_ndk.z", "-larkui_ndk", "-lz",
"-lrawfile.z", "-lohresmgr",
) else listOf(
"-target", ohosClangTarget(arch),
"-L$skiaBinDir", "-lGLESv3", "-lEGL", "-ldl", "-lhilog",
"-lrawfile.z", "-lohresmgr",
"$skiaBinDir/libskia.a", "$skiaBinDir/libskparagraph.a", "$skiaBinDir/libskshaper.a"
))
else -> mutableListOf() else -> mutableListOf()
} }
if (skiko.includeTestHelpers) { if (skiko.includeTestHelpers) {
...@@ -319,7 +637,8 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -319,7 +637,8 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
val crossCompileTask = compileNativeBridgesTask(os, arch, isUikitSim = isUikitSim) val crossCompileTask = compileNativeBridgesTask(os, arch, isUikitSim = isUikitSim)
// TODO: move to LinkSkikoTask. // TODO: move to LinkSkikoTask.
val actionName = "linkNativeBridges".withSuffix(isUikitSim = isUikitSim) val actionName = (if (isFusion) "linkFusionNativeBridges" else "linkNativeBridges")
.withSuffix(isUikitSim = isUikitSim)
val linkTask = project.registerSkikoTask<Exec>(actionName, os, arch) { val linkTask = project.registerSkikoTask<Exec>(actionName, os, arch) {
dependsOn(crossCompileTask) dependsOn(crossCompileTask)
val objectFilesDir = crossCompileTask.map { it.outDir.get() } val objectFilesDir = crossCompileTask.map { it.outDir.get() }
...@@ -327,12 +646,15 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -327,12 +646,15 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
include("**/*.o") include("**/*.o")
} }
inputs.files(objectFiles) inputs.files(objectFiles)
val outDir = layout.buildDirectory.dir("nativeBridges/static/$targetString").get().asFile val outDir = layout.buildDirectory.dir("nativeBridges/static/$bridgeArtifactKey").get().asFile
val staticLib = "$nativeBridgesLibPrefix-$targetString.a" val staticLib = "$nativeBridgesLibPrefix-$bridgeArtifactKey.a"
workingDir = outDir workingDir = outDir
when (os) { when (os) {
OS.Linux -> { OS.Linux, OS.OHOS -> {
executable = if (arch == Arch.Arm64 && hostArch != Arch.Arm64) "aarch64-linux-gnu-ar" else "ar" executable = if (arch == Arch.Arm64 && hostArch != Arch.Arm64) "aarch64-linux-gnu-ar" else "ar"
if (os == OS.OHOS) executable = System.getenv("OHOS_LLVM_AR")
?: ohosToolchainRoot()?.resolve("bin/llvm-ar")?.absolutePath
?: "llvm-ar"
argumentProviders.add { listOf("-crs", staticLib) } argumentProviders.add { listOf("-crs", staticLib) }
} }
OS.MacOS, OS.IOS, OS.TVOS -> { OS.MacOS, OS.IOS, OS.TVOS -> {
...@@ -350,7 +672,7 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -350,7 +672,7 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
val compilationDependency = if (requiresSymbolPatching) { val compilationDependency = if (requiresSymbolPatching) {
val patchActionName = "patchSkikoSymbols".withSuffix(isUikitSim = isUikitSim) val patchActionName = "patchSkikoSymbols".withSuffix(isUikitSim = isUikitSim)
project.registerSkikoTask<PatchSkiaSymbolsTask>(patchActionName, os, arch) { project.registerSkikoTask<PatchSkiaSymbolsTask>(patchActionName, os, arch) {
dependsOn(unzipper) dependsOn(unzipper!!)
dependsOn(linkTask) dependsOn(linkTask)
skiaLibs.set(nativeArchives.map { File(it) }) skiaLibs.set(nativeArchives.map { File(it) })
symbolSourceLibs.set(emptyList()) symbolSourceLibs.set(emptyList())
...@@ -362,13 +684,14 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin ...@@ -362,13 +684,14 @@ fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: Kotlin
} }
hideSkiaSymbols.configure { hideSkiaSymbols.configure {
dependsOn(unzipper) unzipper?.let { dependsOn(it) }
dependsOn(compilationDependency) dependsOn(compilationDependency)
} }
target.compilations.all { target.compilations.all {
compileTaskProvider.configure { compileTaskProvider.configure {
dependsOn(hideSkiaSymbols) dependsOn(hideSkiaSymbols)
ohRenderArchive?.let { dependsOn(it) }
} }
} }
} }
......
...@@ -5,11 +5,24 @@ kotlin.mpp.applyDefaultHierarchyTemplate=false ...@@ -5,11 +5,24 @@ kotlin.mpp.applyDefaultHierarchyTemplate=false
kotlin.mpp.enableCInteropCommonization=true kotlin.mpp.enableCInteropCommonization=true
kotlin.js.yarn.lock.file=false kotlin.js.yarn.lock.file=false
deploy.version=0.0.0 deploy.version=0.150.1-0.1.0-01
deploy.release=true
# a tag from https://github.com/JetBrains/skia/releases # a tag from https://github.com/JetBrains/skia/releases
dependencies.skia=m150-1f14f1166a dependencies.skia=m150-1f14f1166a
# OHOS is enabled explicitly by the command line so the default build remains portable.
skiko.native.ohos.enabled=false
skiko.native.ohos.arm64.enabled=false
skiko.native.ohos.x64.enabled=false
skiko.native.ohos.fusionRenderer.enabled=false
# Official m150 version used by Android/iOS `available-at` redirects in the
# fork root module. The redirect is enabled explicitly by build-ohos.sh.
skiko.upstream.redirect.version=0.150.1
skiko.upstream.kotlin.version=2.3.20
skiko.upstream.coroutines.version=1.8.0
# a tag from https://github.com/JetBrains/angle-pack # a tag from https://github.com/JetBrains/angle-pack
dependencies.angle=ec4d8f8e4d dependencies.angle=ec4d8f8e4d
......
dependencyResolutionManagement { dependencyResolutionManagement {
repositories {
val composeRepoProperties = java.util.Properties().apply {
val propertiesFile = rootDir.parentFile.resolve("local.properties")
if (propertiesFile.isFile) propertiesFile.inputStream().use(::load)
}
composeRepoProperties.getProperty("COMPOSE_REPO_URL")
?.takeIf(String::isNotBlank)
?.let { repoUrl ->
maven(repoUrl) {
credentials {
username = composeRepoProperties.getProperty("COMPOSE_REPO_USERNAME")
password = composeRepoProperties.getProperty("COMPOSE_REPO_KEY")
}
}
}
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-inner/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central")
}
google()
}
versionCatalogs { versionCatalogs {
register("libs") { register("libs") {
from(files("../dependencies.toml")) from(files("../dependencies.toml"))
...@@ -8,6 +30,22 @@ dependencyResolutionManagement { ...@@ -8,6 +30,22 @@ dependencyResolutionManagement {
pluginManagement { pluginManagement {
repositories { repositories {
val composeRepoProperties = java.util.Properties().apply {
val propertiesFile = rootDir.parentFile.resolve("local.properties")
if (propertiesFile.isFile) propertiesFile.inputStream().use(::load)
}
composeRepoProperties.getProperty("COMPOSE_REPO_URL")
?.takeIf(String::isNotBlank)
?.let { repoUrl ->
maven(repoUrl) {
credentials {
username = composeRepoProperties.getProperty("COMPOSE_REPO_USERNAME")
password = composeRepoProperties.getProperty("COMPOSE_REPO_KEY")
}
}
}
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-inner/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral { mavenCentral {
url = uri("https://cache-redirector.jetbrains.com/maven-central") url = uri("https://cache-redirector.jetbrains.com/maven-central")
} }
......
...@@ -9,11 +9,15 @@ ...@@ -9,11 +9,15 @@
#include "ports/SkTypeface_win.h" #include "ports/SkTypeface_win.h"
#endif #endif
#if (defined(SK_BUILD_FOR_UNIX) || defined(SK_BUILD_FOR_LINUX)) && !defined(SKIKO_WASM) #if (defined(SK_BUILD_FOR_UNIX) || defined(SK_BUILD_FOR_LINUX)) && !defined(SKIKO_WASM) && !defined(SK_BUILD_FOR_OHOS)
#include "ports/SkFontMgr_fontconfig.h" #include "ports/SkFontMgr_fontconfig.h"
#include "ports/SkFontScanner_FreeType.h" #include "ports/SkFontScanner_FreeType.h"
#endif #endif
#ifdef SK_BUILD_FOR_OHOS
SK_API sk_sp<SkFontMgr> SkFontMgr_New_OHOS(const char* path = nullptr);
#endif
#ifdef SK_BUILD_FOR_ANDROID #ifdef SK_BUILD_FOR_ANDROID
#include "ports/SkFontMgr_android.h" #include "ports/SkFontMgr_android.h"
#include "ports/SkFontScanner_FreeType.h" #include "ports/SkFontScanner_FreeType.h"
...@@ -37,7 +41,7 @@ sk_sp<SkFontMgr> SkFontMgrSkikoDefault() { ...@@ -37,7 +41,7 @@ sk_sp<SkFontMgr> SkFontMgrSkikoDefault() {
return SkFontMgr_New_DirectWrite(); return SkFontMgr_New_DirectWrite();
#endif #endif
#if (defined(SK_BUILD_FOR_UNIX) || defined(SK_BUILD_FOR_LINUX)) && !defined(SKIKO_WASM) #if (defined(SK_BUILD_FOR_UNIX) || defined(SK_BUILD_FOR_LINUX)) && !defined(SKIKO_WASM) && !defined(SK_BUILD_FOR_OHOS)
return SkFontMgr_New_FontConfig(nullptr, SkFontScanner_Make_FreeType()); return SkFontMgr_New_FontConfig(nullptr, SkFontScanner_Make_FreeType());
#endif #endif
...@@ -45,6 +49,10 @@ sk_sp<SkFontMgr> SkFontMgrSkikoDefault() { ...@@ -45,6 +49,10 @@ sk_sp<SkFontMgr> SkFontMgrSkikoDefault() {
return SkFontMgr_New_Android(nullptr, SkFontScanner_Make_FreeType()); return SkFontMgr_New_Android(nullptr, SkFontScanner_Make_FreeType());
#endif #endif
#ifdef SK_BUILD_FOR_OHOS
return SkFontMgr_New_OHOS();
#endif
#ifdef SKIKO_WASM #ifdef SKIKO_WASM
sk_sp<SkData> embeddedFontData = sk_sp<SkData> embeddedFontData =
SkData::MakeWithoutCopy(SK_EMBEDDED_FONTS.entries[0].data, SK_EMBEDDED_FONTS.entries[0].size); SkData::MakeWithoutCopy(SK_EMBEDDED_FONTS.entries[0].data, SK_EMBEDDED_FONTS.entries[0].size);
......
...@@ -10,6 +10,10 @@ open class Canvas internal constructor(ptr: NativePointer, managed: Boolean, int ...@@ -10,6 +10,10 @@ open class Canvas internal constructor(ptr: NativePointer, managed: Boolean, int
init { init {
staticLoad() staticLoad()
} }
//region Ohos
fun makeFromNative(ptr: NativePointer): Canvas = Canvas(ptr, false, Unit)
//endregion
} }
/** /**
* *
...@@ -1857,4 +1861,4 @@ internal inline fun Canvas.runRestoringState(block: Canvas.() -> Unit) { ...@@ -1857,4 +1861,4 @@ internal inline fun Canvas.runRestoringState(block: Canvas.() -> Unit) {
} finally { } finally {
restoreToCount(restoreCount) restoreToCount(restoreCount)
} }
} }
\ No newline at end of file
...@@ -7,6 +7,9 @@ enum class OS(val id: String) { ...@@ -7,6 +7,9 @@ enum class OS(val id: String) {
MacOS("macos"), MacOS("macos"),
Ios("ios"), Ios("ios"),
Tvos("tvos"), Tvos("tvos"),
//region ohos
Ohos("ohos"),
//endregion
Unknown("unknown") Unknown("unknown")
; ;
......
/*
* Copyright 2025-2025 Huawei Technologies Co., Ltd. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
#ifdef SK_BUILD_FOR_OHOS
#include "include/gpu/ganesh/gl/GrGLAssembleInterface.h"
#include "include/gpu/ganesh/gl/GrGLInterface.h"
#include "include/gpu/ganesh/gl/GrGLExtensions.h"
#include "src/gpu/ganesh/gl/GrGLCoreFunctions.h"
#include "src/gpu/ganesh/gl/GrGLUtil.h"
#include <cstring>
#include <dlfcn.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
static GrGLFuncPtr ohos_get_gl_proc(void* ctx, const char name[])
{
(void)ctx;
if (!name) {
return nullptr;
}
// Prefer EGL for extensions, but fall back to dlsym for core GLES symbols.
// This keeps core entry points like glGetString available even when
// eglGetProcAddress only resolves extensions.
GrGLFuncPtr proc = eglGetProcAddress(name);
if (proc) {
return proc;
}
return reinterpret_cast<GrGLFuncPtr>(dlsym(RTLD_DEFAULT, name));
}
sk_sp<const GrGLInterface> GrGLMakeNativeInterface_OHOS()
{
sk_sp<const GrGLInterface> interface = GrGLMakeAssembledInterface(nullptr, ohos_get_gl_proc);
if (!interface) {
return nullptr;
}
// Initialize GL extensions - this is CRITICAL for validation
// We need to cast away const to initialize extensions
GrGLInterface* mutableInterface = const_cast<GrGLInterface*>(interface.get());
if (!mutableInterface->fExtensions.isInitialized()) {
if (!mutableInterface->fExtensions.init(
mutableInterface->fStandard,
mutableInterface->fFunctions.fGetString,
mutableInterface->fFunctions.fGetStringi,
mutableInterface->fFunctions.fGetIntegerv)) {
return nullptr;
}
}
return interface;
}
#endif // SK_BUILD_FOR_OHOS
/*
* Copyright 2025-2025 Huawei Technologies Co., Ltd. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
#pragma once
#ifdef SK_BUILD_FOR_OHOS
#include "ganesh/gl/GrGLInterface.h"
// Declared in GrGLMakeNativeInterface_ohos.cpp
sk_sp<const GrGLInterface> GrGLMakeNativeInterface_OHOS();
#endif // SK_BUILD_FOR_OHOS
...@@ -81,7 +81,9 @@ SKIKO_EXPORT KBoolean org_jetbrains_skia_Region__1nSetRects(KNativePointer ptr, ...@@ -81,7 +81,9 @@ SKIKO_EXPORT KBoolean org_jetbrains_skia_Region__1nSetRects(KNativePointer ptr,
std::vector<SkIRect> rects(count); std::vector<SkIRect> rects(count);
for (int i = 0, off = 0; i < count; i++, off += 4) for (int i = 0, off = 0; i < count; i++, off += 4)
rects[i] = {coords[off], coords[off+1], coords[off+2], coords[off+3]}; rects[i] = {coords[off], coords[off+1], coords[off+2], coords[off+3]};
return instance->setRects({rects.data(), count}); // Keep the pointer/count overload: OHRender still exposes this signature, while m150 Skia
// retains it as a compatibility wrapper around the SkSpan overload.
return instance->setRects(rects.data(), count);
} }
SKIKO_EXPORT KBoolean org_jetbrains_skia_Region__1nSetRegion(KNativePointer ptr, KNativePointer regionPtr) { SKIKO_EXPORT KBoolean org_jetbrains_skia_Region__1nSetRegion(KNativePointer ptr, KNativePointer regionPtr) {
......
...@@ -237,4 +237,3 @@ private class RunHandlerImpl(val runHandler: RunHandler) : Managed(RunHandler_nC ...@@ -237,4 +237,3 @@ private class RunHandlerImpl(val runHandler: RunHandler) : Managed(RunHandler_nC
val PTR = RunHandler_nGetFinalizer() val PTR = RunHandler_nGetFinalizer()
} }
} }
...@@ -10,6 +10,9 @@ actual val hostOs: OS by lazy { ...@@ -10,6 +10,9 @@ actual val hostOs: OS by lazy {
OsFamily.WINDOWS -> OS.Windows OsFamily.WINDOWS -> OS.Windows
OsFamily.IOS -> OS.Ios OsFamily.IOS -> OS.Ios
OsFamily.TVOS -> OS.Tvos OsFamily.TVOS -> OS.Tvos
//region ohos
OsFamily.OHOS -> OS.Ohos
//endregion
else -> throw Error("Unsupported OS ${Platform.osFamily}") else -> throw Error("Unsupported OS ${Platform.osFamily}")
} }
} }
...@@ -28,4 +31,4 @@ actual val hostId by lazy { ...@@ -28,4 +31,4 @@ actual val hostId by lazy {
} }
actual val kotlinBackend: KotlinBackend actual val kotlinBackend: KotlinBackend
get() = KotlinBackend.Native get() = KotlinBackend.Native
\ No newline at end of file
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <memory>
#include <tuple>
#include <utility>
#include "SkCodec.h"
#include "SkData.h"
#include "SkImage.h"
#include "SkRect.h"
#include "common.h"
#include "rawfile/raw_file_manager.h"
#include "resourcemanager/ohresmgr.h"
namespace {
constexpr KInt kDefaultScale = 0;
constexpr KInt kCropScale = 1;
constexpr KInt kFitScale = 2;
constexpr KInt kInsideScale = 3;
constexpr KInt kFillBoundsScale = 4;
constexpr KInt kFillWidthScale = 5;
constexpr KInt kFillHeightScale = 6;
constexpr KInt kNoneScale = 7;
struct TargetDecodePlan {
int width;
int height;
bool crop;
SkIRect subset;
};
void ReleaseMallocData(const void* data, void*) {
std::free(const_cast<void*>(data));
}
int CeilToBucket(int value, int step) {
if (value <= 0) return 0;
const int safeStep = step > 0 ? step : 16;
const int64_t bucket =
((static_cast<int64_t>(value) + safeStep - 1) / safeStep) * safeStep;
return static_cast<int>(std::min<int64_t>(bucket, std::numeric_limits<int>::max()));
}
int CeilScaledDimension(int source, double scale) {
if (!std::isfinite(scale) || scale <= 0.0) return 1;
return std::max(1, static_cast<int>(std::ceil(std::min<double>(source * scale, source))));
}
bool BuildTargetDecodePlan(const SkImageInfo& sourceInfo,
KInt targetWidth,
KInt targetHeight,
KInt contentScaleMode,
KInt decodeSizeBucket,
TargetDecodePlan* result) {
const int sourceWidth = sourceInfo.width();
const int sourceHeight = sourceInfo.height();
if (result == nullptr || sourceWidth <= 0 || sourceHeight <= 0 ||
targetWidth <= 0 || targetHeight <= 0) {
return false;
}
const double widthScale = static_cast<double>(targetWidth) / sourceWidth;
const double heightScale = static_cast<double>(targetHeight) / sourceHeight;
double scaleX = widthScale;
double scaleY = heightScale;
switch (contentScaleMode) {
case kCropScale: {
const double scale = std::max(widthScale, heightScale);
scaleX = scale;
scaleY = scale;
break;
}
case kFitScale: {
const double scale = std::min(widthScale, heightScale);
scaleX = scale;
scaleY = scale;
break;
}
case kInsideScale: {
const double scale = std::min(1.0, std::min(widthScale, heightScale));
scaleX = scale;
scaleY = scale;
break;
}
case kFillWidthScale:
scaleX = widthScale;
scaleY = widthScale;
break;
case kFillHeightScale:
scaleX = heightScale;
scaleY = heightScale;
break;
case kNoneScale:
scaleX = 1.0;
scaleY = 1.0;
break;
case kDefaultScale:
case kFillBoundsScale:
break;
default:
return false;
}
const int requestedWidth = CeilScaledDimension(sourceWidth, scaleX);
const int requestedHeight = CeilScaledDimension(sourceHeight, scaleY);
int sampleSize = 1;
while (sampleSize <= std::numeric_limits<int>::max() / 2 &&
sourceWidth / (sampleSize * 2) >= requestedWidth &&
sourceHeight / (sampleSize * 2) >= requestedHeight) {
sampleSize *= 2;
}
const int sampledWidth = std::max(1, sourceWidth / sampleSize);
const int sampledHeight = std::max(1, sourceHeight / sampleSize);
const bool forceExactOutput =
contentScaleMode == kCropScale || contentScaleMode == kFillBoundsScale;
int outputWidth = forceExactOutput
? targetWidth
: std::min(sampledWidth, CeilToBucket(targetWidth, decodeSizeBucket));
int outputHeight = forceExactOutput
? targetHeight
: std::min(sampledHeight, CeilToBucket(targetHeight, decodeSizeBucket));
outputWidth = std::max(1, std::min(outputWidth, std::numeric_limits<int>::max()));
outputHeight = std::max(1, std::min(outputHeight, std::numeric_limits<int>::max()));
if (outputWidth >= sourceWidth && outputHeight >= sourceHeight) {
outputWidth = sourceWidth;
outputHeight = sourceHeight;
}
result->width = outputWidth;
result->height = outputHeight;
result->crop = contentScaleMode == kCropScale;
result->subset = SkIRect::MakeWH(sourceWidth, sourceHeight);
if (result->crop) {
const double sourceRatio = static_cast<double>(sourceWidth) / sourceHeight;
const double targetRatio = static_cast<double>(outputWidth) / outputHeight;
if (sourceRatio > targetRatio) {
const double cropWidth = sourceHeight * targetRatio;
const int left = std::max(0, static_cast<int>((sourceWidth - cropWidth) / 2.0));
const int right = std::min(sourceWidth, static_cast<int>(std::ceil(left + cropWidth)));
result->subset = SkIRect::MakeLTRB(left, 0, std::max(left + 1, right), sourceHeight);
} else {
const double cropHeight = sourceWidth / targetRatio;
const int top = std::max(0, static_cast<int>((sourceHeight - cropHeight) / 2.0));
const int bottom = std::min(sourceHeight, static_cast<int>(std::ceil(top + cropHeight)));
result->subset = SkIRect::MakeLTRB(0, top, sourceWidth, std::max(top + 1, bottom));
}
}
return true;
}
sk_sp<SkData> MakeRawFileData(void* nativeResourceManager, const char* rawFilePath) {
if (nativeResourceManager == nullptr || rawFilePath == nullptr || rawFilePath[0] == '\0') {
return nullptr;
}
auto* manager = static_cast<NativeResourceManager*>(nativeResourceManager);
RawFile* rawFile = OH_ResourceManager_OpenRawFile(manager, rawFilePath);
if (rawFile == nullptr) return nullptr;
RawFileDescriptor descriptor = {};
if (OH_ResourceManager_GetRawFileDescriptorData(rawFile, &descriptor) &&
descriptor.fd >= 0 && descriptor.start >= 0 && descriptor.length > 0) {
sk_sp<SkData> hapData = SkData::MakeFromFD(descriptor.fd);
const size_t start = static_cast<size_t>(descriptor.start);
const size_t length = static_cast<size_t>(descriptor.length);
const bool validRange = hapData != nullptr && start <= hapData->size() &&
length <= hapData->size() - start;
OH_ResourceManager_ReleaseRawFileDescriptorData(&descriptor);
OH_ResourceManager_CloseRawFile(rawFile);
if (validRange) {
return SkData::MakeSubset(hapData.get(), start, length);
}
return nullptr;
}
const long rawFileSize = OH_ResourceManager_GetRawFileSize(rawFile);
if (rawFileSize <= 0) {
OH_ResourceManager_CloseRawFile(rawFile);
return nullptr;
}
const size_t dataLength = static_cast<size_t>(rawFileSize);
void* data = std::malloc(dataLength);
if (data == nullptr) {
OH_ResourceManager_CloseRawFile(rawFile);
return nullptr;
}
const int bytesRead = OH_ResourceManager_ReadRawFile(rawFile, data, dataLength);
OH_ResourceManager_CloseRawFile(rawFile);
if (bytesRead <= 0 || static_cast<size_t>(bytesRead) != dataLength) {
std::free(data);
return nullptr;
}
return SkData::MakeWithProc(data, dataLength, ReleaseMallocData, nullptr);
}
sk_sp<SkData> MakeMediaData(void* nativeResourceManager, KInt mediaResId) {
if (nativeResourceManager == nullptr || mediaResId <= 0) return nullptr;
uint8_t* data = nullptr;
uint64_t dataLength = 0;
const auto error = OH_ResourceManager_GetMediaData(
static_cast<NativeResourceManager*>(nativeResourceManager),
static_cast<uint32_t>(mediaResId),
&data,
&dataLength,
0);
if (error != SUCCESS || data == nullptr || dataLength == 0 ||
dataLength > std::numeric_limits<size_t>::max()) {
std::free(data);
return nullptr;
}
return SkData::MakeWithProc(
data,
static_cast<size_t>(dataLength),
ReleaseMallocData,
nullptr);
}
KNativePointer DecodeResourceImage(sk_sp<SkData> encodedData,
KInt targetWidth,
KInt targetHeight,
KInt contentScaleMode,
KInt decodeSizeBucket) {
if (encodedData == nullptr) return 0;
if (targetWidth == 0 && targetHeight == 0) {
sk_sp<SkImage> image = SkImages::DeferredFromEncodedData(encodedData);
return reinterpret_cast<KNativePointer>(image.release());
}
if (targetWidth <= 0 || targetHeight <= 0) return 0;
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(std::move(encodedData));
if (codec == nullptr) return 0;
const SkImageInfo sourceInfo = codec->getInfo();
TargetDecodePlan plan = {};
if (!BuildTargetDecodePlan(
sourceInfo,
targetWidth,
targetHeight,
contentScaleMode,
decodeSizeBucket,
&plan)) {
return 0;
}
SkCodec::Options options;
if (plan.crop) options.fSubset = &plan.subset;
const SkImageInfo targetInfo = sourceInfo.makeWH(plan.width, plan.height);
auto decoded = codec->getImage(targetInfo, plan.crop ? &options : nullptr);
sk_sp<SkImage> image = std::move(std::get<0>(decoded));
if (std::get<1>(decoded) != SkCodec::kSuccess || image == nullptr ||
image->width() != plan.width || image->height() != plan.height) {
return 0;
}
return reinterpret_cast<KNativePointer>(image.release());
}
} // namespace
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromEncodedOhosTargetSize(
KByte* encodedArray,
KInt encodedLength,
KInt desiredWidth,
KInt desiredHeight,
KInt cropX,
KInt cropY,
KInt cropWidth,
KInt cropHeight) {
if (encodedArray == nullptr || encodedLength <= 0 || desiredWidth <= 0 || desiredHeight <= 0) {
return 0;
}
sk_sp<SkData> encodedData = SkData::MakeWithCopy(encodedArray, encodedLength);
if (encodedData == nullptr) {
return 0;
}
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromData(std::move(encodedData));
if (codec == nullptr) {
return 0;
}
const SkImageInfo sourceInfo = codec->getInfo();
const bool hasCrop = cropWidth > 0 && cropHeight > 0;
const bool hasPartialCrop = cropX != 0 || cropY != 0 || cropWidth != 0 || cropHeight != 0;
if (hasPartialCrop && !hasCrop) {
return 0;
}
SkIRect subset;
SkCodec::Options options;
if (hasCrop) {
if (cropX < 0 || cropY < 0 || cropX >= sourceInfo.width() || cropY >= sourceInfo.height() ||
cropWidth > sourceInfo.width() - cropX || cropHeight > sourceInfo.height() - cropY) {
return 0;
}
subset = SkIRect::MakeXYWH(cropX, cropY, cropWidth, cropHeight);
options.fSubset = &subset;
}
const SkImageInfo targetInfo = sourceInfo.makeWH(desiredWidth, desiredHeight);
auto decoded = codec->getImage(targetInfo, hasCrop ? &options : nullptr);
sk_sp<SkImage> image = std::move(std::get<0>(decoded));
if (std::get<1>(decoded) != SkCodec::kSuccess || image == nullptr ||
image->width() != desiredWidth || image->height() != desiredHeight) {
return 0;
}
return reinterpret_cast<KNativePointer>(image.release());
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromRawFileOhos(
void* nativeResourceManager,
const char* rawFilePath,
KInt targetWidth,
KInt targetHeight,
KInt contentScaleMode,
KInt decodeSizeBucket) {
return DecodeResourceImage(
MakeRawFileData(nativeResourceManager, rawFilePath),
targetWidth,
targetHeight,
contentScaleMode,
decodeSizeBucket);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Image__1nMakeFromMediaOhos(
void* nativeResourceManager,
KInt mediaResId,
KInt targetWidth,
KInt targetHeight,
KInt contentScaleMode,
KInt decodeSizeBucket) {
return DecodeResourceImage(
MakeMediaData(nativeResourceManager, mediaResId),
targetWidth,
targetHeight,
contentScaleMode,
decodeSizeBucket);
}
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <atomic>
#include <arkui/native_node_napi.h>
#include <arkui/native_interface.h>
#include "common.h"
#include "Utils.h"
#include "include/oh/OHRenderNode.h"
#include "SkCanvas.h"
static ArkUI_NativeNodeAPI_1* GetNativeNodeAPI() {
static ArkUI_NativeNodeAPI_1* s_api = nullptr;
if (s_api == nullptr) {
OH_ArkUI_GetModuleInterface(ARKUI_NATIVE_NODE, ArkUI_NativeNodeAPI_1, s_api);
}
return s_api;
}
// Mounts an ArkUI_NodeHandle into a NodeContent container
static void mountHandleToNodeContent(napi_env env, napi_value nodeContent, ArkUI_NodeHandle handle) {
ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi = GetNativeNodeAPI();
if (arkUINativeNodeApi == nullptr) {
Error("mountHandleToNodeContent: Failed to get ArkUI_NativeNodeAPI_1");
return;
}
ArkUI_NumberValue value[] = {{.f32 = 1.0f}};
ArkUI_AttributeItem item = {value, 1};
arkUINativeNodeApi->setAttribute(handle, NODE_WIDTH_PERCENT, &item);
arkUINativeNodeApi->setAttribute(handle, NODE_HEIGHT_PERCENT, &item);
ArkUI_NodeContentHandle contentHandle;
OH_ArkUI_GetNodeContentFromNapiValue(env, nodeContent, &contentHandle);
OH_ArkUI_NodeContent_AddNode(contentHandle, handle);
}
class ExternalRenderNode {
public:
std::shared_ptr<OHRenderNode> ptr;
ArkUI_NodeHandle nodeHandle = nullptr;
// Type 1: ArkTS path — wrap ArkTS FrameNode directly via CreateExternalNode.
explicit ExternalRenderNode(napi_value node) {
ptr = OHRenderNode::CreateExternalNode(node);
}
// Type 2: C-API path — wrap outerFrameNode in a CUSTOM parent node via CreateExternalNodeC.
ExternalRenderNode(napi_env env, int32_t rootNodeId, int32_t extNodeId, napi_value node)
: ExternalRenderNode(rootNodeId, extNodeId, [&] {
ArkUI_NodeHandle handle = nullptr;
OH_ArkUI_GetNodeHandleFromNapiValue(env, node, &handle);
return handle;
}()) {}
ExternalRenderNode(int32_t rootNodeId, int32_t extNodeId, ArkUI_NodeHandle handle) {
this->nodeHandle = handle;
ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi = GetNativeNodeAPI();
if (arkUINativeNodeApi == nullptr) {
Error("ExternalRenderNode: Failed to get ArkUI_NativeNodeAPI_1");
return;
}
ArkUI_NodeHandle handleExtra = arkUINativeNodeApi->createNode(ARKUI_NODE_CUSTOM);
arkUINativeNodeApi->addChild(handleExtra, handle);
RootRenderNodeManager::Instance().AdoptNode(rootNodeId, handleExtra);
ptr = OHRenderNode::CreateExternalNodeC(handleExtra);
std::weak_ptr<OHRenderNode> weakNode = ptr;
RootRenderNodeManager::Instance().MapAdoptNode(rootNodeId, extNodeId, weakNode);
}
~ExternalRenderNode() {
// C-API path only (Type 2/3): nodeHandle is the user's outer FrameNode (JS-owned),
// which we wrapped under a C-side handleExtra in the constructor:
//
// handleExtra (C-API CUSTOM, owned by RootRenderNodeManager.adoptedChildren_)
// └── nodeHandle == outerFrameNode (JS-owned)
// └── ArkUIView (JS-owned, has its own .dispose())
//
// Detach ArkUIView from outerFrameNode here, before either side runs its own
// teardown. If we leave the C-side parent/child link in place, the GC ordering
// between (a) ArkUIView.dispose() in ets, (b) JS GC of outerFrameNode, and
// (c) RootRenderNodeManager.UnregisterNode -> disposeNode(handleExtra) is
// unspecified, and any disposeNode walking through stale child pointers
// crashes. Breaking the link on the Main thread (cleaner schedules here) lets
// each owner clean up its own subgraph in isolation.
//
// NOTE: handleExtra itself is not freed here — it is reaped only when its
// root node is unregistered via RootRenderNodeManager.UnregisterNode (see
// OHRender's RenderNodeImplC.cpp UnregisterNodeInternal). There is no
// per-extNode unadopt API today, so handleExtra entries accumulate inside
// adoptedChildren_/adoptedNodeMap_ for the lifetime of the rootNodeId.
//
// Type 1 (ArkTS) keeps nodeHandle == nullptr and skips this branch entirely.
if (this->nodeHandle != nullptr) {
ArkUI_NativeNodeAPI_1 *arkUINativeNodeApi = GetNativeNodeAPI();
if (arkUINativeNodeApi != nullptr) {
arkUINativeNodeApi->removeAllChildren(this->nodeHandle);
}
this->nodeHandle = nullptr;
}
ptr = nullptr;
}
};
static void freeExternalRenderNode(ExternalRenderNode* node) {
delete node;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_ExternalRenderNode__1nMakeForArkTs
(napi_value node) {
ExternalRenderNode* renderNode = new ExternalRenderNode(node);
if (renderNode->ptr == nullptr) {
delete renderNode;
return nullptr;
}
return reinterpret_cast<KNativePointer>(renderNode);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_ExternalRenderNode__1nMakeForCApi
(napi_env env, int32_t rootNodeId, int32_t extNodeId, napi_value node) {
ExternalRenderNode* renderNode = new ExternalRenderNode(env, rootNodeId, extNodeId, node);
if (renderNode->ptr == nullptr) {
delete renderNode;
return nullptr;
}
return reinterpret_cast<KNativePointer>(renderNode);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_ExternalRenderNode__1nMakeFromHandle
(int32_t rootNodeId, int32_t extNodeId, ArkUI_NodeHandle handle)
{
ExternalRenderNode* renderNode = new ExternalRenderNode(rootNodeId, extNodeId, handle);
if (renderNode->ptr == nullptr) {
delete renderNode;
return nullptr;
}
return reinterpret_cast<KNativePointer>(renderNode);
}
SKIKO_EXPORT void org_jetbrains_skia_ExternalRenderNode__1nMountHandleToNodeContent
(napi_env env, napi_value nodeContent, ArkUI_NodeHandle handle) {
mountHandleToNodeContent(env, nodeContent, handle);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_ExternalRenderNode__1nGetExternalRenderNodeFinalizer() {
return reinterpret_cast<KNativePointer>(freeExternalRenderNode);
}
SKIKO_EXPORT void org_jetbrains_skia_Canvas__1nDrawRenderNode
(SkCanvas* canvas, ExternalRenderNode* node, KFloat left, KFloat top, KFloat right, KFloat bottom) {
if (canvas == nullptr || node == nullptr) {
return;
}
SkRect rect{left, top, right, bottom};
canvas->drawRenderNode(node->ptr, rect);
}
SKIKO_EXPORT void org_jetbrains_skia_ExternalRenderNode__1nMapFrameNodePos
(int32_t rootNodeId, int32_t extNodeId, double touchX, double touchY,
float* outX, float* outY) {
SkPoint gPoint = SkPoint::Make(touchX, touchY);
SkPoint lPoint = SkPoint::Make(0, 0);
if (auto node = RootRenderNodeManager::Instance().GetAdoptOHRenderNode(rootNodeId, extNodeId)) {
node->mapPointFromGlobal(gPoint, &lPoint);
}
*outX = lPoint.x();
*outY = lPoint.y();
}
SKIKO_EXPORT void org_jetbrains_skia_ExternalRenderNode__1nMapPointFromGlobal
(ExternalRenderNode* renderNode, double globalX, double globalY,
float* outX, float* outY) {
SkPoint gPoint = SkPoint::Make((float)globalX, (float)globalY);
SkPoint lPoint = SkPoint::Make(0, 0);
if (renderNode != nullptr && renderNode->ptr != nullptr) {
renderNode->ptr->mapPointFromGlobal(gPoint, &lPoint);
}
*outX = lPoint.x();
*outY = lPoint.y();
}
#include <chrono>
#include <vector>
#include "SkString.h"
#include "common.h"
//region ohos
// The Fusion renderer uses OHRender instead of Ganesh and OHRender is built
// without SkSL, Skottie and several advanced typeface APIs. Keep the complete
// m150 native ABI linkable and return deterministic unsupported values here.
// This turns missing-symbol/IrLinkageError crashes into ordinary null results
// (or a descriptive RuntimeEffect compilation error).
static void deleteUnsupportedBackendTexture(KNativePointer) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_BackendTexture__1nGetFinalizer() {
return reinterpret_cast<KNativePointer>(&deleteUnsupportedBackendTexture);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_BackendTexture__1nMakeGL
(KInt, KInt, KBoolean, KInt, KInt, KInt) {
return nullptr;
}
SKIKO_EXPORT void org_jetbrains_skia_BackendTexture__1nGLTextureParametersModified
(KNativePointer) {
}
SKIKO_EXPORT KNativePointer BackendRenderTarget_nMakeMetal
(KInt, KInt, KNativePointer) {
return nullptr;
}
SKIKO_EXPORT KNativePointer BackendRenderTarget_MakeDirect3D
(KInt, KInt, KNativePointer, KInt, KInt, KInt) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_DirectContext__1nMakeMetal
(KNativePointer, KNativePointer) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_DirectContext__1nMakeDirect3D
(KNativePointer, KNativePointer, KNativePointer) {
return nullptr;
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nFlush
(KNativePointer, KNativePointer) {
}
SKIKO_EXPORT KLong org_jetbrains_skia_DirectContext__1nGetResourceCacheLimit
(KNativePointer) {
return 0;
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nSetResourceCacheLimit
(KNativePointer, KLong) {
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nFlushAndSubmit
(KNativePointer, KNativePointer, KBoolean) {
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nReset
(KNativePointer, KInt) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Surface__1nMakeRenderTarget
(KNativePointer, KBoolean, KInt, KInt, KInt, KInt, KNativePointer,
KInt, KInt, KInteropPointer, KBoolean) {
return nullptr;
}
struct UnsupportedRuntimeEffectResult {
SkString error = SkString("RuntimeEffect is not supported by OHRender Fusion Renderer");
};
static KNativePointer makeUnsupportedRuntimeEffectResult() {
return reinterpret_cast<KNativePointer>(new UnsupportedRuntimeEffectResult());
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeShader
(KNativePointer, KNativePointer, KNativePointerArray, KInt, KFloat*) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeBlender
(KNativePointer, KNativePointer) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeForShader
(KInteropPointer) {
return makeUnsupportedRuntimeEffectResult();
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeForColorFilter
(KInteropPointer) {
return makeUnsupportedRuntimeEffectResult();
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeForBlender
(KInteropPointer) {
return makeUnsupportedRuntimeEffectResult();
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1Result_nGetPtr
(KNativePointer) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1Result_nGetError
(KNativePointer ptr) {
auto* result = reinterpret_cast<UnsupportedRuntimeEffectResult*>(ptr);
return result == nullptr ? nullptr : reinterpret_cast<KNativePointer>(&result->error);
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeEffect__1Result_nDestroy
(KNativePointer ptr) {
delete reinterpret_cast<UnsupportedRuntimeEffectResult*>(ptr);
}
static void deleteUnsupportedRuntimeShaderBuilder(KNativePointer) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeShaderBuilder__1nGetFinalizer() {
return reinterpret_cast<KNativePointer>(&deleteUnsupportedRuntimeShaderBuilder);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeShaderBuilder__1nMakeFromRuntimeEffect
(KNativePointer) {
return nullptr;
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformInt
(KNativePointer, KInteropPointer, KInt) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformInt2
(KNativePointer, KInteropPointer, KInt, KInt) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformInt3
(KNativePointer, KInteropPointer, KInt, KInt, KInt) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformInt4
(KNativePointer, KInteropPointer, KInt, KInt, KInt, KInt) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloat
(KNativePointer, KInteropPointer, KFloat) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloat2
(KNativePointer, KInteropPointer, KFloat, KFloat) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloat3
(KNativePointer, KInteropPointer, KFloat, KFloat, KFloat) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloat4
(KNativePointer, KInteropPointer, KFloat, KFloat, KFloat, KFloat) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloatArray
(KNativePointer, KInteropPointer, KFloat*, int32_t) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloatMatrix22
(KNativePointer, KInteropPointer, KFloat*) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloatMatrix33
(KNativePointer, KInteropPointer, KFloat*) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nUniformFloatMatrix44
(KNativePointer, KInteropPointer, KFloat*) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nChildShader
(KNativePointer, KInteropPointer, KNativePointer) {
}
SKIKO_EXPORT void org_jetbrains_skia_RuntimeShaderBuilder__1nChildColorFilter
(KNativePointer, KInteropPointer, KNativePointer) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeShaderBuilder__1nMakeShader
(KNativePointer, KFloat*) {
return nullptr;
}
SKIKO_EXPORT void org_jetbrains_skia_Typeface__1nGetUTF32Glyphs
(KNativePointer, KInt*, KInt count, KShort* result) {
for (KInt index = 0; index < count; ++index) result[index] = 0;
}
SKIKO_EXPORT KShort org_jetbrains_skia_Typeface__1nGetUTF32Glyph
(KNativePointer, KInt) {
return 0;
}
SKIKO_EXPORT KInt org_jetbrains_skia_Typeface__1nGetGlyphsCount
(KNativePointer) {
return 0;
}
SKIKO_EXPORT KInt org_jetbrains_skia_Typeface__1nGetTablesCount
(KNativePointer) {
return 0;
}
SKIKO_EXPORT KInt org_jetbrains_skia_Typeface__1nGetTableTagsCount
(KNativePointer) {
return 0;
}
SKIKO_EXPORT void org_jetbrains_skia_Typeface__1nGetTableTags
(KNativePointer, KInt*, KInt) {
}
SKIKO_EXPORT KInt org_jetbrains_skia_Typeface__1nGetTableSize
(KNativePointer, KInt) {
return 0;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Typeface__1nGetTableData
(KNativePointer, KInt) {
return nullptr;
}
SKIKO_EXPORT KInt org_jetbrains_skia_Typeface__1nGetUnitsPerEm
(KNativePointer) {
return 0;
}
SKIKO_EXPORT bool org_jetbrains_skia_Typeface__1nGetKerningPairAdjustments
(KNativePointer, KInteropPointer, KInt, KInt*) {
return false;
}
SKIKO_EXPORT KInteropPointer org_jetbrains_skia_Typeface__1nGetFamilyNames
(KNativePointer) {
return reinterpret_cast<KInteropPointer>(new std::vector<KInteropPointer>());
}
SKIKO_EXPORT void org_jetbrains_skia_Typeface__1nGetBounds
(KNativePointer, KFloat* bounds) {
bounds[0] = 0;
bounds[1] = 0;
bounds[2] = 0;
bounds[3] = 0;
}
SKIKO_EXPORT KLong org_jetbrains_skia_OHRenderNode__1nGetCurrentTimeMicros() {
return static_cast<KLong>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()
).count());
}
SKIKO_EXPORT KInt skikobridge_napi_get_system_theme() {
return 0;
}
//endregion
#include "SkPath.h"
#include "SkRRect.h"
#include "common.h"
//region ohos
// m150 exposes immutable Path factories backed by newer SkSpan-based Skia APIs.
// OHRender uses the older pointer/count overloads, so keep the Kotlin ABI and
// translate only the native construction calls here.
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromRaw
(KFloat* ptsArray, KInt ptsCount,
KByte* verbsArray, KInt verbsCount,
KFloat* conicWeightsArray, KInt conicWeightsCount,
KInt fillType, KBoolean isVolatile) {
SkPath path = SkPath::Make(
reinterpret_cast<const SkPoint*>(ptsArray), ptsCount,
reinterpret_cast<const uint8_t*>(verbsArray), verbsCount,
conicWeightsArray, conicWeightsCount,
static_cast<SkPathFillType>(fillType),
isVolatile
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromRect
(KFloat left, KFloat top, KFloat right, KFloat bottom,
KInt fillType, KInt direction, KInt startIndex) {
SkPath path = SkPath::Rect(
SkRect::MakeLTRB(left, top, right, bottom),
static_cast<SkPathDirection>(direction),
startIndex
);
path.setFillType(static_cast<SkPathFillType>(fillType));
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromOval
(KFloat left, KFloat top, KFloat right, KFloat bottom,
KInt direction, KInt startIndex) {
SkPath path = SkPath::Oval(
SkRect::MakeLTRB(left, top, right, bottom),
static_cast<SkPathDirection>(direction),
startIndex
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromCircle
(KFloat centerX, KFloat centerY, KFloat radius, KInt direction) {
SkPath path = SkPath::Circle(
centerX,
centerY,
radius,
static_cast<SkPathDirection>(direction)
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromRRect
(KFloat* radiiArray,
KFloat left, KFloat top, KFloat right, KFloat bottom,
KInt direction, KInt startIndex) {
SkRRect rrect;
rrect.setRectRadii(
SkRect::MakeLTRB(left, top, right, bottom),
reinterpret_cast<const SkVector*>(radiiArray)
);
SkPath path = SkPath::RRect(
rrect,
static_cast<SkPathDirection>(direction),
startIndex
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromRRectXY
(KFloat left, KFloat top, KFloat right, KFloat bottom,
KFloat rx, KFloat ry, KInt direction) {
SkPath path = SkPath::RRect(
SkRect::MakeLTRB(left, top, right, bottom),
rx,
ry,
static_cast<SkPathDirection>(direction)
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Path__1nMakeFromPolygon
(KFloat* ptsArray, KInt ptsCount,
KBoolean isClosed, KInt fillType, KBoolean isVolatile) {
SkPath path = SkPath::Polygon(
reinterpret_cast<const SkPoint*>(ptsArray), ptsCount,
isClosed,
static_cast<SkPathFillType>(fillType),
isVolatile
);
return reinterpret_cast<KNativePointer>(new SkPath(path));
}
//endregion
#include "common.h"
//region ohos
// Fusion Renderer owns its rendering context and surface through OHRender. Keep
// the public Skiko Ganesh entry points link-safe, but report them as unavailable
// instead of leaving lazy native symbols that crash when called.
static void deleteUnsupportedBackendRenderTarget(KNativePointer) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_BackendRenderTarget__1nGetFinalizer() {
return reinterpret_cast<KNativePointer>(&deleteUnsupportedBackendRenderTarget);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_BackendRenderTarget__1nMakeGL
(KInt width, KInt height, KInt sampleCnt, KInt stencilBits, KInt fbId, KInt fbFormat) {
return nullptr;
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_DirectContext__1nMakeGL() {
return nullptr;
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nFlushDefault(KNativePointer ptr) {
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nSubmit
(KNativePointer ptr, KBoolean syncCpu) {
}
SKIKO_EXPORT void org_jetbrains_skia_DirectContext__1nAbandon
(KNativePointer ptr, KInt flags) {
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Surface__1nMakeFromBackendRenderTarget
(KNativePointer pContext, KNativePointer pBackendRenderTarget, KInt surfaceOrigin,
KInt colorType, KNativePointer colorSpacePtr, KInteropPointer surfacePropsInts) {
return nullptr;
}
//endregion
#include <iostream>
#include "SkCanvas.h"
#include "SkRRect.h"
#include "common.h"
#include "include/oh/OHRenderNode.h"
SKIKO_EXPORT KNativePointer org_jetbrains_skia_OHRenderNode__1nCreateRootRenderNode
(KNativePointer context, void* callback) {
auto cb = reinterpret_cast<NodeDrawCallback>(callback);
auto node = reinterpret_cast<KNativePointer>(OHRenderNode::CreateRootRenderNode(context, cb));
return node;
}
SKIKO_EXPORT void org_jetbrains_skia_OHRenderNode__1nSetNodeConstructor
(napi_env env, napi_value fun) {
OHRenderNode::SetNodeConstructor(env, fun);
return;
}
SKIKO_EXPORT void org_jetbrains_skia_OHRenderNode__1nRenderNodeDraw
(napi_env env, napi_value jsCanvas, napi_value jsNode) {
OHRenderNode::RenderNodeDraw(env, jsCanvas, jsNode);
return;
}
SKIKO_EXPORT void org_jetbrains_skia_OHRenderNode__1nRenderNodeNotifyRedraw
(napi_env env, napi_value jsNode) {
OHRenderNode::RenderNodeNotifyRedraw(env, jsNode);
return;
}
SKIKO_EXPORT void org_jetbrains_skia_OHRenderNode__1nSetPixelRatio
(double ratio) {
OHRenderNode::SetPixelRatio(ratio);
return;
}
SKIKO_EXPORT void org_jetbrains_skia_OHRenderNode__1nBuildInstance
(napi_env env, napi_value fun) {
NodeStatusModify::BuildInstance(env, fun);
return;
}
#include <atomic>
#include <arkui/native_node_napi.h>
#include "common.h"
#include "Utils.h"
#include "include/oh/OHRenderNode.h"
typedef void (*RenderNodeDrawCallback)(KInt, KNativePointer);
static RenderNodeDrawCallback drawCallback = nullptr;
static std::atomic<int32_t> renderNodeID(0);
class IRenderNode {
public:
virtual void notifyRedraw() = 0;
virtual int32_t getID() = 0;
};
class JsRenderNode : IRenderNode {
private:
const napi_env env;
const int32_t id;
napi_ref nodeRef = nullptr;
public:
JsRenderNode(napi_env env, int32_t id) : env(env), id(id) {};
~JsRenderNode() {
if (nodeRef != nullptr) {
napi_delete_reference(env, nodeRef);
nodeRef = nullptr;
}
}
static void freeJsRenderNode(JsRenderNode* node) {
delete node;
}
static void jsNodeDrawCallback(void *context, void *canvas) {
if (drawCallback == nullptr) {
Error("jsNodeDrawCallback: empty drawCallback");
return;
}
JsRenderNode* renderNode = reinterpret_cast<JsRenderNode*>(context);
drawCallback(renderNode->getID(), canvas);
}
void createJsNode() {
if (nodeRef == nullptr) {
napi_value jsNode = OHRenderNode::CreateRootRenderNode(this, &jsNodeDrawCallback);
napi_status status = napi_create_reference(env, jsNode, 1, &nodeRef);
if (status != napi_ok) {
Error("JsRenderNode createJsNode failed: createReference failed");
nodeRef = nullptr;
}
}
}
napi_value getJsNode() {
if (nodeRef == nullptr) {
Error("JsRenderNode getJsNode failed: empty nodeRef");
return nullptr;
}
napi_value jsNode;
napi_status status = napi_get_reference_value(env, nodeRef, &jsNode);
if (status != napi_ok) {
Error("JsRenderNode getJsNode failed: getReference failed");
return nullptr;
}
return jsNode;
}
void notifyRedraw() override {
napi_handle_scope scope = nullptr;
napi_open_handle_scope(env, &scope);
const napi_value jsNode = getJsNode();
if (jsNode == nullptr) {
Error("JsRenderNode notifyRedraw failed: empty jsNode");
return;
}
OHRenderNode::RenderNodeNotifyRedraw(env, jsNode);
napi_close_handle_scope(env, scope);
}
int32_t getID() override {
return id;
}
void draw(napi_value canvas) {
napi_handle_scope scope = nullptr;
napi_open_handle_scope(env, &scope);
const napi_value jsNode = getJsNode();
if (jsNode == nullptr) {
Error("JsRenderNode draw failed: empty jsNode");
return;
}
OHRenderNode::RenderNodeDraw(env, canvas, jsNode);
napi_close_handle_scope(env, scope);
}
};
class CRenderNode : IRenderNode {
private:
const int32_t id;
const ArkUI_NodeHandle node;
public:
static void cRenderNodeDraw(int32_t id, void* canvas) {
if (drawCallback == nullptr) {
Error("cNodeDrawCallback: empty drawCallback");
return;
}
drawCallback(id, canvas);
}
CRenderNode(int32_t id): id(id), node(OHRenderNode::CreateRootRenderNodeWithoutEvent(id, &cRenderNodeDraw)) {};
~CRenderNode() {
RootRenderNodeManager::Instance().UnregisterNode(id);
}
void notifyRedraw() override {
RootRenderNodeManager::Instance().NotifyRedraw(id);
}
int32_t getID() override {
return id;
}
int32_t attach(ArkUI_NodeContentHandle parent) {
return OH_ArkUI_NodeContent_AddNode(parent, node);
}
ArkUI_NodeHandle getNodeHandle() {
return node;
}
void reSize(int32_t width, int32_t height) {
RootRenderNodeManager::Instance().Resize(id, width, height);
}
static void freeCRenderNode(CRenderNode* node) {
delete node;
}
};
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nSetDrawCallback
(void* callback) {
drawCallback = reinterpret_cast<RenderNodeDrawCallback>(callback);
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nSetNodeConstructor
(napi_env env, napi_value function) {
OHRenderNode::SetNodeConstructor(env, function);
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nSetPixelRatio
(double ratio) {
OHRenderNode::SetPixelRatio(ratio);
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nNodeStatusModifyBuildInstance
(napi_env env, napi_value function) {
NodeStatusModify::BuildInstance(env, function);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RenderNode__1nCreateJsRenderNode
(napi_env env) {
JsRenderNode* node = new JsRenderNode(env, ++renderNodeID);
node->createJsNode();
return reinterpret_cast<KNativePointer>(node);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RenderNode__1nGetJsNodeFinalizer() {
return reinterpret_cast<KNativePointer>((&JsRenderNode::freeJsRenderNode));
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nRenderNodeNotifyRedraw
(void* ptr) {
IRenderNode* node = reinterpret_cast<IRenderNode*>(ptr);
node->notifyRedraw();
}
SKIKO_EXPORT KInt org_jetbrains_skia_RenderNode__1nRenderNodeGetNodeId
(void* ptr) {
IRenderNode* node = reinterpret_cast<IRenderNode*>(ptr);
return node->getID();
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nRenderNodeDraw
(void* ptr, napi_value canvas) {
JsRenderNode* node = reinterpret_cast<JsRenderNode*>(ptr);
node->draw(canvas);
}
SKIKO_EXPORT napi_value org_jetbrains_skia_RenderNode__1nRenderNodeGetJsNode
(void* ptr) {
JsRenderNode* node = reinterpret_cast<JsRenderNode*>(ptr);
return node->getJsNode();
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nRenderNodeJsNodeDraw
(napi_env env, napi_value node, napi_value canvas) {
OHRenderNode::RenderNodeDraw(env, canvas, node);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RenderNode__1nCreateCRenderNode
(napi_env env, napi_value content) {
ArkUI_NodeContentHandle handle{};
auto ret = OH_ArkUI_GetNodeContentFromNapiValue(env, content, &handle);
if (ret != ARKUI_ERROR_CODE_NO_ERROR) {
Error("CRenderNode create GetNodeContentFromNapiValue failed: %{public}d", ret);
return nullptr;
}
CRenderNode* node = new CRenderNode(++renderNodeID);
ret = node->attach(handle);
if (ret != ARKUI_ERROR_CODE_NO_ERROR) {
Error("CRenderNode create attach failed: %{public}d", ret);
delete node;
return nullptr;
}
return reinterpret_cast<KNativePointer>(node);
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nRenderNodeReSize
(void* ptr, int width, int height) {
CRenderNode* node = reinterpret_cast<CRenderNode*>(ptr);
node->reSize(width, height);
}
SKIKO_EXPORT ArkUI_NodeHandle org_jetbrains_skia_RenderNode__1nRenderNodeGetNodeHandle
(void* ptr) {
CRenderNode* node = reinterpret_cast<CRenderNode*>(ptr);
return node->getNodeHandle();
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RenderNode__1nGetCNodeFinalizer() {
return reinterpret_cast<KNativePointer>((&CRenderNode::freeCRenderNode));
}
SKIKO_EXPORT void org_jetbrains_skia_RenderNode__1nSetCAPIFixed
(bool fixed) {
OHRenderNode::SetCAPIFixed(fixed);
}
#include <hilog/log.h>
#define OHOS_LOG_TAG "SkikoOHOS"
#ifdef SKIKO_DEBUG
#define Debug(...) ((void)OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, OHOS_LOG_TAG, __VA_ARGS__))
#else
#define Debug(...)
#endif
#define Info(...) ((void)OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, OHOS_LOG_TAG, __VA_ARGS__))
#define Warn(...) ((void)OH_LOG_Print(LOG_APP, LOG_WARN, LOG_DOMAIN, OHOS_LOG_TAG, __VA_ARGS__))
#define Error(...) ((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, OHOS_LOG_TAG, __VA_ARGS__))
\ No newline at end of file
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.getPtr
import org.jetbrains.skia.impl.reachabilityBarrier
import org.jetbrains.skiko.ExternalRenderNode
fun Canvas.drawRenderNode(node: ExternalRenderNode, rect: Rect): Canvas {
Stats.onNativeCall()
try {
_nDrawRenderNode(getPtr(this), node.nativePtr, rect.left, rect.top, rect.right, rect.bottom)
} finally {
reachabilityBarrier(this)
}
return this
}
@ExternalSymbolName("org_jetbrains_skia_Canvas__1nDrawRenderNode")
private external fun _nDrawRenderNode(ptr: NativePointer, nodePtr: NativePointer, left: Float, top: Float, right: Float, bottom: Float)
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
@Deprecated(
message = "Use a renderer-scoped OhosImageInteropSession instead",
)
@Suppress("UNUSED_PARAMETER")
fun Image.Companion.makeFromOhosSurfaceBuffer(
context: DirectContext,
surfaceBufferPtr: NativePointer,
colorType: ColorType = ColorType.RGBA_8888,
alphaType: ColorAlphaType = ColorAlphaType.PREMUL,
colorSpace: ColorSpace = ColorSpace.sRGB,
): Image {
throw UnsupportedOperationException(
"Fusion Renderer does not support the legacy DirectContext SurfaceBuffer import API. " +
"Use OhosImageInteropSession and handle OhosImageImportResult.Unsupported.",
)
}
/**
* Fusion Renderer has no queued EGL SurfaceBuffer releases.
*/
fun drainOhosSurfaceBufferReleases() = Unit
package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
import kotlinx.cinterop.COpaquePointer
import platform.ArkTS.ArkTS_Napi_NativeModule.napi_env
import platform.ArkTS.ArkTS_Napi_NativeModule.napi_value
import kotlin.native.SymbolName
public object OHRenderNode {
fun createRootRenderNode(context: NativePointer?, callback: COpaquePointer?): napi_value{
val node = OHRenderNode_nCreateRootRenderNode(context, callback)
return node
}
fun setNodeConstructor(env: napi_env, func: napi_value) {
OHRenderNode_nSetNodeConstructor(env, func)
return
}
fun renderNodeDraw(env: napi_env, jsCanvas: napi_value, jsNode: napi_value) {
OHRenderNode_nRenderNodeDraw(env, jsCanvas, jsNode)
return
}
fun renderNodeNotifyRedraw(env: napi_env, jsNode: napi_value) {
OHRenderNode_nRenderNodeNotifyRedraw(env, jsNode)
return
}
fun setPixelRatio(ratio: Double) {
OHRenderNode_nSetPixelRatio(ratio)
return
}
fun buildInstance(env: napi_env, func: napi_value) {
OHRenderNode_nBuildInstance(env, func)
return
}
}
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nCreateRootRenderNode")
private external fun OHRenderNode_nCreateRootRenderNode(context: NativePointer?, callback: COpaquePointer?): napi_value
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nSetNodeConstructor")
private external fun OHRenderNode_nSetNodeConstructor(env: napi_env, func: napi_value)
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nRenderNodeDraw")
private external fun OHRenderNode_nRenderNodeDraw(env: napi_env, jsCanvas: napi_value, jsNode: napi_value)
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nRenderNodeNotifyRedraw")
private external fun OHRenderNode_nRenderNodeNotifyRedraw(env: napi_env, jsNode: napi_value)
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nSetPixelRatio")
private external fun OHRenderNode_nSetPixelRatio(ratio: Double)
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nBuildInstance")
private external fun OHRenderNode_nBuildInstance(env: napi_env, func: napi_value)
@ExternalSymbolName("org_jetbrains_skia_OHRenderNode__1nGetCurrentTimeMicros")
private external fun OHRenderNode_nGetCurrentTimeMicros(): Long
package org.jetbrains.skia
import org.jetbrains.skia.impl.*
/**
* Fusion-renderer (OHRender) only: create a [Typeface] directly from font [data].
*
* Backed by the OHRender fork's `SkTypeface::MakeFromData` (a data-only typeface;
* its glyphs are resolved later when `TypefaceFontProvider::registerTypeface`
* registers the buffer into the OHOS FontCollection by family name).
*
* This restores the `Typeface.makeFromData` binding that skiko removed in
* d04654c6 ("update to skia m126 #969") — upstream skia m126 dropped the public
* `SkTypeface::MakeFromData`, which is why it was removed for all targets. It is
* re-added here in the fusion ohos source set ONLY (paired with the
* `org_jetbrains_skia_Typeface__1nMakeFromData` C++ binding guarded by
* `#ifdef USE_OHRENDER`), so iOS / native / js / ohos-skia are unaffected.
*/
fun Typeface.Companion.makeFromData(data: Data, index: Int = 0): Typeface {
return try {
Stats.onNativeCall()
val ptr = _nMakeFromData(getPtr(data), index)
require(ptr != Native.NullPointer) { "Failed to create Typeface from data $data" }
Typeface(ptr)
} finally {
reachabilityBarrier(data)
}
}
@ExternalSymbolName("org_jetbrains_skia_Typeface__1nMakeFromData")
private external fun _nMakeFromData(dataPtr: NativePointer, index: Int): NativePointer
package org.jetbrains.skiko
import org.jetbrains.skia.ColorType
import platform.ArkTS.ArkTS_Napi_NativeModule.napi_env
import kotlin.concurrent.Volatile
object ArkTsContext {
@Volatile
private var napiEnv: napi_env? = null
val NapiEnv: napi_env get() = napiEnv ?: error("napiEnv is null")
fun initEnv(env: napi_env) {
napiEnv = env;
// Fusion-renderer only: align the platform-native 32-bit color order with OHRender's C++ side.
// OHRender is built with SK_R32_SHIFT defaulting to 0, so its native kN32 is RGBA_8888 (its
// image decoders and MakeRasterN32Premul all produce RGBA). skiko's commonMain default is
// ColorType.N32 = BGRA_8888, leaving the Kotlin side disagreeing with native: a BGRA dst (e.g.
// AnimatedImagePainter.readFrameToImage's makeN32Premul) reaches an RGBA codec and used to hit
// kInvalidConversion. Setting N32 = RGBA here makes the Kotlin side match native everywhere.
// The self-render (skia) source set has no ArkTsContext, so it keeps the BGRA default untouched.
ColorType.N32 = ColorType.RGBA_8888
}
private var ratio: Float = 1.0F
val Ratio: Float get() = ratio
fun initRatio(rat: Float) {
ratio = rat
}
}
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
interface IClipboardProxy {
fun setText(text: String)
fun getText(): String?
fun hasText(): Boolean
}
object PlatformProxy {
var clipboardProxy: IClipboardProxy? = null
}
package org.jetbrains.skiko
import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.FloatVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.nativeNullPtr
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.impl.Managed
import org.jetbrains.skia.impl._nInvokeFinalizer
import platform.ArkUI.ArkUI_NativeModule.ArkUI_NodeHandle
import platform.ArkTS.ArkTS_Napi_NativeModule.napi_env
import platform.ArkTS.ArkTS_Napi_NativeModule.napi_value
import kotlin.concurrent.AtomicNativePtr
import kotlin.native.internal.NativePtr
import kotlin.native.ref.createCleaner
abstract class RenderNode protected constructor(val ptr: NativePtr, private val finalizer: NativePtr) :
Managed(ptr, finalizer, managed = false) {
private val _ptrAtomic = AtomicNativePtr(ptr)
@OptIn(kotlin.experimental.ExperimentalNativeApi::class)
private val mainCleaner = createCleaner(_ptrAtomic to finalizer) { (atomicPtr, fin) ->
val p = atomicPtr.value
if (p != nativeNullPtr && atomicPtr.compareAndSet(p, nativeNullPtr)) {
CoroutineScope(Dispatchers.Main).launch {
_nInvokeFinalizer(fin, p)
}
}
}
override fun close() {
val p = _ptrAtomic.value
if (p == nativeNullPtr) return
if (_ptrAtomic.compareAndSet(p, nativeNullPtr)) {
_ptr = NativePtr.NULL
CoroutineScope(Dispatchers.Main).launch {
_nInvokeFinalizer(finalizer, p)
}
}
}
override val isClosed: Boolean
get() = _ptrAtomic.value == nativeNullPtr
val id: Int by lazy { _nRenderNodeGetId(ptr) }
fun notifyRedraw() {
_nRenderNodeNotifyRedraw(ptr)
}
}
class JsRenderNode internal constructor(ptr: NativePtr) : RenderNode(ptr, FINALIZER) {
companion object {
val FINALIZER =_nRenderNodeGetJsNodeFinalizer()
var isContextReady: Boolean = false
private set
fun initJsRenderNodeContext(nodeConstructor: napi_value, statusModifyConstructor: napi_value,
drawCallback: COpaquePointer) {
_nRenderNodeSetNodeConstructor(ArkTsContext.NapiEnv, nodeConstructor)
_nRenderNodeNodeStatusModifyBuildInstance(ArkTsContext.NapiEnv, statusModifyConstructor)
_nRenderNodeNodeSetDrawCallback(drawCallback)
isContextReady = true
}
fun setPixelRatio(ratio: Double) {
_nRenderNodeNodeSetPixelRatio(ratio)
}
fun jsNodeDraw(canvas: napi_value, jsNode: napi_value) {
_nRenderNodeNodeJsNodeDraw(ArkTsContext.NapiEnv, jsNode, canvas)
}
}
constructor() : this(_nRenderNodeNodeCreateJsRenderNode(ArkTsContext.NapiEnv))
fun getJsNode(): napi_value {
return _nRenderNodeNodeGetJsNode(ptr)
}
fun draw(canvas: napi_value) {
_nRenderNodeNodeDraw(ptr, canvas)
}
}
class CRenderNode internal constructor(ptr: NativePtr) : RenderNode(ptr, FINALIZER) {
companion object {
val FINALIZER = _nRenderNodeGetCNodeFinalizer()
fun setCAPIFixed(fixed: Boolean) {
_nRenderNodeNodeSetCAPIFixed(fixed)
}
}
private var width: Int = 0
private var height: Int = 0
constructor(content: napi_value) : this(_nRenderNodeCreateCRenderNode(ArkTsContext.NapiEnv, content)) {}
val nodeHandle: ArkUI_NodeHandle
get() = _nRenderNodeGetNodeHandle(ptr)
fun reSize(width: Int, height: Int) {
if (this.width != width || this.height != height) {
this.width = width
this.height = height
_nRenderNodeReSize(ptr, width, height)
}
}
}
class ExternalRenderNode internal constructor(val nativePtr: NativePtr) : Managed(nativePtr, FIN, managed = false) {
private val _ptrAtomic = AtomicNativePtr(nativePtr)
@OptIn(kotlin.experimental.ExperimentalNativeApi::class)
private val mainCleaner = createCleaner(_ptrAtomic to FIN) { (atomicPtr, fin) ->
val p = atomicPtr.value
if (p != nativeNullPtr && atomicPtr.compareAndSet(p, nativeNullPtr)) {
CoroutineScope(Dispatchers.Main).launch {
_nInvokeFinalizer(fin, p)
}
}
}
override fun close() {
val p = _ptrAtomic.value
if (p == nativeNullPtr) return
if (_ptrAtomic.compareAndSet(p, nativeNullPtr)) {
_ptr = NativePtr.NULL
CoroutineScope(Dispatchers.Main).launch {
_nInvokeFinalizer(FIN, p)
}
}
}
override val isClosed: Boolean
get() = _ptrAtomic.value == nativeNullPtr
companion object {
private val FIN = _nExternalRenderNodeGetFinalizer()
// ArkTS path: wraps an ArkTS RenderNode (napi_value) directly as an ExternalRenderNode.
fun makeForArkTs(node: napi_value): ExternalRenderNode? {
val ptr = _nExternalRenderNodeMakeForArkTs(node)
if (ptr == NativePtr.NULL) return null
return ExternalRenderNode(ptr)
}
// CAPI path: converts an ArkTS FrameNode (napi_value) to ArkUI_NodeHandle inside C++, then wraps as an ExternalRenderNode.
fun makeForCApi(rootNodeId: Int, extNodeId: Int, node: napi_value): ExternalRenderNode? {
val ptr = _nExternalRenderNodeMakeForCApi(ArkTsContext.NapiEnv, rootNodeId, extNodeId, node)
if (ptr == NativePtr.NULL) return null
return ExternalRenderNode(ptr)
}
fun makeFromHandle(rootNodeId: Int, extNodeId: Int, handle: ArkUI_NodeHandle): ExternalRenderNode? {
val ptr = _nExternalRenderNodeMakeFromHandle(rootNodeId, extNodeId, handle)
if (ptr == NativePtr.NULL) return null
return ExternalRenderNode(ptr)
}
// Helper: mount a CAPI ArkUI_NodeHandle into a NodeContent container (100% width/height).
// Must be called before makeForCApi when the interop view is a CAPI node.
fun mountHandleToNodeContent(nodeContent: napi_value, handle: ArkUI_NodeHandle) {
_nExternalRenderNodeMountHandleToNodeContent(ArkTsContext.NapiEnv, nodeContent, handle)
}
fun mapFrameNodePos(rootNodeId: Int, extNodeId: Int, touchX: Double, touchY: Double): Pair<Float, Float> {
return memScoped {
val outX = alloc<FloatVar>()
val outY = alloc<FloatVar>()
_nExternalRenderNodeMapFrameNodePos(rootNodeId, extNodeId, touchX, touchY, outX.ptr, outY.ptr)
Pair(outX.value, outY.value)
}
}
}
fun mapPointFromGlobal(x: Double, y: Double): Pair<Float, Float> {
return memScoped {
val outX = alloc<FloatVar>()
val outY = alloc<FloatVar>()
_nExternalRenderNodeMapPointFromGlobal(nativePtr, x, y, outX.ptr, outY.ptr)
Pair(outX.value, outY.value)
}
}
}
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeGetNodeId")
private external fun _nRenderNodeGetId(ptr: NativePtr): Int
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeNotifyRedraw")
private external fun _nRenderNodeNotifyRedraw(ptr: NativePtr)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nGetJsNodeFinalizer")
private external fun _nRenderNodeGetJsNodeFinalizer(): NativePtr
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nSetNodeConstructor")
private external fun _nRenderNodeSetNodeConstructor(env: napi_env, function: napi_value)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nNodeStatusModifyBuildInstance")
private external fun _nRenderNodeNodeStatusModifyBuildInstance(env: napi_env, function: napi_value)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nSetDrawCallback")
private external fun _nRenderNodeNodeSetDrawCallback(callback: COpaquePointer)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nCreateJsRenderNode")
private external fun _nRenderNodeNodeCreateJsRenderNode(env: napi_env): NativePtr
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nSetPixelRatio")
private external fun _nRenderNodeNodeSetPixelRatio(ratio: Double)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeGetJsNode")
private external fun _nRenderNodeNodeGetJsNode(ptr: NativePtr): napi_value
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeDraw")
private external fun _nRenderNodeNodeDraw(ptr: NativePtr, canvas: napi_value)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeJsNodeDraw")
private external fun _nRenderNodeNodeJsNodeDraw(env: napi_env, node: napi_value, canvas: napi_value)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nCreateCRenderNode")
private external fun _nRenderNodeCreateCRenderNode(napiEnv: napi_env, content: napi_value): NativePtr
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeReSize")
private external fun _nRenderNodeReSize(ptr: NativePtr, width: Int, height: Int)
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nRenderNodeGetNodeHandle")
private external fun _nRenderNodeGetNodeHandle(ptr: NativePtr): ArkUI_NodeHandle
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nGetCNodeFinalizer")
private external fun _nRenderNodeGetCNodeFinalizer(): NativePtr
@ExternalSymbolName("org_jetbrains_skia_RenderNode__1nSetCAPIFixed")
private external fun _nRenderNodeNodeSetCAPIFixed(fixed: Boolean)
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nGetExternalRenderNodeFinalizer")
private external fun _nExternalRenderNodeGetFinalizer(): NativePtr
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMakeForArkTs")
private external fun _nExternalRenderNodeMakeForArkTs(ptr: napi_value): NativePtr
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMakeForCApi")
private external fun _nExternalRenderNodeMakeForCApi(env: napi_env, rootNodeId: Int, extNodeId: Int, ptr: napi_value): NativePtr
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMakeFromHandle")
private external fun _nExternalRenderNodeMakeFromHandle(rootNodeId: Int, extNodeId: Int, handle: ArkUI_NodeHandle): NativePtr
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMountHandleToNodeContent")
private external fun _nExternalRenderNodeMountHandleToNodeContent(env: napi_env, nodeContent: napi_value, handle: ArkUI_NodeHandle)
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMapFrameNodePos")
private external fun _nExternalRenderNodeMapFrameNodePos(
rootNodeId: Int, extNodeId: Int, touchX: Double, touchY: Double,
outX: CPointer<FloatVar>, outY: CPointer<FloatVar>
)
@ExternalSymbolName("org_jetbrains_skia_ExternalRenderNode__1nMapPointFromGlobal")
private external fun _nExternalRenderNodeMapPointFromGlobal(
ptr: NativePtr, x: Double, y: Double,
outX: CPointer<FloatVar>, outY: CPointer<FloatVar>
)
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry
/**
* Generic layer for Skiko rendering.
*/
actual open class SkiaLayer {
/**
* Current graphics API used for rendering.
*/
actual var renderApi: GraphicsApi
get() = GraphicsApi.OPENGL
set(_) { throw UnsupportedOperationException() }
/**
* Current content scale.
*/
actual val contentScale: Float
get() = TODO("Not yet implemented")
/**
* Pixel geometry corresponding to graphics device which renders this layer
*/
actual val pixelGeometry: PixelGeometry
get() = TODO("Not yet implemented")
/**
* If rendering is full screen.
*/
actual var fullscreen: Boolean
get() = TODO("Not yet implemented")
set(value) {}
/**
* Underlying platform component.
*/
actual val component: Any?
get() = TODO("Not yet implemented")
/**
* Current render delegate used for rendering.
*/
actual var renderDelegate: SkikoRenderDelegate?
get() = TODO("Not yet implemented")
set(value) {}
/**
* Attach this SkikoView to platform container.
* Actual type of attach container is platform-specific.
*/
actual fun attachTo(container: Any) {
}
/**
* Detach this SkikoView from platform container.
*/
actual fun detach() {
}
/**
* Force redraw.
*/
actual fun needRender(throttledToVsync: Boolean) {
}
@Deprecated("Use needRender() instead", ReplaceWith("needRender()"))
actual fun needRedraw() = needRender()
/**
* Drawing function.
*/
internal actual fun draw(canvas: Canvas) {
}
}
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
import org.jetbrains.skia.ExternalSymbolName
@ExternalSymbolName("skikobridge_napi_get_system_theme")
private external fun skikobridge_napi_get_system_theme(): Int
actual val currentSystemTheme: SystemTheme
get() = try {
val themeValue = skikobridge_napi_get_system_theme()
when (themeValue) {
1 -> SystemTheme.LIGHT
2 -> SystemTheme.DARK
else -> SystemTheme.UNKNOWN
}
} catch (_: Exception) {
SystemTheme.UNKNOWN
}
/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(
kotlinx.cinterop.ExperimentalForeignApi::class,
org.jetbrains.skiko.ExperimentalSkikoApi::class,
)
package org.jetbrains.skia
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.COpaquePointer
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.cstr
import kotlinx.cinterop.memScoped
import org.jetbrains.skia.impl.InteropPointer
import org.jetbrains.skia.impl.Native
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.interopScope
import org.jetbrains.skiko.ExperimentalSkikoApi
/**
* Renderer-independent OHOS image decode request.
*
* The same request and result contract is published by the Skia and Fusion Renderer artifacts.
*/
@ExperimentalSkikoApi
sealed class OhosImageDecodeRequest {
class EncodedTargetSize(
val bytes: ByteArray,
val desiredWidthPx: Int,
val desiredHeightPx: Int,
val cropX: Int = 0,
val cropY: Int = 0,
val cropWidth: Int = 0,
val cropHeight: Int = 0,
) : OhosImageDecodeRequest()
class RawFile(
// Nullable so request validation can classify a null manager as INVALID_INPUT. Callers
// that obtain the manager from OhosAssetReader pass a non-null value unchanged.
val nativeResourceManager: COpaquePointer?,
val path: String,
val targetWidthPx: Int? = null,
val targetHeightPx: Int? = null,
val contentScaleMode: Int,
val decodeSizeBucketPx: Int,
) : OhosImageDecodeRequest()
class MediaResource(
val nativeResourceManager: COpaquePointer?,
val resourceId: Int,
val targetWidthPx: Int? = null,
val targetHeightPx: Int? = null,
val contentScaleMode: Int,
val decodeSizeBucketPx: Int,
) : OhosImageDecodeRequest()
}
@ExperimentalSkikoApi
enum class OhosImageDecodeFailureReason {
INVALID_INPUT,
DECODE_FAILED,
}
/**
* Result of an OHOS image decode.
*
* A successful result owns [image]; the caller must close it.
*/
@ExperimentalSkikoApi
sealed class OhosImageDecodeResult {
class Success(val image: Image) : OhosImageDecodeResult()
class Failure(
val reason: OhosImageDecodeFailureReason,
val message: String,
val cause: Throwable? = null,
) : OhosImageDecodeResult()
}
/**
* Stable OHOS decode facade shared by Skia and Fusion Renderer artifacts.
*
* This is a **synchronous, blocking** call. Long-running decodes should be dispatched
* to a background thread by the caller (e.g. `withContext(Dispatchers.Default)`).
*
* This operation does not require a renderer or [DirectContext]. The returned [Image] is
* renderer-independent and owned by the caller — the caller must [Image.close] it when done.
*
* For [OhosImageDecodeRequest.RawFile] and [OhosImageDecodeRequest.MediaResource], the
* `nativeResourceManager` must remain valid for the entire duration of this call.
*/
@ExperimentalSkikoApi
object OhosImageDecoder {
/**
* Decodes an OHOS image synchronously.
*
* @param request the decode request; must pass [OhosImageDecodeRequest.isValid].
* @return [OhosImageDecodeResult.Success] with an [Image] the caller owns and must close,
* or [OhosImageDecodeResult.Failure] with a reason and message.
*/
fun decode(request: OhosImageDecodeRequest): OhosImageDecodeResult {
if (!request.isValid()) {
return OhosImageDecodeResult.Failure(
OhosImageDecodeFailureReason.INVALID_INPUT,
"Invalid OHOS image decode request",
)
}
val image = try {
when (request) {
is OhosImageDecodeRequest.EncodedTargetSize ->
makeFromEncodedOhosTargetSizeShared(
bytes = request.bytes,
desiredWidthPx = request.desiredWidthPx,
desiredHeightPx = request.desiredHeightPx,
cropX = request.cropX,
cropY = request.cropY,
cropWidth = request.cropWidth,
cropHeight = request.cropHeight,
)
is OhosImageDecodeRequest.RawFile ->
makeFromRawFileOhosShared(
// isValid() already guaranteed a non-null manager; !! is safe on this path.
nativeResourceManager = request.nativeResourceManager!!,
rawFilePath = request.path,
targetWidthPx = request.targetWidthPx,
targetHeightPx = request.targetHeightPx,
contentScaleMode = request.contentScaleMode,
decodeSizeBucketPx = request.decodeSizeBucketPx,
)
is OhosImageDecodeRequest.MediaResource ->
makeFromMediaOhosShared(
// isValid() already guaranteed a non-null manager; !! is safe on this path.
nativeResourceManager = request.nativeResourceManager!!,
mediaResId = request.resourceId,
targetWidthPx = request.targetWidthPx,
targetHeightPx = request.targetHeightPx,
contentScaleMode = request.contentScaleMode,
decodeSizeBucketPx = request.decodeSizeBucketPx,
)
}
} catch (error: Exception) {
// Catch only Exception, not Error/OOM/LinkageError — let fatal errors propagate.
return OhosImageDecodeResult.Failure(
OhosImageDecodeFailureReason.DECODE_FAILED,
"OHOS image decode failed",
error,
)
}
return if (image != null) {
OhosImageDecodeResult.Success(image)
} else {
OhosImageDecodeResult.Failure(
OhosImageDecodeFailureReason.DECODE_FAILED,
"OHOS image decode failed",
)
}
}
}
private fun OhosImageDecodeRequest.isValid(): Boolean = when (this) {
is OhosImageDecodeRequest.EncodedTargetSize -> {
val noCrop = cropX == 0 && cropY == 0 && cropWidth == 0 && cropHeight == 0
val validCrop = cropX >= 0 && cropY >= 0 && cropWidth > 0 && cropHeight > 0
bytes.isNotEmpty() && desiredWidthPx > 0 && desiredHeightPx > 0 && (noCrop || validCrop)
}
is OhosImageDecodeRequest.RawFile ->
// COpaquePointer is nullable; a null resource-manager pointer must be classified as
// INVALID_INPUT rather than reaching native code, which rejects it inconsistently.
nativeResourceManager != null &&
path.isNotEmpty() &&
hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)
is OhosImageDecodeRequest.MediaResource ->
nativeResourceManager != null &&
resourceId > 0 &&
hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)
}
internal fun makeFromEncodedOhosTargetSizeShared(
bytes: ByteArray,
desiredWidthPx: Int,
desiredHeightPx: Int,
cropX: Int = 0,
cropY: Int = 0,
cropWidth: Int = 0,
cropHeight: Int = 0,
): Image? {
if (bytes.isEmpty() || desiredWidthPx <= 0 || desiredHeightPx <= 0) return null
Stats.onNativeCall()
val ptr = interopScope {
_nMakeFromEncodedOhosTargetSize(
bytes = toInterop(bytes),
encodedLength = bytes.size,
desiredWidthPx = desiredWidthPx,
desiredHeightPx = desiredHeightPx,
cropX = cropX,
cropY = cropY,
cropWidth = cropWidth,
cropHeight = cropHeight,
)
}
return if (ptr == Native.NullPointer) null else Image(ptr)
}
internal fun makeFromRawFileOhosShared(
nativeResourceManager: COpaquePointer,
rawFilePath: String,
targetWidthPx: Int? = null,
targetHeightPx: Int? = null,
contentScaleMode: Int,
decodeSizeBucketPx: Int,
): Image? {
if (rawFilePath.isEmpty() || !hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)) return null
Stats.onNativeCall()
val ptr = memScoped {
_nMakeFromRawFileOhos(
nativeResourceManager = nativeResourceManager,
rawFilePath = rawFilePath.cstr.ptr,
targetWidthPx = targetWidthPx ?: 0,
targetHeightPx = targetHeightPx ?: 0,
contentScaleMode = contentScaleMode,
decodeSizeBucketPx = decodeSizeBucketPx,
)
}
return if (ptr == Native.NullPointer) null else Image(ptr)
}
internal fun makeFromMediaOhosShared(
nativeResourceManager: COpaquePointer,
mediaResId: Int,
targetWidthPx: Int? = null,
targetHeightPx: Int? = null,
contentScaleMode: Int,
decodeSizeBucketPx: Int,
): Image? {
if (mediaResId <= 0 || !hasValidOptionalTargetSize(targetWidthPx, targetHeightPx)) return null
Stats.onNativeCall()
val ptr = _nMakeFromMediaOhos(
nativeResourceManager = nativeResourceManager,
mediaResId = mediaResId,
targetWidthPx = targetWidthPx ?: 0,
targetHeightPx = targetHeightPx ?: 0,
contentScaleMode = contentScaleMode,
decodeSizeBucketPx = decodeSizeBucketPx,
)
return if (ptr == Native.NullPointer) null else Image(ptr)
}
private fun hasValidOptionalTargetSize(targetWidthPx: Int?, targetHeightPx: Int?): Boolean =
(targetWidthPx == null && targetHeightPx == null) ||
(targetWidthPx != null && targetWidthPx > 0 && targetHeightPx != null && targetHeightPx > 0)
@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromEncodedOhosTargetSize")
private external fun _nMakeFromEncodedOhosTargetSize(
bytes: InteropPointer,
encodedLength: Int,
desiredWidthPx: Int,
desiredHeightPx: Int,
cropX: Int,
cropY: Int,
cropWidth: Int,
cropHeight: Int,
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromRawFileOhos")
private external fun _nMakeFromRawFileOhos(
nativeResourceManager: COpaquePointer,
rawFilePath: CPointer<ByteVar>,
targetWidthPx: Int,
targetHeightPx: Int,
contentScaleMode: Int,
decodeSizeBucketPx: Int,
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromMediaOhos")
private external fun _nMakeFromMediaOhos(
nativeResourceManager: COpaquePointer,
mediaResId: Int,
targetWidthPx: Int,
targetHeightPx: Int,
contentScaleMode: Int,
decodeSizeBucketPx: Int,
): NativePointer
@ExperimentalSkikoApi
enum class OhosImageInteropCapability {
SURFACE_BUFFER_ZERO_COPY,
}
@ExperimentalSkikoApi
enum class OhosImageTransferMode {
ZERO_COPY,
}
/**
* Result of importing an OHOS native image into the active renderer.
*/
@ExperimentalSkikoApi
sealed class OhosImageImportResult {
class Success(
val image: Image,
val transferMode: OhosImageTransferMode,
) : OhosImageImportResult()
class Unsupported(
val capability: OhosImageInteropCapability,
val message: String,
) : OhosImageImportResult()
class Failure(
val message: String,
val cause: Throwable? = null,
) : OhosImageImportResult()
}
/**
* Renderer-scoped image interop contract.
*
* Implementations hide backend-only state such as a Skia [DirectContext]. Callers can use this
* interface without branching on the active renderer.
*
* **Ownership contract for [importSurfaceBuffer]:**
* - The caller retains ownership of the passed `SurfaceBuffer` reference. On Skia success,
* the implementation internally calls `OH_NativeBuffer_Reference` so the produced [Image]
* survives independently; the caller may unreference its own ref after the call returns.
* - On [OhosImageImportResult.Success], the returned [Image] is owned by the caller —
* it **must** be [Image.close]d when no longer needed.
* - The Fusion Renderer returns [OhosImageImportResult.Unsupported] and does **not** create
* or require a [DirectContext].
*/
@ExperimentalSkikoApi
interface OhosImageInteropSession {
fun supports(capability: OhosImageInteropCapability): Boolean
/**
* Imports an OHOS SurfaceBuffer into the active renderer without copying its pixels.
*
* Call this only on the active renderer thread. A session is scoped to its renderer lifecycle;
* callers must handle [OhosImageImportResult.Failure] when a previously captured session has
* expired. A successful result owns its image, which must be closed by the caller.
*/
fun importSurfaceBuffer(
surfaceBufferPtr: NativePointer,
colorType: ColorType = ColorType.RGBA_8888,
alphaType: ColorAlphaType = ColorAlphaType.PREMUL,
colorSpace: ColorSpace = ColorSpace.sRGB,
): OhosImageImportResult
companion object {
val Unsupported: OhosImageInteropSession = object : OhosImageInteropSession {
override fun supports(capability: OhosImageInteropCapability): Boolean = false
override fun importSurfaceBuffer(
surfaceBufferPtr: NativePointer,
colorType: ColorType,
alphaType: ColorAlphaType,
colorSpace: ColorSpace,
): OhosImageImportResult = OhosImageImportResult.Unsupported(
OhosImageInteropCapability.SURFACE_BUFFER_ZERO_COPY,
"The active renderer does not support OHOS SurfaceBuffer import",
)
}
}
}
/**
* Copyright (c) 2026 Eazytec Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "SkData.h"
#include "SkImage.h"
#include "SkBitmap.h"
#include "SkShader.h"
#include "SkEncodedImageFormat.h"
#include "include/gpu/ganesh/SkImageGanesh.h"
#include "gpu/ganesh/GrBackendSurface.h"
#include "gpu/ganesh/GrDirectContext.h"
#include "gpu/ganesh/GrTypes.h"
#include "gpu/ganesh/gl/GrGLTypes.h"
#include "gpu/ganesh/gl/GrGLBackendSurface.h"
#include "gpu/GpuTypes.h"
#include "SkAlphaType.h"
#include "SkColorSpace.h"
#include "SkColorType.h"
#include "common.h"
#include "Utils.h"
#define EGL_EGLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#define GL_GLEXT_PROTOTYPES
#include <GLES2/gl2ext.h>
#include "native_window/external_window.h"
#include <native_buffer/native_buffer.h>
#include <hilog/log.h>
#include <mutex>
#include <vector>
namespace {
const unsigned int LOG_PRINT_DOMAIN = 0xFF00;
// A/B: YUV NativeBuffer texture target.
// ExternalOes is the Khronos-correct target for YUV EGLImage siblings; Texture2D matches
// current OHOS sample snippets. Flip if device/Skia rejects one path.
enum class YuvImportTargetMode {
Texture2D = 0,
ExternalOes = 1,
};
static constexpr YuvImportTargetMode kYuvImportTargetMode = YuvImportTargetMode::ExternalOes;
struct OhosEglImageReleaseContext {
EGLDisplay display = EGL_NO_DISPLAY;
EGLImageKHR image = EGL_NO_IMAGE_KHR;
GLuint textureId = 0;
GLenum textureTarget = GL_TEXTURE_2D;
OH_NativeBuffer* nativeBuffer = nullptr;
OHNativeWindowBuffer* nativeWindowBuffer = nullptr;
};
static std::mutex gReleaseQueueMutex;
static std::vector<OhosEglImageReleaseContext *> gReleaseQueue;
static bool IsYuv420SpNativeFormat(int32_t format) {
return format == NATIVEBUFFER_PIXEL_FMT_YCRCB_420_SP ||
format == NATIVEBUFFER_PIXEL_FMT_YCBCR_420_SP;
}
static void OhosTextureReleaseProc(SkImages::ReleaseContext ctx) {
auto releaseCtx = static_cast<OhosEglImageReleaseContext *>(ctx);
if (!releaseCtx) {
return;
}
std::lock_guard<std::mutex> lock(gReleaseQueueMutex);
gReleaseQueue.push_back(releaseCtx);
}
static void DrainOhosReleaseQueueOnCurrentGLThread() {
EGLContext curCtx = eglGetCurrentContext();
if (curCtx == EGL_NO_CONTEXT) {
return;
}
std::vector<OhosEglImageReleaseContext *> pending;
{
std::lock_guard<std::mutex> lock(gReleaseQueueMutex);
pending.swap(gReleaseQueue);
}
if (pending.empty()) {
return;
}
PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR =
reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
for (auto* releaseCtx : pending) {
if (!releaseCtx) continue;
if (releaseCtx->textureId != 0) {
glDeleteTextures(1, &releaseCtx->textureId);
}
if (eglDestroyImageKHR && releaseCtx->image != EGL_NO_IMAGE_KHR &&
releaseCtx->display != EGL_NO_DISPLAY) {
eglDestroyImageKHR(releaseCtx->display, releaseCtx->image);
}
if (releaseCtx->nativeWindowBuffer) {
OH_NativeWindow_DestroyNativeWindowBuffer(releaseCtx->nativeWindowBuffer);
}
if (releaseCtx->nativeBuffer) {
OH_NativeBuffer_Unreference(releaseCtx->nativeBuffer);
}
delete releaseCtx;
}
}
} // namespace
SKIKO_EXPORT KNativePointer
org_jetbrains_skia_Image__1nMakeFromOhosSurfaceBuffer(KNativePointer contextPtr,
KNativePointer surfaceBufferPtr,
KInt colorType,
KInt alphaType,
KNativePointer colorSpacePtr) {
if (!contextPtr || !surfaceBufferPtr) return nullptr;
auto* context = reinterpret_cast<GrDirectContext*>(contextPtr);
auto* surfaceBuffer = reinterpret_cast<OH_NativeBuffer*>(surfaceBufferPtr);
if (!context || !surfaceBuffer) return nullptr;
EGLContext curCtx = eglGetCurrentContext();
if (curCtx == EGL_NO_CONTEXT) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
"MakeFromOhosSurfaceBuffer failed: EGL_NO_CONTEXT");
return nullptr;
}
DrainOhosReleaseQueueOnCurrentGLThread();
EGLDisplay display = eglGetCurrentDisplay();
if (display == EGL_NO_DISPLAY) display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) return nullptr;
PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR =
reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR =
reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
if (!eglCreateImageKHR) return nullptr;
OH_NativeBuffer_Config config = {};
OH_NativeBuffer_GetConfig(surfaceBuffer, &config);
const bool isYuvBuffer = IsYuv420SpNativeFormat(config.format);
const GLenum textureTarget =
(isYuvBuffer && kYuvImportTargetMode == YuvImportTargetMode::ExternalOes)
? GL_TEXTURE_EXTERNAL_OES
: GL_TEXTURE_2D;
GLint prevActiveTex = GL_TEXTURE0;
GLint prevTex2D = 0;
GLint prevTexExternal = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &prevActiveTex);
glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTex2D);
if (textureTarget == GL_TEXTURE_EXTERNAL_OES) {
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &prevTexExternal);
}
OHNativeWindowBuffer* nativeWindowBuffer = nullptr;
EGLImageKHR eglImage = EGL_NO_IMAGE_KHR;
GLuint texId = 0;
bool bufferRefHeld = false;
KNativePointer outPtr = nullptr;
auto cleanupFail = [&]() {
if (texId != 0) glDeleteTextures(1, &texId);
if (eglDestroyImageKHR && eglImage != EGL_NO_IMAGE_KHR) {
eglDestroyImageKHR(display, eglImage);
}
if (nativeWindowBuffer) {
OH_NativeWindow_DestroyNativeWindowBuffer(nativeWindowBuffer);
}
if (bufferRefHeld) {
OH_NativeBuffer_Unreference(surfaceBuffer);
}
};
nativeWindowBuffer = OH_NativeWindow_CreateNativeWindowBufferFromNativeBuffer(surfaceBuffer);
if (!nativeWindowBuffer) {
cleanupFail();
return nullptr;
}
if (OH_NativeBuffer_Reference(surfaceBuffer) != 0) {
cleanupFail();
return nullptr;
}
bufferRefHeld = true;
EGLint attrs[] = { EGL_IMAGE_PRESERVED, EGL_TRUE, EGL_NONE };
eglImage = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_OHOS, nativeWindowBuffer, attrs);
if (eglImage == EGL_NO_IMAGE_KHR) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
"MakeFromOhosSurfaceBuffer: eglCreateImageKHR failed format=%{public}d yuv=%{public}d",
config.format, isYuvBuffer ? 1 : 0);
cleanupFail();
return nullptr;
}
glGenTextures(1, &texId);
if (texId == 0) {
cleanupFail();
return nullptr;
}
glBindTexture(textureTarget, texId);
glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(textureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(textureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glEGLImageTargetTexture2DOES(textureTarget, static_cast<GLeglImageOES>(eglImage));
const GLenum bindErr = glGetError();
if (bindErr != GL_NO_ERROR) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
"MakeFromOhosSurfaceBuffer: glEGLImageTargetTexture2DOES err=0x%{public}x "
"target=0x%{public}x format=%{public}d",
bindErr, textureTarget, config.format);
cleanupFail();
return nullptr;
}
if (textureTarget == GL_TEXTURE_EXTERNAL_OES) {
glBindTexture(GL_TEXTURE_EXTERNAL_OES, static_cast<GLuint>(prevTexExternal));
} else {
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(prevTex2D));
}
glActiveTexture(prevActiveTex);
// GPU samples YUV EGLImage as RGB; Skia color type describes the sampled result.
#ifdef GL_RGBA8_OES
const GrGLenum glSizedFormat = GL_RGBA8_OES;
#else
const GrGLenum glSizedFormat = GL_RGBA8;
#endif
// OH_NativeBuffer_GetConfig returns void, so validate the populated dimensions before
// handing them to Skia. A zero or negative width/height would build a degenerate backend
// texture (and likely a GL error downstream); reject early and run the same cleanup as
// every other failure path so the EGL image, texture and NativeBuffer reference are freed.
if (config.width <= 0 || config.height <= 0) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
"MakeFromOhosSurfaceBuffer failed: invalid NativeBuffer config w=%d h=%d",
config.width, config.height);
cleanupFail();
return nullptr;
}
GrGLTextureInfo textureInfo = { textureTarget, texId, glSizedFormat };
GrBackendTexture backendTexture = GrBackendTextures::MakeGL(config.width,
config.height,
skgpu::Mipmapped::kNo,
textureInfo);
auto cs = reinterpret_cast<SkColorSpace *>(colorSpacePtr);
sk_sp<SkColorSpace> colorSpace = cs ? sk_ref_sp(cs) : nullptr;
// For YUV buffers ignore caller color/alpha: sampled texels are opaque RGB.
const SkColorType effectiveColorType =
isYuvBuffer ? kRGBA_8888_SkColorType : static_cast<SkColorType>(colorType);
const SkAlphaType effectiveAlphaType =
isYuvBuffer ? kOpaque_SkAlphaType : static_cast<SkAlphaType>(alphaType);
OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_PRINT_DOMAIN, "EGLCore",
"ImportContract: w=%{public}d h=%{public}d stride=%{public}d "
"nbFormat=%{public}d actualYuv=%{public}d glTarget=0x%{public}x "
"skColorType=%{public}d skAlphaType=%{public}d",
config.width,
config.height,
config.stride,
config.format,
isYuvBuffer ? 1 : 0,
textureTarget,
static_cast<int>(effectiveColorType),
static_cast<int>(effectiveAlphaType));
context->resetContext(kAll_GrBackendState);
auto releaseCtx = new OhosEglImageReleaseContext{
display, eglImage, texId, textureTarget, surfaceBuffer, nativeWindowBuffer
};
sk_sp<SkImage> image = SkImages::BorrowTextureFrom(
context,
backendTexture,
kTopLeft_GrSurfaceOrigin,
effectiveColorType,
effectiveAlphaType,
colorSpace,
OhosTextureReleaseProc,
releaseCtx);
if (!image) {
OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "EGLCore",
"MakeFromOhosSurfaceBuffer: BorrowTextureFrom failed yuv=%{public}d target=0x%{public}x",
isYuvBuffer ? 1 : 0, textureTarget);
OhosTextureReleaseProc(releaseCtx);
DrainOhosReleaseQueueOnCurrentGLThread();
return nullptr;
}
outPtr = reinterpret_cast<KNativePointer>(image.release());
return outPtr;
}
SKIKO_EXPORT void org_jetbrains_skia_Image__1nDrainOhosSurfaceBufferReleases() {
DrainOhosReleaseQueueOnCurrentGLThread();
}
/**
* Copyright (c) 2026 Eazytec Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef SKIA_CPP_UTILS_H
#define SKIA_CPP_UTILS_H
#include <hilog/log.h>
#ifndef SKIKO_OHOS_LOG_TAG
#define SKIKO_OHOS_LOG_TAG "SkikoOHOS"
#endif
#ifdef SKIKO_DEBUG
#define SKIKO_LOG_DEBUG(...) ((void)OH_LOG_Print(LOG_APP, LOG_DEBUG, LOG_DOMAIN, SKIKO_OHOS_LOG_TAG, __VA_ARGS__))
#else
#define SKIKO_LOG_DEBUG(...)
#endif
#define SKIKO_LOG_INFO(...) ((void)OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, SKIKO_OHOS_LOG_TAG, __VA_ARGS__))
#define SKIKO_LOG_WARN(...) ((void)OH_LOG_Print(LOG_APP, LOG_WARN, LOG_DOMAIN, SKIKO_OHOS_LOG_TAG, __VA_ARGS__))
#define SKIKO_LOG_ERROR(...) ((void)OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, SKIKO_OHOS_LOG_TAG, __VA_ARGS__))
#endif
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
package org.jetbrains.skia
import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.Native
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.getPtr
import org.jetbrains.skia.impl.reachabilityBarrier
/**
* Import an OH_NativeBuffer-backed texture into Skia.
*
* For YUV420 SP buffers (`YCRCB_420_SP` / `YCBCR_420_SP`), native code detects format and
* forces opaque RGBA8 semantics on the **sampled** result (GPU converts YUV→RGB). Caller
* [colorType]/[alphaType] are ignored in that case.
*/
@Deprecated(
message = "Use a renderer-scoped OhosImageInteropSession instead",
)
fun Image.Companion.makeFromOhosSurfaceBuffer(
context: DirectContext,
surfaceBufferPtr: NativePointer,
colorType: ColorType = ColorType.RGBA_8888,
alphaType: ColorAlphaType = ColorAlphaType.PREMUL,
colorSpace: ColorSpace = ColorSpace.sRGB
): Image {
return try {
Stats.onNativeCall()
val ptr = _nMakeFromOhosSurfaceBuffer(
getPtr(context),
surfaceBufferPtr,
colorType.ordinal,
alphaType.ordinal,
getPtr(colorSpace)
)
if (ptr == Native.NullPointer) throw RuntimeException("Failed to makeFromOhosSurfaceBuffer")
Image(ptr)
} finally {
reachabilityBarrier(context)
reachabilityBarrier(colorSpace)
}
}
fun drainOhosSurfaceBufferReleases() {
Stats.onNativeCall()
_nDrainOhosSurfaceBufferReleases()
}
@ExternalSymbolName("org_jetbrains_skia_Image__1nMakeFromOhosSurfaceBuffer")
private external fun _nMakeFromOhosSurfaceBuffer(
contextPtr: NativePointer,
surfaceBufferPtr: NativePointer,
colorType: Int,
alphaType: Int,
colorSpacePtr: NativePointer
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Image__1nDrainOhosSurfaceBufferReleases")
private external fun _nDrainOhosSurfaceBufferReleases()
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
interface IClipboardProxy {
fun setText(text: String)
fun getText(): String?
fun hasText(): Boolean
}
object PlatformProxy {
var clipboardProxy: IClipboardProxy? = null
}
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
import org.jetbrains.skia.Canvas
import org.jetbrains.skia.PixelGeometry
/**
* Generic layer for Skiko rendering.
*/
actual open class SkiaLayer {
/**
* Current graphics API used for rendering.
*/
actual var renderApi: GraphicsApi
get() = GraphicsApi.OPENGL
set(_) { throw UnsupportedOperationException() }
/**
* Current content scale.
*/
actual val contentScale: Float
get() = TODO("Not yet implemented")
/**
* Pixel geometry corresponding to graphics device which renders this layer
*/
actual val pixelGeometry: PixelGeometry
get() = TODO("Not yet implemented")
/**
* If rendering is full screen.
*/
actual var fullscreen: Boolean
get() = TODO("Not yet implemented")
set(value) {}
/**
* Underlying platform component.
*/
actual val component: Any?
get() = TODO("Not yet implemented")
/**
* Current render delegate used for rendering.
*/
actual var renderDelegate: SkikoRenderDelegate?
get() = TODO("Not yet implemented")
set(value) {}
/**
* Attach this SkikoView to platform container.
* Actual type of attach container is platform-specific.
*/
actual fun attachTo(container: Any) {
}
/**
* Detach this SkikoView from platform container.
*/
actual fun detach() {
}
/**
* Force redraw.
*/
actual fun needRender(throttledToVsync: Boolean) {
}
@Deprecated("Use needRender() instead", ReplaceWith("needRender()"))
actual fun needRedraw() {
needRender()
}
/**
* Drawing function.
*/
internal actual fun draw(canvas: Canvas) {
}
}
/*
* Eazytec is pleased to support the open source community by making Skiko available.
* Copyright (C) 2026 Eazytec. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.skiko
import org.jetbrains.skia.ExternalSymbolName
@ExternalSymbolName("skikobridge_napi_get_system_theme")
private external fun skikobridge_napi_get_system_theme(): Int
actual val currentSystemTheme: SystemTheme
get() = try {
val themeValue = skikobridge_napi_get_system_theme()
when (themeValue) {
1 -> SystemTheme.LIGHT
2 -> SystemTheme.DARK
else -> SystemTheme.UNKNOWN
}
} catch (_: Exception) {
SystemTheme.UNKNOWN
}
...@@ -69,6 +69,15 @@ kotlin { ...@@ -69,6 +69,15 @@ kotlin {
linuxX64() linuxX64()
linuxArm64() linuxArm64()
} }
// region Ohos
// Keep this subproject valid when an OHOS-only build disables every other target.
if (supportNativeOhosArm64) {
ohosArm64()
}
if (supportNativeOhosX64) {
ohosX64()
}
// endregion
if (supportNativeIosArm64) { if (supportNativeIosArm64) {
iosArm64() iosArm64()
} }
......
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