Unverified Commit a11256b2 authored by Oleksandr Karpovich's avatar Oleksandr Karpovich Committed by GitHub

Add suspend fun Bitmap.installPixelsFromArrayBuffer for web (#1158)

This function will copy the pixels bytes directly from JS ArrayBuffer to
skiko linear memory. The allocated memory will be freed when SkBitmap is
removed.
parent f30f9eee
package org.jetbrains.skia.webext
import kotlinx.coroutines.await
import org.jetbrains.skiko.wasm.awaitSkiko
internal actual suspend fun getSkikoWasm(): SkikoWasm {
return awaitSkiko.await().unsafeCast<SkikoWasm>()
}
internal actual fun skikoArrayBuffer(skikoWasm: SkikoWasm): WebArrayBufferExt =
js("skikoWasm.wasmExports.memory.buffer")
internal actual fun copyBuffer(
src: WebArrayBufferExt,
dst: WebArrayBufferExt,
size: Int,
dstOffset: Int
) {
js("""
var dstView = new Uint8Array(dst);
var srcView = new Uint8Array(src);
dstView.set(srcView, dstOffset);
""")
}
\ No newline at end of file
...@@ -5,4 +5,4 @@ package org.jetbrains.skiko.wasm ...@@ -5,4 +5,4 @@ package org.jetbrains.skiko.wasm
import kotlin.js.Promise import kotlin.js.Promise
external val awaitSkiko: Promise<Unit> internal external val awaitSkiko: Promise<Any>
...@@ -159,6 +159,20 @@ SKIKO_EXPORT KBoolean org_jetbrains_skia_Bitmap__1nInstallPixels ...@@ -159,6 +159,20 @@ SKIKO_EXPORT KBoolean org_jetbrains_skia_Bitmap__1nInstallPixels
return instance->installPixels(imageInfo, copy, rowBytes, deletePixelsBytes, nullptr); return instance->installPixels(imageInfo, copy, rowBytes, deletePixelsBytes, nullptr);
} }
// Currently for web only:
SKIKO_EXPORT KBoolean org_jetbrains_skia_Bitmap__1nInstallPixelsFromPointer
(KNativePointer ptr, KInt width, KInt height, KInt colorType, KInt alphaType, KNativePointer colorSpacePtr, KNativePointer pixelsPtr, KInt rowBytes) {
SkBitmap* instance = reinterpret_cast<SkBitmap*>(ptr);
SkColorSpace* colorSpace = reinterpret_cast<SkColorSpace*>(colorSpacePtr);
SkImageInfo imageInfo = SkImageInfo::Make(width,
height,
static_cast<SkColorType>(colorType),
static_cast<SkAlphaType>(alphaType),
sk_ref_sp<SkColorSpace>(colorSpace));
return instance->installPixels(imageInfo, pixelsPtr, rowBytes, deletePixelsBytes, nullptr);
}
SKIKO_EXPORT KBoolean org_jetbrains_skia_Bitmap__1nAllocPixels SKIKO_EXPORT KBoolean org_jetbrains_skia_Bitmap__1nAllocPixels
(KNativePointer ptr) { (KNativePointer ptr) {
SkBitmap* instance = reinterpret_cast<SkBitmap*>((ptr)); SkBitmap* instance = reinterpret_cast<SkBitmap*>((ptr));
......
package org.jetbrains.skia.webext
import kotlinx.coroutines.await
import org.jetbrains.skiko.wasm.awaitSkiko
internal actual suspend fun getSkikoWasm(): SkikoWasm {
return awaitSkiko.await()
}
internal actual fun skikoArrayBuffer(skikoWasm: SkikoWasm): WebArrayBufferExt =
js("skikoWasm.wasmExports.memory.buffer")
internal actual fun copyBuffer(
src: WebArrayBufferExt,
dst: WebArrayBufferExt,
size: Int,
dstOffset: Int
) {
js("""
var dstView = new Uint8Array(dst);
var srcView = new Uint8Array(src);
dstView.set(srcView, dstOffset);
""")
}
\ No newline at end of file
package org.jetbrains.skiko.wasm package org.jetbrains.skiko.wasm
import kotlinx.coroutines.await
import org.jetbrains.skia.impl.NativePointer import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skiko.GL import org.jetbrains.skiko.GL
import org.jetbrains.skiko.w3c.HTMLCanvasElement import org.jetbrains.skiko.w3c.HTMLCanvasElement
import org.khronos.webgl.ArrayBuffer
@JsFun( @JsFun(
"""() => { """() => {
......
@file:OptIn(ExperimentalSkikoApi::class)
package org.jetbrains.skia.webext
import org.jetbrains.skia.impl.Native
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl._malloc
import org.jetbrains.skiko.ExperimentalSkikoApi
internal expect fun skikoArrayBuffer(skikoWasm: SkikoWasm): WebArrayBufferExt
/**
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
*/
@ExperimentalSkikoApi
external interface WebArrayBufferExt {
val byteLength: Int
operator fun set(ix: Int, value: Byte)
}
internal external interface SkikoWasm
internal suspend fun copyBufferToSkiko(
src: WebArrayBufferExt
): NativePointer {
val ptr = _malloc(src.byteLength)
if (ptr != Native.NullPointer) {
val skikoArrayBuffer = skikoArrayBuffer(getSkikoWasm())
copyBuffer(src, skikoArrayBuffer, src.byteLength, ptr)
}
return ptr
}
internal expect suspend fun getSkikoWasm(): SkikoWasm
internal expect fun copyBuffer(src: WebArrayBufferExt, dst: WebArrayBufferExt, size: Int, dstOffset: Int)
\ No newline at end of file
package org.jetbrains.skia.webext
import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.ExternalSymbolName
import org.jetbrains.skia.ImageInfo
import org.jetbrains.skia.impl.Native
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.getPtr
import org.jetbrains.skiko.ExperimentalSkikoApi
@ExperimentalSkikoApi
suspend fun Bitmap.installPixelsFromArrayBuffer(
info: ImageInfo,
pixelsArrayBuffer: WebArrayBufferExt,
rowBytes: Int
): Boolean {
val pixelsPtr = copyBufferToSkiko(pixelsArrayBuffer)
if (pixelsPtr == Native.NullPointer) return false
return _nInstallPixelsFromPointer(
_ptr,
info.width,
info.height,
info.colorInfo.colorType.ordinal,
info.colorInfo.alphaType.ordinal,
getPtr(info.colorInfo.colorSpace),
pixelsPtr,
rowBytes
)
}
@ExternalSymbolName("org_jetbrains_skia_Bitmap__1nInstallPixelsFromPointer")
private external fun _nInstallPixelsFromPointer(
ptr: NativePointer,
width: Int,
height: Int,
colorType: Int,
alphaType: Int,
colorSpacePtr: NativePointer,
pixelsPointer: NativePointer,
rowBytes: Int
): Boolean
\ No newline at end of file
package org.jetbrains.skia.webext
import org.jetbrains.skia.Bitmap
import org.jetbrains.skia.ColorAlphaType
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.ImageInfo
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl._free
import org.jetbrains.skia.impl.skia_memGetByte
import org.jetbrains.skiko.tests.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.measureTimedValue
class WebExtTest {
@Test
fun canCopyArrayBufferToSkikoMemory() = runTest {
val bytes = ByteArray(16_000_000) { (it and 0xFF).toByte() }
val ba = createInt8ArrayToCopy(bytes.size)
for (i in bytes.indices) ba[i] = bytes[i]
val ptr = measureTimedValue<NativePointer> { copyBufferToSkiko(ba.buffer) }
println("copyBufferToSkiko took ${ptr.duration}\n")
// Sanity check
assertEquals(bytes[2], skia_memGetByte(ptr.value + 2))
assertEquals(bytes[228], skia_memGetByte(ptr.value + 228))
assertEquals(bytes[5555], skia_memGetByte(ptr.value + 5555))
assertEquals(bytes[10_000_000], skia_memGetByte(ptr.value + 10_000_000))
assertEquals(bytes[14_000_000], skia_memGetByte(ptr.value + 14_000_000))
_free(ptr.value)
}
@Test
fun canCreateBitmapAndSetPixelsFromArrayBuffer() = runTest {
val width = 100
val height = 100
val bytesPerPixel = 4 // RGBA_8888
val bytes = ByteArray(width * height * bytesPerPixel) { i ->
val pixelIndex = i / 4
val x = pixelIndex % width
val y = pixelIndex / width
when (i % 4) {
0 -> x.toByte() // R: gradient by X
1 -> y.toByte() // G: gradient by Y
2 -> (x + y).toByte() // B: combined gradient
3 -> 255.toByte() // A: fully opaque
else -> 0
}
}
val ba = createInt8ArrayToCopy(bytes.size)
for (i in bytes.indices) ba[i] = bytes[i]
val info = ImageInfo(width, height, ColorType.RGBA_8888, ColorAlphaType.PREMUL)
val bitmap = Bitmap()
val success = measureTimedValue {
bitmap.installPixelsFromArrayBuffer(info, ba.buffer, width * bytesPerPixel)
}
println("installPixelsFromArrayBuffer took ${success.duration}\n")
@Suppress("RedundantIf") // Weird? Yes. on js success is not a boolean, it's = 1
assertEquals(true, if (success.value) true else false)
// Check some specific pixels
// (0,0) -> R=0, G=0, B=0, A=255 -> 0xFF000000
assertEquals(0xFF000000.toInt(), bitmap.getColor(0, 0))
// (50, 0) -> R=50, G=0, B=50, A=255 -> 0xFF320032 (50 is 0x32)
assertEquals(0xFF320032.toInt(), bitmap.getColor(50, 0))
// (0, 50) -> R=0, G=50, B=50, A=255 -> 0xFF003232
assertEquals(0xFF003232.toInt(), bitmap.getColor(0, 50))
// (99, 99) -> R=99, G=99, B=198, A=255 -> 0xFF6363C6 (99 is 0x63, 198 is 0xC6)
assertEquals(0xFF6363C6.toInt(), bitmap.getColor(99, 99))
}
}
private fun createInt8ArrayToCopy(length: Int): Int8ArrayInternal =
js("new Int8Array(length)")
private external interface Int8ArrayInternal {
val buffer: WebArrayBufferExt
}
private operator fun Int8ArrayInternal.set(index: Int, value: Byte) {
int8ArraySet(this, index, value)
}
private fun int8ArraySet(arrayInternal: Int8ArrayInternal, index: Int, value: Byte) {
js("arrayInternal[index] = value;")
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment