Introduced skikoMain sourceset for skiko dependent platforms
Moved compose/ui/ui-graphics to skikoMain

Test: jvmTest, desktopTest
Change-Id: I8e900a36fbacf09fae1d94190e83ae5c5c8ba67b
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
index 5138c91..9fdedb5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt
@@ -80,7 +80,7 @@
                 sourceSets
                     .filter { it.name.contains("main", ignoreCase = true) }
                     // TODO(igotti): come up with better filtering for non-Android sources.
-                    .filterNot { it.name == "desktopMain" }
+                    .filterNot { it.name == "desktopMain" || it.name == "skikoMain" }
                     .flatMap { it.kotlin.sourceDirectories }
                     .also { require(it.isNotEmpty()) }
             } ?: project.provider {
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 5fd756c..26340f2 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -84,10 +84,18 @@
                 api("androidx.annotation:annotation:1.2.0")
             }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
-                implementation(libs.kotlinStdlibJdk8)
-                api(libs.skiko)
+            skikoMain {
+                dependencies {
+                    api(libs.skikoCommon)
+                }
+            }
+
+            desktopMain {
+                dependsOn skikoMain
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+                    implementation(libs.kotlinStdlibJdk8)
+                }
             }
 
             androidTest.dependencies {
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt
index 8612ddf..e9591c3 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopColorFilter.desktop.kt
@@ -18,8 +18,6 @@
 
 import org.jetbrains.skia.ColorFilter as SkiaColorFilter
 
-actual typealias NativeColorFilter = SkiaColorFilter
-
 /**
  * Obtain a reference to the desktop ColorFilter type
  */
@@ -29,25 +27,5 @@
 /**
  * Obtain a [org.jetbrains.skia.ColorFilter] instance from this [ColorFilter]
  */
-fun ColorFilter.asSkiaColorFilter(): SkiaColorFilter = nativeColorFilter
-
 @Deprecated("Use asComposeColorFilter()", replaceWith = ReplaceWith("asComposeColorFilter()"))
-fun org.jetbrains.skia.ColorFilter.toComposeColorFilter(): ColorFilter = ColorFilter(this)
-
-/**
- * Create a [ColorFilter] from the given [org.jetbrains.skia.ColorFilter] instance
- */
-fun org.jetbrains.skia.ColorFilter.asComposeColorFilter(): ColorFilter = ColorFilter(this)
-
-internal actual fun actualTintColorFilter(color: Color, blendMode: BlendMode): ColorFilter =
-    ColorFilter(SkiaColorFilter.makeBlend(color.toArgb(), blendMode.toSkia()))
-
-internal actual fun actualColorMatrixColorFilter(colorMatrix: ColorMatrix): ColorFilter =
-    ColorFilter(
-        SkiaColorFilter.makeMatrix(
-            org.jetbrains.skia.ColorMatrix(*colorMatrix.values)
-        )
-    )
-
-internal actual fun actualLightingColorFilter(multiply: Color, add: Color): ColorFilter =
-    ColorFilter(SkiaColorFilter.makeLighting(multiply.toArgb(), add.toArgb()))
\ No newline at end of file
+fun SkiaColorFilter.toComposeColorFilter(): ColorFilter = ColorFilter(this)
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt
index 7099247..fd0bd46 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopImageAsset.desktop.kt
@@ -16,17 +16,10 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.compose.ui.graphics.colorspace.ColorSpace
-import androidx.compose.ui.graphics.colorspace.ColorSpaces
 import org.jetbrains.skia.Bitmap
-import org.jetbrains.skia.ColorAlphaType
-import org.jetbrains.skia.ColorInfo
-import org.jetbrains.skia.ColorType
 import org.jetbrains.skia.Image
-import org.jetbrains.skia.ImageInfo
 import java.nio.ByteBuffer
 import java.nio.ByteOrder
-import kotlin.math.abs
 
 /**
  * Create an [ImageBitmap] from the given [Bitmap]. Note this does
@@ -34,51 +27,13 @@
  * will modify the returned [ImageBitmap]
  */
 @Deprecated("Use asComposeImageBitmap", replaceWith = ReplaceWith("asComposeImageBitmap()"))
-fun Bitmap.asImageBitmap(): ImageBitmap = DesktopImageBitmap(this)
+fun Bitmap.asImageBitmap(): ImageBitmap = asComposeImageBitmap()
 
 /**
  * Create an [ImageBitmap] from the given [Image].
  */
 @Deprecated("Use toComposeImageBitmap", replaceWith = ReplaceWith("toComposeImageBitmap()"))
-fun Image.asImageBitmap(): ImageBitmap = DesktopImageBitmap(toBitmap())
-
-/**
- * Create an [ImageBitmap] from the given [Bitmap]. Note this does
- * not create a copy of the original [Bitmap] and changes to it
- * will modify the returned [ImageBitmap]
- */
-fun Bitmap.asComposeImageBitmap(): ImageBitmap = DesktopImageBitmap(this)
-
-/**
- * Create an [ImageBitmap] from the given [Image].
- */
-fun Image.toComposeImageBitmap(): ImageBitmap = DesktopImageBitmap(toBitmap())
-
-private fun Image.toBitmap(): Bitmap {
-    val bitmap = Bitmap()
-    bitmap.allocPixels(ImageInfo.makeN32(width, height, ColorAlphaType.PREMUL))
-    val canvas = org.jetbrains.skia.Canvas(bitmap)
-    canvas.drawImage(this, 0f, 0f)
-    bitmap.setImmutable()
-    return bitmap
-}
-
-internal actual fun ActualImageBitmap(
-    width: Int,
-    height: Int,
-    config: ImageBitmapConfig,
-    hasAlpha: Boolean,
-    colorSpace: ColorSpace
-): ImageBitmap {
-    val colorType = config.toSkiaColorType()
-    val alphaType = if (hasAlpha) ColorAlphaType.PREMUL else ColorAlphaType.OPAQUE
-    val skiaColorSpace = colorSpace.toSkiaColorSpace()
-    val colorInfo = ColorInfo(colorType, alphaType, skiaColorSpace)
-    val imageInfo = ImageInfo(colorInfo, width, height)
-    val bitmap = Bitmap()
-    bitmap.allocPixels(imageInfo)
-    return DesktopImageBitmap(bitmap)
-}
+fun Image.asImageBitmap(): ImageBitmap = toComposeImageBitmap()
 
 /**
  * @Throws UnsupportedOperationException if this [ImageBitmap] is not backed by an
@@ -87,98 +42,9 @@
 @Deprecated("Use asSkiaBitmap()", replaceWith = ReplaceWith("asSkiaBitmap()"))
 fun ImageBitmap.asDesktopBitmap(): Bitmap = asSkiaBitmap()
 
-/**
- * Obtain a reference to the [org.jetbrains.skia.Bitmap]
- *
- * @Throws UnsupportedOperationException if this [ImageBitmap] is not backed by an
- * org.jetbrains.skia.Image
- */
-fun ImageBitmap.asSkiaBitmap(): Bitmap =
-    when (this) {
-        is DesktopImageBitmap -> bitmap
-        else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image")
-    }
-
-private class DesktopImageBitmap(val bitmap: Bitmap) : ImageBitmap {
-    override val colorSpace = bitmap.colorSpace.toComposeColorSpace()
-    override val config = bitmap.colorType.toComposeConfig()
-    override val hasAlpha = !bitmap.isOpaque
-    override val height get() = bitmap.height
-    override val width get() = bitmap.width
-    override fun prepareToDraw() = Unit
-
-    override fun readPixels(
-        buffer: IntArray,
-        startX: Int,
-        startY: Int,
-        width: Int,
-        height: Int,
-        bufferOffset: Int,
-        stride: Int
-    ) {
-        // similar to https://cs.android.com/android/platform/superproject/+/42c50042d1f05d92ecc57baebe3326a57aeecf77:frameworks/base/graphics/java/android/graphics/Bitmap.java;l=2007
-        val lastScanline: Int = bufferOffset + (height - 1) * stride
-        require(startX >= 0 && startY >= 0)
-        require(width > 0 && startX + width <= this.width)
-        require(height > 0 && startY + height <= this.height)
-        require(abs(stride) >= width)
-        require(bufferOffset >= 0 && bufferOffset + width <= buffer.size)
-        require(lastScanline >= 0 && lastScanline + width <= buffer.size)
-
-        // similar to https://cs.android.com/android/platform/superproject/+/9054ca2b342b2ea902839f629e820546d8a2458b:frameworks/base/libs/hwui/jni/Bitmap.cpp;l=898;bpv=1
-        val colorInfo = ColorInfo(
-            ColorType.BGRA_8888,
-            ColorAlphaType.UNPREMUL,
-            org.jetbrains.skia.ColorSpace.sRGB
-        )
-        val imageInfo = ImageInfo(colorInfo, width, height)
-        val bytesPerPixel = 4
-        val bytes = bitmap.readPixels(imageInfo, stride * bytesPerPixel.toLong(), startX, startY)!!
-
-        ByteBuffer.wrap(bytes)
-            .order(ByteOrder.LITTLE_ENDIAN) // to return ARGB
-            .asIntBuffer()
-            .get(buffer, bufferOffset, bytes.size / bytesPerPixel)
-    }
+internal actual fun ByteArray.putBytesInto(array: IntArray, offset: Int, length: Int) {
+    ByteBuffer.wrap(this)
+        .order(ByteOrder.LITTLE_ENDIAN) // to return ARGB
+        .asIntBuffer()
+        .get(array, offset, length)
 }
-
-// TODO(demin): [API] maybe we should use:
-//  `else -> throw UnsupportedOperationException()`
-//  in toSkiaColorType/toComposeConfig/toComposeColorSpace/toSkiaColorSpace
-//  see [https://android-review.googlesource.com/c/platform/frameworks/support/+/1429835/comment/c219501b_63c3d1fe/]
-
-private fun ImageBitmapConfig.toSkiaColorType() = when (this) {
-    ImageBitmapConfig.Argb8888 -> ColorType.N32
-    ImageBitmapConfig.Alpha8 -> ColorType.ALPHA_8
-    ImageBitmapConfig.Rgb565 -> ColorType.RGB_565
-    ImageBitmapConfig.F16 -> ColorType.RGBA_F16
-    else -> ColorType.N32
-}
-
-private fun ColorType.toComposeConfig() = when (this) {
-    ColorType.N32 -> ImageBitmapConfig.Argb8888
-    ColorType.ALPHA_8 -> ImageBitmapConfig.Alpha8
-    ColorType.RGB_565 -> ImageBitmapConfig.Rgb565
-    ColorType.RGBA_F16 -> ImageBitmapConfig.F16
-    else -> ImageBitmapConfig.Argb8888
-}
-
-private fun org.jetbrains.skia.ColorSpace?.toComposeColorSpace(): ColorSpace {
-    return when (this) {
-        org.jetbrains.skia.ColorSpace.sRGB -> ColorSpaces.Srgb
-        org.jetbrains.skia.ColorSpace.sRGBLinear -> ColorSpaces.LinearSrgb
-        org.jetbrains.skia.ColorSpace.displayP3 -> ColorSpaces.DisplayP3
-        else -> ColorSpaces.Srgb
-    }
-}
-
-// TODO(demin): support all color spaces.
-//  to do this we need to implement SkColorSpace::MakeRGB in skia
-private fun ColorSpace.toSkiaColorSpace(): org.jetbrains.skia.ColorSpace {
-    return when (this) {
-        ColorSpaces.Srgb -> org.jetbrains.skia.ColorSpace.sRGB
-        ColorSpaces.LinearSrgb -> org.jetbrains.skia.ColorSpace.sRGBLinear
-        ColorSpaces.DisplayP3 -> org.jetbrains.skia.ColorSpace.displayP3
-        else -> org.jetbrains.skia.ColorSpace.sRGB
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt
index d0c6172..14e0070 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPath.desktop.kt
@@ -16,197 +16,6 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.RoundRect
-import org.jetbrains.skia.Matrix33
-import org.jetbrains.skia.PathDirection
-import org.jetbrains.skia.PathFillMode
-import org.jetbrains.skia.PathOp
-
-actual fun Path(): Path = DesktopPath()
-
-/**
- * Convert the [org.jetbrains.skia.Path] instance into a Compose-compatible Path
- */
-fun org.jetbrains.skia.Path.asComposePath(): Path = DesktopPath(this)
-
 @Suppress("NOTHING_TO_INLINE")
 @Deprecated("Use asSkiaPath()", replaceWith = ReplaceWith("asSkiaPath()"))
 inline fun Path.asDesktopPath(): org.jetbrains.skia.Path = asSkiaPath()
-
-/**
- * Obtain a reference to the [org.jetbrains.skia.Path]
- *
- * @Throws UnsupportedOperationException if this Path is not backed by an org.jetbrains.skia.Path
- */
-fun Path.asSkiaPath(): org.jetbrains.skia.Path =
-    if (this is DesktopPath) {
-        internalPath
-    } else {
-        throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Path")
-    }
-
-internal class DesktopPath(
-    internalPath: org.jetbrains.skia.Path = org.jetbrains.skia.Path()
-) : Path {
-    var internalPath = internalPath
-        private set
-
-    override var fillType: PathFillType
-        get() {
-            if (internalPath.fillMode == PathFillMode.EVEN_ODD) {
-                return PathFillType.EvenOdd
-            } else {
-                return PathFillType.NonZero
-            }
-        }
-
-        set(value) {
-            internalPath.fillMode =
-                if (value == PathFillType.EvenOdd) {
-                    PathFillMode.EVEN_ODD
-                } else {
-                    PathFillMode.WINDING
-                }
-        }
-
-    override fun moveTo(x: Float, y: Float) {
-        internalPath.moveTo(x, y)
-    }
-
-    override fun relativeMoveTo(dx: Float, dy: Float) {
-        internalPath.rMoveTo(dx, dy)
-    }
-
-    override fun lineTo(x: Float, y: Float) {
-        internalPath.lineTo(x, y)
-    }
-
-    override fun relativeLineTo(dx: Float, dy: Float) {
-        internalPath.rLineTo(dx, dy)
-    }
-
-    override fun quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float) {
-        internalPath.quadTo(x1, y1, x2, y2)
-    }
-
-    override fun relativeQuadraticBezierTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float) {
-        internalPath.rQuadTo(dx1, dy1, dx2, dy2)
-    }
-
-    override fun cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) {
-        internalPath.cubicTo(
-            x1, y1,
-            x2, y2,
-            x3, y3
-        )
-    }
-
-    override fun relativeCubicTo(
-        dx1: Float,
-        dy1: Float,
-        dx2: Float,
-        dy2: Float,
-        dx3: Float,
-        dy3: Float
-    ) {
-        internalPath.rCubicTo(
-            dx1, dy1,
-            dx2, dy2,
-            dx3, dy3
-        )
-    }
-
-    override fun arcTo(
-        rect: Rect,
-        startAngleDegrees: Float,
-        sweepAngleDegrees: Float,
-        forceMoveTo: Boolean
-    ) {
-        internalPath.arcTo(
-            rect.toSkiaRect(),
-            startAngleDegrees,
-            sweepAngleDegrees,
-            forceMoveTo
-        )
-    }
-
-    override fun addRect(rect: Rect) {
-        internalPath.addRect(rect.toSkiaRect(), PathDirection.COUNTER_CLOCKWISE)
-    }
-
-    override fun addOval(oval: Rect) {
-        internalPath.addOval(oval.toSkiaRect(), PathDirection.COUNTER_CLOCKWISE)
-    }
-
-    override fun addArcRad(oval: Rect, startAngleRadians: Float, sweepAngleRadians: Float) {
-        addArc(oval, degrees(startAngleRadians), degrees(sweepAngleRadians))
-    }
-
-    override fun addArc(oval: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float) {
-        internalPath.addArc(oval.toSkiaRect(), startAngleDegrees, sweepAngleDegrees)
-    }
-
-    override fun addRoundRect(roundRect: RoundRect) {
-        internalPath.addRRect(roundRect.toSkiaRRect(), PathDirection.COUNTER_CLOCKWISE)
-    }
-
-    override fun addPath(path: Path, offset: Offset) {
-        internalPath.addPath(path.asSkiaPath(), offset.x, offset.y)
-    }
-
-    override fun close() {
-        internalPath.closePath()
-    }
-
-    override fun reset() {
-        // preserve fillType to match the Android behavior
-        // see https://cs.android.com/android/_/android/platform/frameworks/base/+/d0f379c1976c600313f1f4c39f2587a649e3a4fc
-        val fillType = this.fillType
-        internalPath.reset()
-        this.fillType = fillType
-    }
-
-    override fun translate(offset: Offset) {
-        internalPath.transform(Matrix33.makeTranslate(offset.x, offset.y))
-    }
-
-    override fun getBounds(): Rect {
-        val bounds = internalPath.bounds
-        return Rect(
-            bounds.left,
-            bounds.top,
-            bounds.right,
-            bounds.bottom
-        )
-    }
-
-    override fun op(
-        path1: Path,
-        path2: Path,
-        operation: PathOperation
-    ): Boolean {
-        val path = org.jetbrains.skia.Path.makeCombining(
-            path1.asSkiaPath(),
-            path2.asSkiaPath(),
-            operation.toSkiaOperation()
-        )
-
-        internalPath = path ?: internalPath
-        return path != null
-    }
-
-    private fun PathOperation.toSkiaOperation() = when (this) {
-        PathOperation.Difference -> PathOp.DIFFERENCE
-        PathOperation.Intersect -> PathOp.INTERSECT
-        PathOperation.Union -> PathOp.UNION
-        PathOperation.Xor -> PathOp.XOR
-        PathOperation.ReverseDifference -> PathOp.REVERSE_DIFFERENCE
-        else -> PathOp.XOR
-    }
-
-    override val isConvex: Boolean get() = internalPath.isConvex
-
-    override val isEmpty: Boolean get() = internalPath.isEmpty
-}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt
index 22d5e8f..b2c0fac 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathEffect.desktop.kt
@@ -16,57 +16,10 @@
 
 package androidx.compose.ui.graphics
 
-import org.jetbrains.skia.PathEffect as SkiaPathEffect
-
-internal class DesktopPathEffect(val nativePathEffect: SkiaPathEffect) : PathEffect
-
-/**
- * Convert the [org.jetbrains.skia.PathEffect] instance into a Compose-compatible PathEffect
- */
-fun SkiaPathEffect.asComposePathEffect(): PathEffect = DesktopPathEffect(this)
+import org.jetbrains.skia.PathEffect as SkPathEffect
 
 /**
  * Obtain a reference to the desktop PathEffect type
  */
 @Deprecated("Use asSkiaPathEffect()", replaceWith = ReplaceWith("asSkiaPathEffect()"))
-fun PathEffect.asDesktopPathEffect(): SkiaPathEffect = asSkiaPathEffect()
-
-/**
- * Obtain a reference to the desktop PathEffect type
- */
-fun PathEffect.asSkiaPathEffect(): SkiaPathEffect =
-    (this as DesktopPathEffect).nativePathEffect
-
-internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
-    DesktopPathEffect(SkiaPathEffect.makeCorner(radius))
-
-internal actual fun actualDashPathEffect(
-    intervals: FloatArray,
-    phase: Float
-): PathEffect = DesktopPathEffect(SkiaPathEffect.makeDash(intervals, phase))
-
-internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
-    DesktopPathEffect(outer.asSkiaPathEffect().makeCompose(inner.asSkiaPathEffect()))
-
-internal actual fun actualStampedPathEffect(
-    shape: Path,
-    advance: Float,
-    phase: Float,
-    style: StampedPathEffectStyle
-): PathEffect =
-    DesktopPathEffect(
-        SkiaPathEffect.makePath1D(
-            shape.asSkiaPath(),
-            advance,
-            phase,
-            style.toSkiaStampedPathEffectStyle()
-        )
-    )
-
-internal fun StampedPathEffectStyle.toSkiaStampedPathEffectStyle(): SkiaPathEffect.Style =
-    when (this) {
-        StampedPathEffectStyle.Morph -> SkiaPathEffect.Style.MORPH
-        StampedPathEffectStyle.Rotate -> SkiaPathEffect.Style.ROTATE
-        StampedPathEffectStyle.Translate -> SkiaPathEffect.Style.TRANSLATE
-        else -> SkiaPathEffect.Style.TRANSLATE
-    }
\ No newline at end of file
+fun PathEffect.asDesktopPathEffect(): SkPathEffect = asSkiaPathEffect()
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt
index fea46c1..e278ef0 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt
+++ b/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/RenderEffect.desktop.kt
@@ -16,141 +16,7 @@
 
 package androidx.compose.ui.graphics
 
-import androidx.compose.runtime.Immutable
-import androidx.compose.ui.geometry.Offset
 import org.jetbrains.skia.ImageFilter
 
-/**
- * Convert the [ImageFilter] instance into a Compose-compatible [RenderEffect]
- */
-fun ImageFilter.asComposeRenderEffect(): RenderEffect =
-    DesktopRenderEffect(this)
-
-/**
- * Intermediate rendering step used to render drawing commands with a corresponding
- * visual effect. A [RenderEffect] can be configured on a [GraphicsLayerScope]
- * and will be applied when drawn.
- */
-@Immutable
-actual sealed class RenderEffect actual constructor() {
-
-    private var internalImageFilter: ImageFilter? = null
-
-    @Deprecated("Use asSkiaImageFilter()", replaceWith = ReplaceWith("asSkiaImageFilter()"))
-    fun asDesktopImageFilter(): ImageFilter = asSkiaImageFilter()
-
-    fun asSkiaImageFilter(): ImageFilter =
-        internalImageFilter ?: createImageFilter().also { internalImageFilter = it }
-
-    protected abstract fun createImageFilter(): ImageFilter
-
-    /**
-     * Capability query to determine if the particular platform supports the [RenderEffect]. Not
-     * all platforms support all render effects
-     */
-    actual open fun isSupported(): Boolean = true
-}
-
-@Immutable
-internal class DesktopRenderEffect(
-    val imageFilter: ImageFilter
-) : RenderEffect() {
-    override fun createImageFilter(): ImageFilter = imageFilter
-}
-
-@Immutable
-actual class BlurEffect actual constructor(
-    private val renderEffect: RenderEffect?,
-    private val radiusX: Float,
-    private val radiusY: Float,
-    private val edgeTreatment: TileMode
-) : RenderEffect() {
-
-    override fun createImageFilter(): ImageFilter =
-        if (renderEffect == null) {
-            ImageFilter.makeBlur(
-                convertRadiusToSigma(radiusX),
-                convertRadiusToSigma(radiusY),
-                edgeTreatment.toSkiaTileMode()
-            )
-        } else {
-            ImageFilter.makeBlur(
-                convertRadiusToSigma(radiusX),
-                convertRadiusToSigma(radiusY),
-                edgeTreatment.toSkiaTileMode(),
-                renderEffect.asSkiaImageFilter(),
-                null
-            )
-        }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is BlurEffect) return false
-
-        if (radiusX != other.radiusX) return false
-        if (radiusY != other.radiusY) return false
-        if (edgeTreatment != other.edgeTreatment) return false
-        if (renderEffect != other.renderEffect) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = renderEffect?.hashCode() ?: 0
-        result = 31 * result + radiusX.hashCode()
-        result = 31 * result + radiusY.hashCode()
-        result = 31 * result + edgeTreatment.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "BlurEffect(renderEffect=$renderEffect, radiusX=$radiusX, radiusY=$radiusY, " +
-            "edgeTreatment=$edgeTreatment)"
-    }
-
-    companion object {
-
-        // Constant used to convert blur radius into a corresponding sigma value
-        // for the gaussian blur algorithm used within SkImageFilter.
-        // This constant approximates the scaling done in the software path's
-        // "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
-        val BlurSigmaScale = 0.57735f
-
-        fun convertRadiusToSigma(radius: Float) =
-            if (radius > 0) {
-                BlurSigmaScale * radius + 0.5f
-            } else {
-                0.0f
-            }
-    }
-}
-
-@Immutable
-actual class OffsetEffect actual constructor(
-    private val renderEffect: RenderEffect?,
-    private val offset: Offset
-) : RenderEffect() {
-
-    override fun createImageFilter(): ImageFilter =
-        ImageFilter.makeOffset(offset.x, offset.y, renderEffect?.asSkiaImageFilter(), null)
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is OffsetEffect) return false
-
-        if (renderEffect != other.renderEffect) return false
-        if (offset != other.offset) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = renderEffect?.hashCode() ?: 0
-        result = 31 * result + offset.hashCode()
-        return result
-    }
-
-    override fun toString(): String {
-        return "OffsetEffect(renderEffect=$renderEffect, offset=$offset)"
-    }
-}
\ No newline at end of file
+@Deprecated("Use asSkiaImageFilter()", replaceWith = ReplaceWith("asSkiaImageFilter()"))
+fun RenderEffect.asDesktopImageFilter(): ImageFilter = asSkiaImageFilter()
diff --git a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
index e894d93..5c9a60a 100644
--- a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
+++ b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopGraphicsTest.kt
@@ -41,7 +41,7 @@
     protected fun initCanvas(widthPx: Int, heightPx: Int): Canvas {
         require(_surface == null)
         _surface = Surface.makeRasterN32Premul(widthPx, heightPx)
-        return DesktopCanvas(_surface!!.canvas)
+        return SkiaBackedCanvas(_surface!!.canvas)
     }
 
     @After
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/BlendMode.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/BlendMode.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/BlendMode.skiko.kt
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/Matrices.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/Matrices.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/Rects.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/Rects.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/Rects.skiko.kt
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopCanvas.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
similarity index 91%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopCanvas.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
index c5fb5ce..b2b81f3 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopCanvas.desktop.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
@@ -29,9 +29,9 @@
 import org.jetbrains.skia.Matrix44
 import org.jetbrains.skia.MipmapMode
 import org.jetbrains.skia.SamplingMode
-import org.jetbrains.skia.ClipMode as SkiaClipMode
-import org.jetbrains.skia.RRect as SkiaRRect
-import org.jetbrains.skia.Rect as SkiaRect
+import org.jetbrains.skia.ClipMode as SkClipMode
+import org.jetbrains.skia.RRect as SkRRect
+import org.jetbrains.skia.Rect as SkRect
 
 actual typealias NativeCanvas = org.jetbrains.skia.Canvas
 
@@ -40,18 +40,18 @@
     require(!skiaBitmap.isImmutable) {
         "Cannot draw on immutable ImageBitmap"
     }
-    return DesktopCanvas(org.jetbrains.skia.Canvas(skiaBitmap))
+    return SkiaBackedCanvas(org.jetbrains.skia.Canvas(skiaBitmap))
 }
 
 /**
  * Convert the [org.jetbrains.skia.Canvas] instance into a Compose-compatible Canvas
  */
-fun org.jetbrains.skia.Canvas.asComposeCanvas(): Canvas = DesktopCanvas(this)
+fun org.jetbrains.skia.Canvas.asComposeCanvas(): Canvas = SkiaBackedCanvas(this)
 
-actual val Canvas.nativeCanvas: NativeCanvas get() = (this as DesktopCanvas).skia
+actual val Canvas.nativeCanvas: NativeCanvas get() = (this as SkiaBackedCanvas).skia
 
-internal class DesktopCanvas(val skia: org.jetbrains.skia.Canvas) : Canvas {
-    private val Paint.skia get() = (this as DesktopPaint).skia
+internal class SkiaBackedCanvas(val skia: org.jetbrains.skia.Canvas) : Canvas {
+    private val Paint.skia get() = (this as SkiaBackedPaint).skia
 
     override fun save() {
         skia.save()
@@ -95,7 +95,7 @@
 
     override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
         val antiAlias = true
-        skia.clipRect(SkiaRect.makeLTRB(left, top, right, bottom), clipOp.toSkia(), antiAlias)
+        skia.clipRect(SkRect.makeLTRB(left, top, right, bottom), clipOp.toSkia(), antiAlias)
     }
 
     override fun clipPath(path: Path, clipOp: ClipOp) {
@@ -108,7 +108,7 @@
     }
 
     override fun drawRect(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
-        skia.drawRect(SkiaRect.makeLTRB(left, top, right, bottom), paint.skia)
+        skia.drawRect(SkRect.makeLTRB(left, top, right, bottom), paint.skia)
     }
 
     override fun drawRoundRect(
@@ -121,7 +121,7 @@
         paint: Paint
     ) {
         skia.drawRRect(
-            SkiaRRect.makeLTRB(
+            SkRRect.makeLTRB(
                 left,
                 top,
                 right,
@@ -134,7 +134,7 @@
     }
 
     override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
-        skia.drawOval(SkiaRect.makeLTRB(left, top, right, bottom), paint.skia)
+        skia.drawOval(SkRect.makeLTRB(left, top, right, bottom), paint.skia)
     }
 
     override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
@@ -203,13 +203,13 @@
         Image.makeFromBitmap(bitmap).use { skiaImage ->
             skia.drawImageRect(
                 skiaImage,
-                SkiaRect.makeXYWH(
+                SkRect.makeXYWH(
                     srcOffset.x,
                     srcOffset.y,
                     srcSize.width,
                     srcSize.height
                 ),
-                SkiaRect.makeXYWH(
+                SkRect.makeXYWH(
                     dstOffset.x,
                     dstOffset.y,
                     dstSize.width,
@@ -348,9 +348,9 @@
     }
 
     private fun ClipOp.toSkia() = when (this) {
-        ClipOp.Difference -> SkiaClipMode.DIFFERENCE
-        ClipOp.Intersect -> SkiaClipMode.INTERSECT
-        else -> SkiaClipMode.INTERSECT
+        ClipOp.Difference -> SkClipMode.DIFFERENCE
+        ClipOp.Intersect -> SkClipMode.INTERSECT
+        else -> SkClipMode.INTERSECT
     }
 
     private fun Matrix.toSkia() = Matrix44(
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
similarity index 75%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
index 0d61dc0..e9df393 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPaint.desktop.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
@@ -16,20 +16,20 @@
 
 package androidx.compose.ui.graphics
 
-import org.jetbrains.skia.PaintMode as SkiaPaintMode
-import org.jetbrains.skia.PaintStrokeCap as SkiaPaintStrokeCap
-import org.jetbrains.skia.PaintStrokeJoin as SkiaPaintStrokeJoin
+import org.jetbrains.skia.PaintMode as SkPaintMode
+import org.jetbrains.skia.PaintStrokeCap as SkPaintStrokeCap
+import org.jetbrains.skia.PaintStrokeJoin as SkPaintStrokeJoin
 
 actual typealias NativePaint = org.jetbrains.skia.Paint
 
-actual fun Paint(): Paint = DesktopPaint()
+actual fun Paint(): Paint = SkiaBackedPaint()
 
 /**
  * Convert the [org.jetbrains.skia.Paint] instance into a Compose-compatible Paint
  */
-fun org.jetbrains.skia.Paint.asComposePaint(): Paint = DesktopPaint(this)
+fun org.jetbrains.skia.Paint.asComposePaint(): Paint = SkiaBackedPaint(this)
 
-internal class DesktopPaint(
+internal class SkiaBackedPaint(
     val skia: org.jetbrains.skia.Paint = org.jetbrains.skia.Paint()
 ) : Paint {
     override fun asFrameworkPaint(): NativePaint = skia
@@ -104,28 +104,28 @@
 
     override var pathEffect: PathEffect? = null
         set(value) {
-            skia.pathEffect = (value as DesktopPathEffect?)?.asSkiaPathEffect()
+            skia.pathEffect = (value as SkiaBackedPathEffect?)?.asSkiaPathEffect()
             field = value
         }
 
     private fun PaintingStyle.toSkia() = when (this) {
-        PaintingStyle.Fill -> SkiaPaintMode.FILL
-        PaintingStyle.Stroke -> SkiaPaintMode.STROKE
-        else -> SkiaPaintMode.FILL
+        PaintingStyle.Fill -> SkPaintMode.FILL
+        PaintingStyle.Stroke -> SkPaintMode.STROKE
+        else -> SkPaintMode.FILL
     }
 
     private fun StrokeCap.toSkia() = when (this) {
-        StrokeCap.Butt -> SkiaPaintStrokeCap.BUTT
-        StrokeCap.Round -> SkiaPaintStrokeCap.ROUND
-        StrokeCap.Square -> SkiaPaintStrokeCap.SQUARE
-        else -> SkiaPaintStrokeCap.BUTT
+        StrokeCap.Butt -> SkPaintStrokeCap.BUTT
+        StrokeCap.Round -> SkPaintStrokeCap.ROUND
+        StrokeCap.Square -> SkPaintStrokeCap.SQUARE
+        else -> SkPaintStrokeCap.BUTT
     }
 
     private fun StrokeJoin.toSkia() = when (this) {
-        StrokeJoin.Miter -> SkiaPaintStrokeJoin.MITER
-        StrokeJoin.Round -> SkiaPaintStrokeJoin.ROUND
-        StrokeJoin.Bevel -> SkiaPaintStrokeJoin.BEVEL
-        else -> SkiaPaintStrokeJoin.MITER
+        StrokeJoin.Miter -> SkPaintStrokeJoin.MITER
+        StrokeJoin.Round -> SkPaintStrokeJoin.ROUND
+        StrokeJoin.Bevel -> SkPaintStrokeJoin.BEVEL
+        else -> SkPaintStrokeJoin.MITER
     }
 }
 
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
new file mode 100644
index 0000000..5552b80
--- /dev/null
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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 androidx.compose.ui.graphics
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.RoundRect
+import org.jetbrains.skia.Matrix33
+import org.jetbrains.skia.PathDirection
+import org.jetbrains.skia.PathFillMode
+import org.jetbrains.skia.PathOp
+
+actual fun Path(): Path = SkiaBackedPath()
+
+/**
+ * Convert the [org.jetbrains.skia.Path] instance into a Compose-compatible Path
+ */
+fun org.jetbrains.skia.Path.asComposePath(): Path = SkiaBackedPath(this)
+
+/**
+ * Obtain a reference to the [org.jetbrains.skia.Path]
+ *
+ * @Throws UnsupportedOperationException if this Path is not backed by an org.jetbrains.skia.Path
+ */
+fun Path.asSkiaPath(): org.jetbrains.skia.Path =
+    if (this is SkiaBackedPath) {
+        internalPath
+    } else {
+        throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Path")
+    }
+
+internal class SkiaBackedPath(
+    internalPath: org.jetbrains.skia.Path = org.jetbrains.skia.Path()
+) : Path {
+    var internalPath = internalPath
+        private set
+
+    override var fillType: PathFillType
+        get() {
+            if (internalPath.fillMode == PathFillMode.EVEN_ODD) {
+                return PathFillType.EvenOdd
+            } else {
+                return PathFillType.NonZero
+            }
+        }
+
+        set(value) {
+            internalPath.fillMode =
+                if (value == PathFillType.EvenOdd) {
+                    PathFillMode.EVEN_ODD
+                } else {
+                    PathFillMode.WINDING
+                }
+        }
+
+    override fun moveTo(x: Float, y: Float) {
+        internalPath.moveTo(x, y)
+    }
+
+    override fun relativeMoveTo(dx: Float, dy: Float) {
+        internalPath.rMoveTo(dx, dy)
+    }
+
+    override fun lineTo(x: Float, y: Float) {
+        internalPath.lineTo(x, y)
+    }
+
+    override fun relativeLineTo(dx: Float, dy: Float) {
+        internalPath.rLineTo(dx, dy)
+    }
+
+    override fun quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float) {
+        internalPath.quadTo(x1, y1, x2, y2)
+    }
+
+    override fun relativeQuadraticBezierTo(dx1: Float, dy1: Float, dx2: Float, dy2: Float) {
+        internalPath.rQuadTo(dx1, dy1, dx2, dy2)
+    }
+
+    override fun cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) {
+        internalPath.cubicTo(
+            x1, y1,
+            x2, y2,
+            x3, y3
+        )
+    }
+
+    override fun relativeCubicTo(
+        dx1: Float,
+        dy1: Float,
+        dx2: Float,
+        dy2: Float,
+        dx3: Float,
+        dy3: Float
+    ) {
+        internalPath.rCubicTo(
+            dx1, dy1,
+            dx2, dy2,
+            dx3, dy3
+        )
+    }
+
+    override fun arcTo(
+        rect: Rect,
+        startAngleDegrees: Float,
+        sweepAngleDegrees: Float,
+        forceMoveTo: Boolean
+    ) {
+        internalPath.arcTo(
+            rect.toSkiaRect(),
+            startAngleDegrees,
+            sweepAngleDegrees,
+            forceMoveTo
+        )
+    }
+
+    override fun addRect(rect: Rect) {
+        internalPath.addRect(rect.toSkiaRect(), PathDirection.COUNTER_CLOCKWISE)
+    }
+
+    override fun addOval(oval: Rect) {
+        internalPath.addOval(oval.toSkiaRect(), PathDirection.COUNTER_CLOCKWISE)
+    }
+
+    override fun addArcRad(oval: Rect, startAngleRadians: Float, sweepAngleRadians: Float) {
+        addArc(oval, degrees(startAngleRadians), degrees(sweepAngleRadians))
+    }
+
+    override fun addArc(oval: Rect, startAngleDegrees: Float, sweepAngleDegrees: Float) {
+        internalPath.addArc(oval.toSkiaRect(), startAngleDegrees, sweepAngleDegrees)
+    }
+
+    override fun addRoundRect(roundRect: RoundRect) {
+        internalPath.addRRect(roundRect.toSkiaRRect(), PathDirection.COUNTER_CLOCKWISE)
+    }
+
+    override fun addPath(path: Path, offset: Offset) {
+        internalPath.addPath(path.asSkiaPath(), offset.x, offset.y)
+    }
+
+    override fun close() {
+        internalPath.closePath()
+    }
+
+    override fun reset() {
+        // preserve fillType to match the Android behavior
+        // see https://cs.android.com/android/_/android/platform/frameworks/base/+/d0f379c1976c600313f1f4c39f2587a649e3a4fc
+        val fillType = this.fillType
+        internalPath.reset()
+        this.fillType = fillType
+    }
+
+    override fun translate(offset: Offset) {
+        internalPath.transform(Matrix33.makeTranslate(offset.x, offset.y))
+    }
+
+    override fun getBounds(): Rect {
+        val bounds = internalPath.bounds
+        return Rect(
+            bounds.left,
+            bounds.top,
+            bounds.right,
+            bounds.bottom
+        )
+    }
+
+    override fun op(
+        path1: Path,
+        path2: Path,
+        operation: PathOperation
+    ): Boolean {
+        val path = org.jetbrains.skia.Path.makeCombining(
+            path1.asSkiaPath(),
+            path2.asSkiaPath(),
+            operation.toSkiaOperation()
+        )
+
+        internalPath = path ?: internalPath
+        return path != null
+    }
+
+    private fun PathOperation.toSkiaOperation() = when (this) {
+        PathOperation.Difference -> PathOp.DIFFERENCE
+        PathOperation.Intersect -> PathOp.INTERSECT
+        PathOperation.Union -> PathOp.UNION
+        PathOperation.Xor -> PathOp.XOR
+        PathOperation.ReverseDifference -> PathOp.REVERSE_DIFFERENCE
+        else -> PathOp.XOR
+    }
+
+    override val isConvex: Boolean get() = internalPath.isConvex
+
+    override val isEmpty: Boolean get() = internalPath.isEmpty
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt
new file mode 100644
index 0000000..cd96db9
--- /dev/null
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathEffect.skiko.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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 androidx.compose.ui.graphics
+
+import org.jetbrains.skia.PathEffect as SkPathEffect
+
+internal class SkiaBackedPathEffect(val nativePathEffect: SkPathEffect) : PathEffect
+
+/**
+ * Convert the [org.jetbrains.skia.PathEffect] instance into a Compose-compatible PathEffect
+ */
+fun SkPathEffect.asComposePathEffect(): PathEffect = SkiaBackedPathEffect(this)
+
+/**
+ * Obtain a reference to skia PathEffect type
+ */
+fun PathEffect.asSkiaPathEffect(): SkPathEffect =
+    (this as SkiaBackedPathEffect).nativePathEffect
+
+internal actual fun actualCornerPathEffect(radius: Float): PathEffect =
+    SkiaBackedPathEffect(SkPathEffect.makeCorner(radius))
+
+internal actual fun actualDashPathEffect(
+    intervals: FloatArray,
+    phase: Float
+): PathEffect = SkiaBackedPathEffect(SkPathEffect.makeDash(intervals, phase))
+
+internal actual fun actualChainPathEffect(outer: PathEffect, inner: PathEffect): PathEffect =
+    SkiaBackedPathEffect(outer.asSkiaPathEffect().makeCompose(inner.asSkiaPathEffect()))
+
+internal actual fun actualStampedPathEffect(
+    shape: Path,
+    advance: Float,
+    phase: Float,
+    style: StampedPathEffectStyle
+): PathEffect =
+    SkiaBackedPathEffect(
+        SkPathEffect.makePath1D(
+            shape.asSkiaPath(),
+            advance,
+            phase,
+            style.toSkiaStampedPathEffectStyle()
+        )
+    )
+
+internal fun StampedPathEffectStyle.toSkiaStampedPathEffectStyle(): SkPathEffect.Style =
+    when (this) {
+        StampedPathEffectStyle.Morph -> SkPathEffect.Style.MORPH
+        StampedPathEffectStyle.Rotate -> SkPathEffect.Style.ROTATE
+        StampedPathEffectStyle.Translate -> SkPathEffect.Style.TRANSLATE
+        else -> SkPathEffect.Style.TRANSLATE
+    }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathMeasure.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
similarity index 88%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathMeasure.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
index f348407..7658c68 100644
--- a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopPathMeasure.desktop.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPathMeasure.skiko.kt
@@ -19,15 +19,15 @@
 /**
  * Convert the [org.jetbrains.skia.PathMeasure] instance into a Compose-compatible PathMeasure
  */
-fun org.jetbrains.skia.PathMeasure.asComposePathEffect(): PathMeasure = DesktopPathMeasure(this)
+fun org.jetbrains.skia.PathMeasure.asComposePathEffect(): PathMeasure = SkiaBackedPathMeasure(this)
 
 /**
- * Obtain a reference to the desktop PathMeasure type
+ * Obtain a reference to skia PathMeasure type
  */
 fun PathMeasure.asSkiaPathMeasure(): org.jetbrains.skia.PathMeasure =
-    (this as DesktopPathMeasure).skia
+    (this as SkiaBackedPathMeasure).skia
 
-internal class DesktopPathMeasure(
+internal class SkiaBackedPathMeasure(
     internal val skia: org.jetbrains.skia.PathMeasure = org.jetbrains.skia.PathMeasure()
 ) : PathMeasure {
 
@@ -52,4 +52,4 @@
 }
 
 actual fun PathMeasure(): PathMeasure =
-    DesktopPathMeasure()
\ No newline at end of file
+    SkiaBackedPathMeasure()
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt
new file mode 100644
index 0000000..8d6e519
--- /dev/null
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedRenderEffect.skiko.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * 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 androidx.compose.ui.graphics
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.geometry.Offset
+import org.jetbrains.skia.ImageFilter
+
+/**
+ * Convert the [ImageFilter] instance into a Compose-compatible [RenderEffect]
+ */
+fun ImageFilter.asComposeRenderEffect(): RenderEffect =
+    SkiaBackedRenderEffect(this)
+
+/**
+ * Intermediate rendering step used to render drawing commands with a corresponding
+ * visual effect. A [RenderEffect] can be configured on a [GraphicsLayerScope]
+ * and will be applied when drawn.
+ */
+@Immutable
+actual sealed class RenderEffect actual constructor() {
+
+    private var internalImageFilter: ImageFilter? = null
+
+    fun asSkiaImageFilter(): ImageFilter =
+        internalImageFilter ?: createImageFilter().also { internalImageFilter = it }
+
+    protected abstract fun createImageFilter(): ImageFilter
+
+    /**
+     * Capability query to determine if the particular platform supports the [RenderEffect]. Not
+     * all platforms support all render effects
+     */
+    actual open fun isSupported(): Boolean = true
+}
+
+@Immutable
+internal class SkiaBackedRenderEffect(
+    val imageFilter: ImageFilter
+) : RenderEffect() {
+    override fun createImageFilter(): ImageFilter = imageFilter
+}
+
+@Immutable
+actual class BlurEffect actual constructor(
+    private val renderEffect: RenderEffect?,
+    private val radiusX: Float,
+    private val radiusY: Float,
+    private val edgeTreatment: TileMode
+) : RenderEffect() {
+
+    override fun createImageFilter(): ImageFilter =
+        if (renderEffect == null) {
+            ImageFilter.makeBlur(
+                convertRadiusToSigma(radiusX),
+                convertRadiusToSigma(radiusY),
+                edgeTreatment.toSkiaTileMode()
+            )
+        } else {
+            ImageFilter.makeBlur(
+                convertRadiusToSigma(radiusX),
+                convertRadiusToSigma(radiusY),
+                edgeTreatment.toSkiaTileMode(),
+                renderEffect.asSkiaImageFilter(),
+                null
+            )
+        }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is BlurEffect) return false
+
+        if (radiusX != other.radiusX) return false
+        if (radiusY != other.radiusY) return false
+        if (edgeTreatment != other.edgeTreatment) return false
+        if (renderEffect != other.renderEffect) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = renderEffect?.hashCode() ?: 0
+        result = 31 * result + radiusX.hashCode()
+        result = 31 * result + radiusY.hashCode()
+        result = 31 * result + edgeTreatment.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "BlurEffect(renderEffect=$renderEffect, radiusX=$radiusX, radiusY=$radiusY, " +
+            "edgeTreatment=$edgeTreatment)"
+    }
+
+    companion object {
+
+        // Constant used to convert blur radius into a corresponding sigma value
+        // for the gaussian blur algorithm used within SkImageFilter.
+        // This constant approximates the scaling done in the software path's
+        // "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
+        val BlurSigmaScale = 0.57735f
+
+        fun convertRadiusToSigma(radius: Float) =
+            if (radius > 0) {
+                BlurSigmaScale * radius + 0.5f
+            } else {
+                0.0f
+            }
+    }
+}
+
+@Immutable
+actual class OffsetEffect actual constructor(
+    private val renderEffect: RenderEffect?,
+    private val offset: Offset
+) : RenderEffect() {
+
+    override fun createImageFilter(): ImageFilter =
+        ImageFilter.makeOffset(offset.x, offset.y, renderEffect?.asSkiaImageFilter(), null)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is OffsetEffect) return false
+
+        if (renderEffect != other.renderEffect) return false
+        if (offset != other.offset) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = renderEffect?.hashCode() ?: 0
+        result = 31 * result + offset.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "OffsetEffect(renderEffect=$renderEffect, offset=$offset)"
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt
new file mode 100644
index 0000000..2e598a0
--- /dev/null
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaColorFilter.skiko.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * 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 androidx.compose.ui.graphics
+
+import org.jetbrains.skia.ColorFilter as SkiaColorFilter
+
+actual typealias NativeColorFilter = SkiaColorFilter
+
+/**
+ * Obtain a [org.jetbrains.skia.ColorFilter] instance from this [ColorFilter]
+ */
+fun ColorFilter.asSkiaColorFilter(): SkiaColorFilter = nativeColorFilter
+
+/**
+ * Create a [ColorFilter] from the given [org.jetbrains.skia.ColorFilter] instance
+ */
+fun org.jetbrains.skia.ColorFilter.asComposeColorFilter(): ColorFilter = ColorFilter(this)
+
+internal actual fun actualTintColorFilter(color: Color, blendMode: BlendMode): ColorFilter =
+    ColorFilter(SkiaColorFilter.makeBlend(color.toArgb(), blendMode.toSkia()))
+
+internal actual fun actualColorMatrixColorFilter(colorMatrix: ColorMatrix): ColorFilter =
+    ColorFilter(
+        SkiaColorFilter.makeMatrix(
+            org.jetbrains.skia.ColorMatrix(*colorMatrix.values)
+        )
+    )
+
+internal actual fun actualLightingColorFilter(multiply: Color, add: Color): ColorFilter =
+    ColorFilter(SkiaColorFilter.makeLighting(multiply.toArgb(), add.toArgb()))
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
new file mode 100644
index 0000000..bc92d05
--- /dev/null
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * 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 androidx.compose.ui.graphics
+
+import androidx.compose.ui.graphics.colorspace.ColorSpace
+import androidx.compose.ui.graphics.colorspace.ColorSpaces
+import org.jetbrains.skia.Bitmap
+import org.jetbrains.skia.ColorAlphaType
+import org.jetbrains.skia.ColorInfo
+import org.jetbrains.skia.ColorType
+import org.jetbrains.skia.Image
+import org.jetbrains.skia.ImageInfo
+import kotlin.math.abs
+
+/**
+ * Create an [ImageBitmap] from the given [Bitmap]. Note this does
+ * not create a copy of the original [Bitmap] and changes to it
+ * will modify the returned [ImageBitmap]
+ */
+fun Bitmap.asComposeImageBitmap(): ImageBitmap = SkiaBackedImageBitmap(this)
+
+/**
+ * Create an [ImageBitmap] from the given [Image].
+ */
+fun Image.toComposeImageBitmap(): ImageBitmap = SkiaBackedImageBitmap(toBitmap())
+
+private fun Image.toBitmap(): Bitmap {
+    val bitmap = Bitmap()
+    bitmap.allocPixels(ImageInfo.makeN32(width, height, ColorAlphaType.PREMUL))
+    val canvas = org.jetbrains.skia.Canvas(bitmap)
+    canvas.drawImage(this, 0f, 0f)
+    bitmap.setImmutable()
+    return bitmap
+}
+
+internal actual fun ActualImageBitmap(
+    width: Int,
+    height: Int,
+    config: ImageBitmapConfig,
+    hasAlpha: Boolean,
+    colorSpace: ColorSpace
+): ImageBitmap {
+    val colorType = config.toSkiaColorType()
+    val alphaType = if (hasAlpha) ColorAlphaType.PREMUL else ColorAlphaType.OPAQUE
+    val skiaColorSpace = colorSpace.toSkiaColorSpace()
+    val colorInfo = ColorInfo(colorType, alphaType, skiaColorSpace)
+    val imageInfo = ImageInfo(colorInfo, width, height)
+    val bitmap = Bitmap()
+    bitmap.allocPixels(imageInfo)
+    return SkiaBackedImageBitmap(bitmap)
+}
+
+/**
+ * Obtain a reference to the [org.jetbrains.skia.Bitmap]
+ *
+ * @Throws UnsupportedOperationException if this [ImageBitmap] is not backed by an
+ * org.jetbrains.skia.Image
+ */
+fun ImageBitmap.asSkiaBitmap(): Bitmap =
+    when (this) {
+        is SkiaBackedImageBitmap -> bitmap
+        else -> throw UnsupportedOperationException("Unable to obtain org.jetbrains.skia.Image")
+    }
+
+private class SkiaBackedImageBitmap(val bitmap: Bitmap) : ImageBitmap {
+    override val colorSpace = bitmap.colorSpace.toComposeColorSpace()
+    override val config = bitmap.colorType.toComposeConfig()
+    override val hasAlpha = !bitmap.isOpaque
+    override val height get() = bitmap.height
+    override val width get() = bitmap.width
+    override fun prepareToDraw() = Unit
+
+    override fun readPixels(
+        buffer: IntArray,
+        startX: Int,
+        startY: Int,
+        width: Int,
+        height: Int,
+        bufferOffset: Int,
+        stride: Int
+    ) {
+        // similar to https://cs.android.com/android/platform/superproject/+/42c50042d1f05d92ecc57baebe3326a57aeecf77:frameworks/base/graphics/java/android/graphics/Bitmap.java;l=2007
+        val lastScanline: Int = bufferOffset + (height - 1) * stride
+        require(startX >= 0 && startY >= 0)
+        require(width > 0 && startX + width <= this.width)
+        require(height > 0 && startY + height <= this.height)
+        require(abs(stride) >= width)
+        require(bufferOffset >= 0 && bufferOffset + width <= buffer.size)
+        require(lastScanline >= 0 && lastScanline + width <= buffer.size)
+
+        // similar to https://cs.android.com/android/platform/superproject/+/9054ca2b342b2ea902839f629e820546d8a2458b:frameworks/base/libs/hwui/jni/Bitmap.cpp;l=898;bpv=1
+        val colorInfo = ColorInfo(
+            ColorType.BGRA_8888,
+            ColorAlphaType.UNPREMUL,
+            org.jetbrains.skia.ColorSpace.sRGB
+        )
+        val imageInfo = ImageInfo(colorInfo, width, height)
+        val bytesPerPixel = 4
+        val bytes = bitmap.readPixels(imageInfo, stride * bytesPerPixel.toLong(), startX, startY)!!
+        bytes.putBytesInto(buffer, bufferOffset, bytes.size / bytesPerPixel)
+    }
+}
+
+internal expect fun ByteArray.putBytesInto(array: IntArray, offset: Int, length: Int)
+
+// TODO(demin): [API] maybe we should use:
+//  `else -> throw UnsupportedOperationException()`
+//  in toSkiaColorType/toComposeConfig/toComposeColorSpace/toSkiaColorSpace
+//  see [https://android-review.googlesource.com/c/platform/frameworks/support/+/1429835/comment/c219501b_63c3d1fe/]
+
+private fun ImageBitmapConfig.toSkiaColorType() = when (this) {
+    ImageBitmapConfig.Argb8888 -> ColorType.N32
+    ImageBitmapConfig.Alpha8 -> ColorType.ALPHA_8
+    ImageBitmapConfig.Rgb565 -> ColorType.RGB_565
+    ImageBitmapConfig.F16 -> ColorType.RGBA_F16
+    else -> ColorType.N32
+}
+
+private fun ColorType.toComposeConfig() = when (this) {
+    ColorType.N32 -> ImageBitmapConfig.Argb8888
+    ColorType.ALPHA_8 -> ImageBitmapConfig.Alpha8
+    ColorType.RGB_565 -> ImageBitmapConfig.Rgb565
+    ColorType.RGBA_F16 -> ImageBitmapConfig.F16
+    else -> ImageBitmapConfig.Argb8888
+}
+
+private fun org.jetbrains.skia.ColorSpace?.toComposeColorSpace(): ColorSpace {
+    return when (this) {
+        org.jetbrains.skia.ColorSpace.sRGB -> ColorSpaces.Srgb
+        org.jetbrains.skia.ColorSpace.sRGBLinear -> ColorSpaces.LinearSrgb
+        org.jetbrains.skia.ColorSpace.displayP3 -> ColorSpaces.DisplayP3
+        else -> ColorSpaces.Srgb
+    }
+}
+
+// TODO(demin): support all color spaces.
+//  to do this we need to implement SkColorSpace::MakeRGB in skia
+private fun ColorSpace.toSkiaColorSpace(): org.jetbrains.skia.ColorSpace {
+    return when (this) {
+        ColorSpaces.Srgb -> org.jetbrains.skia.ColorSpace.sRGB
+        ColorSpaces.LinearSrgb -> org.jetbrains.skia.ColorSpace.sRGBLinear
+        ColorSpaces.DisplayP3 -> org.jetbrains.skia.ColorSpace.displayP3
+        else -> org.jetbrains.skia.ColorSpace.sRGB
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopShader.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopShader.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopTileMode.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopTileMode.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaTileMode.skiko.kt
diff --git a/compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopVertexMode.desktop.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt
similarity index 100%
rename from compose/ui/ui-graphics/src/desktopMain/kotlin/androidx/compose/ui/graphics/DesktopVertexMode.desktop.kt
rename to compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9ec2be1..8887613 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,6 +150,7 @@
 rxjava3 = { module = "io.reactivex.rxjava3:rxjava", version = "3.0.0" }
 shadow = { module = "com.github.jengelman.gradle.plugins:shadow", version = "6.1.0" }
 skiko = { module = "org.jetbrains.skiko:skiko-jvm", version.ref = "skiko" }
+skikoCommon = { module = "org.jetbrains.skiko:skiko", version.ref = "skiko" }
 skikoMacOsArm64 = { module = "org.jetbrains.skiko:skiko-jvm-runtime-macos-arm64", version.ref = "skiko" }
 skikoMacOsX64 = { module = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64", version.ref = "skiko" }
 skikoWindowsX64 = { module = "org.jetbrains.skiko:skiko-jvm-runtime-windows-x64", version.ref = "skiko" }