| /* |
| * Copyright (C) 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 com.example.testapp |
| |
| import android.graphics.Bitmap |
| import android.graphics.Canvas |
| import android.renderscript.Element |
| import android.renderscript.RenderScript |
| import android.renderscript.toolkit.Range2d |
| import android.renderscript.toolkit.Rgba3dArray |
| import android.renderscript.toolkit.YuvFormat |
| import java.nio.ByteBuffer |
| import java.util.Random |
| import kotlin.math.floor |
| import kotlin.math.max |
| import kotlin.math.min |
| |
| /** |
| * A vector of 4 integers. |
| */ |
| class Int4( |
| var x: Int = 0, |
| var y: Int = 0, |
| var z: Int = 0, |
| var w: Int = 0 |
| ) { |
| operator fun plus(other: Int4) = Int4(x + other.x, y + other.y, z + other.z, w + other.w) |
| operator fun plus(n: Int) = Int4(x + n, y + n, z + n, w + n) |
| |
| operator fun minus(other: Int4) = Int4(x - other.x, y - other.y, z - other.z, w - other.w) |
| operator fun minus(n: Int) = Int4(x - n, y - n, z - n, w - n) |
| |
| operator fun times(other: Int4) = Int4(x * other.x, y * other.y, z * other.z, w * other.w) |
| operator fun times(n: Int) = Int4(x * n, y * n, z * n, w * n) |
| |
| fun toFloat4() = Float4(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) |
| } |
| |
| fun min(a: Int4, b: Int4) = Int4(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w)) |
| |
| /** |
| * A vector of 4 floats. |
| */ |
| data class Float4( |
| var x: Float = 0f, |
| var y: Float = 0f, |
| var z: Float = 0f, |
| var w: Float = 0f |
| ) { |
| operator fun plus(other: Float4) = Float4(x + other.x, y + other.y, z + other.z, w + other.w) |
| operator fun plus(f: Float) = Float4(x + f, y + f, z + f, w + f) |
| |
| operator fun minus(other: Float4) = Float4(x - other.x, y - other.y, z - other.z, w - other.w) |
| operator fun minus(f: Float) = Float4(x - f, y - f, z - f, w - f) |
| |
| operator fun times(other: Float4) = Float4(x * other.x, y * other.y, z * other.z, w * other.w) |
| operator fun times(f: Float) = Float4(x * f, y * f, z * f, w * f) |
| |
| operator fun div(other: Float4) = Float4(x / other.x, y / other.y, z / other.z, w / other.w) |
| operator fun div(f: Float) = Float4(x / f, y / f, z / f, w / f) |
| |
| fun intFloor() = Int4(floor(x).toInt(), floor(y).toInt(), floor(z).toInt(), floor(w).toInt()) |
| } |
| |
| /** |
| * Convert a UByteArray to a Float4 vector |
| */ |
| @ExperimentalUnsignedTypes |
| fun UByteArray.toFloat4(): Float4 { |
| require(size == 4) |
| return Float4(this[0].toFloat(), this[1].toFloat(), this[2].toFloat(), this[3].toFloat()) |
| } |
| |
| /** |
| * Convert a ByteArray to a Float4 vector |
| */ |
| @ExperimentalUnsignedTypes |
| fun ByteArray.toFloat4(): Float4 { |
| require(size == 4) |
| return Float4( |
| this[0].toUByte().toFloat(), |
| this[1].toUByte().toFloat(), |
| this[2].toUByte().toFloat(), |
| this[3].toUByte().toFloat() |
| ) |
| } |
| |
| data class Dimension(val sizeX: Int, val sizeY: Int, val sizeZ: Int) |
| |
| /** |
| * An RGBA value represented by 4 Int. |
| * |
| * Note that the arithmetical operations consider a 0..255 value the equivalent of 0f..1f. |
| * After adding or subtracting, the value is clamped. After multiplying, the value is rescaled to |
| * stay in the 0..255 range. This is useful for the Blend operation. |
| */ |
| @ExperimentalUnsignedTypes |
| data class Rgba( |
| var r: Int = 0, |
| var g: Int = 0, |
| var b: Int = 0, |
| var a: Int = 0 |
| ) { |
| operator fun plus(other: Rgba) = |
| Rgba(r + other.r, g + other.g, b + other.b, a + other.a).clampToUByteRange() |
| |
| operator fun minus(other: Rgba) = |
| Rgba(r - other.r, g - other.g, b - other.b, a - other.a).clampToUByteRange() |
| |
| operator fun times(other: Rgba) = Rgba(r * other.r, g * other.g, b * other.b, a * other.a) shr 8 |
| operator fun times(scalar: Int) = Rgba(r * scalar, g * scalar, b * scalar, a * scalar) shr 8 |
| |
| infix fun xor(other: Rgba) = Rgba(r xor other.r, g xor other.g, b xor other.b, a xor other.a) |
| |
| infix fun shr(other: Int) = Rgba(r shr other, g shr other, b shr other, a shr other) |
| |
| private fun clampToUByteRange() = Rgba( |
| r.clampToUByteRange(), |
| g.clampToUByteRange(), |
| b.clampToUByteRange(), |
| a.clampToUByteRange() |
| ) |
| } |
| |
| /** |
| * A 2D array of UByte vectors, stored in row-major format. |
| * |
| * Arrays of vectorSize == 3 are padded to 4. |
| */ |
| @ExperimentalUnsignedTypes |
| class Vector2dArray( |
| val values: UByteArray, |
| val vectorSize: Int, |
| val sizeX: Int, |
| val sizeY: Int |
| ) { |
| /** |
| * If true, index access that would try to get a value that's out of bounds will simply |
| * return the border value instead. E.g. for [3, -3] would return the value for [3, 0], |
| * assuming that the sizeX > 3. |
| */ |
| var clipReadToRange: Boolean = false |
| |
| operator fun get(x: Int, y: Int): UByteArray { |
| var fixedX = x |
| var fixedY = y |
| if (clipReadToRange) { |
| fixedX = min(max(x, 0), sizeX - 1) |
| fixedY = min(max(y, 0), sizeY - 1) |
| } else { |
| require(x in 0 until sizeX && y in 0 until sizeY) { "Out of bounds" } |
| } |
| val start = indexOfVector(fixedX, fixedY) |
| return UByteArray(paddedSize(vectorSize)) { values[start + it] } |
| } |
| |
| operator fun set(x: Int, y: Int, value: UByteArray) { |
| require(value.size == paddedSize(vectorSize)) { "Not the expected vector size" } |
| require(x in 0 until sizeX && y in 0 until sizeY) { "Out of bounds" } |
| val start = indexOfVector(x, y) |
| for (i in value.indices) { |
| values[start + i] = value[i] |
| } |
| } |
| |
| private fun indexOfVector(x: Int, y: Int) = ((y * sizeX) + x) * paddedSize(vectorSize) |
| |
| fun createSameSized() = Vector2dArray(UByteArray(values.size), vectorSize, sizeX, sizeY) |
| |
| fun forEach(restriction: Range2d?, work: (Int, Int) -> (Unit)) { |
| forEachCell(sizeX, sizeY, restriction, work) |
| } |
| } |
| |
| /** |
| * A 2D array of float vectors, stored in row-major format. |
| * |
| * Arrays of vectorSize == 3 are padded to 4. |
| */ |
| class FloatVector2dArray( |
| val values: FloatArray, |
| val vectorSize: Int, |
| val sizeX: Int, |
| val sizeY: Int |
| ) { |
| /** |
| * If true, index access that would try to get a value that's out of bounds will simply |
| * return the border value instead. E.g. for [3, -3] would return the value for [3, 0], |
| * assuming that the sizeX > 3. |
| */ |
| var clipAccessToRange: Boolean = false |
| |
| operator fun get(x: Int, y: Int): FloatArray { |
| var fixedX = x |
| var fixedY = y |
| if (clipAccessToRange) { |
| fixedX = min(max(x, 0), sizeX - 1) |
| fixedY = min(max(y, 0), sizeY - 1) |
| } else { |
| require(x in 0 until sizeX && y in 0 until sizeY) { "Out of bounds" } |
| } |
| val start = indexOfVector(fixedX, fixedY) |
| return FloatArray(vectorSize) { values[start + it] } |
| } |
| |
| operator fun set(x: Int, y: Int, value: FloatArray) { |
| require(x in 0 until sizeX && y in 0 until sizeY) { "Out of bounds" } |
| val start = indexOfVector(x, y) |
| for (i in value.indices) { |
| values[start + i] = value[i] |
| } |
| } |
| |
| private fun indexOfVector(x: Int, y: Int) = ((y * sizeX) + x) * paddedSize(vectorSize) |
| |
| fun createSameSized() = FloatVector2dArray(FloatArray(values.size), vectorSize, sizeX, sizeY) |
| |
| fun forEach(restriction: Range2d?, work: (Int, Int) -> (Unit)) { |
| forEachCell(sizeX, sizeY, restriction, work) |
| } |
| } |
| |
| /** |
| * A 2D array of RGBA data. |
| */ |
| @ExperimentalUnsignedTypes |
| class Rgba2dArray( |
| private val values: ByteArray, |
| val sizeX: Int, |
| val sizeY: Int |
| ) { |
| operator fun get(x: Int, y: Int): Rgba { |
| val i = indexOfVector(x, y) |
| return Rgba( |
| values[i].toUByte().toInt(), |
| values[i + 1].toUByte().toInt(), |
| values[i + 2].toUByte().toInt(), |
| values[i + 3].toUByte().toInt() |
| ) |
| } |
| |
| operator fun set(x: Int, y: Int, value: Rgba) { |
| // Verify that x, y, z, w are in the 0..255 range |
| require(value.r in 0..255) |
| require(value.g in 0..255) |
| require(value.b in 0..255) |
| require(value.a in 0..255) |
| val i = indexOfVector(x, y) |
| values[i] = value.r.toUByte().toByte() |
| values[i + 1] = value.g.toUByte().toByte() |
| values[i + 2] = value.b.toUByte().toByte() |
| values[i + 3] = value.a.toUByte().toByte() |
| } |
| |
| private fun indexOfVector(x: Int, y: Int) = ((y * sizeX) + x) * 4 |
| |
| fun forEachCell(restriction: Range2d?, work: (Int, Int) -> (Unit)) = |
| forEachCell(sizeX, sizeY, restriction, work) |
| } |
| |
| /** |
| * Return a value that's between start and end, with the fraction indicating how far along. |
| */ |
| fun mix(start: Float, end: Float, fraction: Float) = start + (end - start) * fraction |
| |
| fun mix(a: Float4, b: Float4, fraction: Float) = Float4( |
| mix(a.x, b.x, fraction), |
| mix(a.y, b.y, fraction), |
| mix(a.z, b.z, fraction), |
| mix(a.w, b.w, fraction) |
| ) |
| |
| /** |
| * For vectors of size 3, the original RenderScript has them occupy the same space as a size 4. |
| * While RenderScript had a method to avoid this padding, it did not apply to Intrinsics. |
| * |
| * To preserve compatibility, the Toolkit doing the same. |
| */ |
| fun paddedSize(vectorSize: Int) = if (vectorSize == 3) 4 else vectorSize |
| |
| /** |
| * Create a ByteArray of the specified size filled with random data. |
| */ |
| fun randomByteArray(seed: Long, sizeX: Int, sizeY: Int, elementSize: Int): ByteArray { |
| val r = Random(seed) |
| return ByteArray(sizeX * sizeY * elementSize) { (r.nextInt(255) - 128).toByte() } |
| } |
| |
| /** |
| * Create a FloatArray of the specified size filled with random data. |
| * |
| * By default, the random data is between 0f and 1f. The factor can be used to scale that. |
| */ |
| fun randomFloatArray( |
| seed: Long, |
| sizeX: Int, |
| sizeY: Int, |
| elementSize: Int, |
| factor: Float = 1f |
| ): FloatArray { |
| val r = Random(seed) |
| return FloatArray(sizeX * sizeY * elementSize) { r.nextFloat() * factor } |
| } |
| |
| /** |
| * Create a cube of the specified size filled with random data. |
| */ |
| fun randomCube(seed: Long, cubeSize: Dimension): ByteArray { |
| val r = Random(seed) |
| return ByteArray(cubeSize.sizeX * cubeSize.sizeY * cubeSize.sizeZ * 4) { |
| (r.nextInt(255) - 128).toByte() |
| } |
| } |
| |
| /** |
| * Create the identity cube, i.e. one that if used in Lut3d, the output is the same as the input |
| */ |
| @ExperimentalUnsignedTypes |
| fun identityCube(cubeSize: Dimension): ByteArray { |
| val data = ByteArray(cubeSize.sizeX * cubeSize.sizeY * cubeSize.sizeZ * 4) |
| val cube = Rgba3dArray(data, cubeSize.sizeX, cubeSize.sizeY, cubeSize.sizeZ) |
| for (z in 0 until cubeSize.sizeZ) { |
| for (y in 0 until cubeSize.sizeY) { |
| for (x in 0 until cubeSize.sizeX) { |
| cube[x, y, z] = |
| byteArrayOf( |
| (x * 255 / (cubeSize.sizeX - 1)).toByte(), |
| (y * 255 / (cubeSize.sizeY - 1)).toByte(), |
| (z * 255 / (cubeSize.sizeZ - 1)).toByte(), |
| (255).toByte() |
| ) |
| } |
| } |
| } |
| return data |
| } |
| |
| fun randomYuvArray(seed: Long, sizeX: Int, sizeY: Int, format: YuvFormat): ByteArray { |
| // YUV formats are not well defined for odd dimensions |
| require(sizeX % 2 == 0 && sizeY % 2 == 0) |
| val halfSizeX = sizeX / 2 |
| val halfSizeY = sizeY / 2 |
| var totalSize = 0 |
| when (format) { |
| YuvFormat.YV12 -> { |
| val strideX = roundUpTo16(sizeX) |
| totalSize = strideX * sizeY + roundUpTo16(strideX / 2) * halfSizeY * 2 |
| } |
| YuvFormat.NV21 -> totalSize = sizeX * sizeY + halfSizeX * halfSizeY * 2 |
| else -> require(false) { "Unknown YUV format $format" } |
| } |
| |
| return randomByteArray(seed, totalSize, 1, 1) |
| } |
| |
| /** |
| * Converts a float to a byte, clamping to make it fit the limited range. |
| */ |
| @ExperimentalUnsignedTypes |
| fun Float.clampToUByte(): UByte = min(255, max(0, (this + 0.5f).toInt())).toUByte() |
| |
| /** |
| * Converts a FloatArray to UByteArray, clamping. |
| */ |
| @ExperimentalUnsignedTypes |
| fun FloatArray.clampToUByte() = UByteArray(size) { this[it].clampToUByte() } |
| |
| /** |
| * Limits an Int to what can fit in a UByte. |
| */ |
| fun Int.clampToUByteRange(): Int = min(255, max(0, this)) |
| |
| /** |
| * Converts an Int to a UByte, clamping. |
| */ |
| @ExperimentalUnsignedTypes |
| fun Int.clampToUByte(): UByte = this.clampToUByteRange().toUByte() |
| |
| /** |
| * Converts a float (0f .. 1f) to a byte (0 .. 255) |
| */ |
| @ExperimentalUnsignedTypes |
| fun unitFloatClampedToUByte(num: Float): UByte = (num * 255f).clampToUByte() |
| |
| /** |
| * Convert a byte (0 .. 255) to a float (0f .. 1f) |
| */ |
| @ExperimentalUnsignedTypes |
| fun byteToUnitFloat(num: UByte) = num.toFloat() * 0.003921569f |
| |
| @ExperimentalUnsignedTypes |
| fun UByteArray.toFloatArray() = FloatArray(size) { this[it].toFloat() } |
| |
| /** |
| * For each cell that's in the 2D array defined by sizeX and sizeY, and clipped down by the |
| * restriction, invoke the work function. |
| */ |
| fun forEachCell(sizeX: Int, sizeY: Int, restriction: Range2d?, work: (Int, Int) -> (Unit)) { |
| val startX = restriction?.startX ?: 0 |
| val startY = restriction?.startY ?: 0 |
| val endX = restriction?.endX ?: sizeX |
| val endY = restriction?.endY ?: sizeY |
| for (y in startY until endY) { |
| for (x in startX until endX) { |
| work(x, y) |
| } |
| } |
| } |
| |
| operator fun FloatArray.times(other: FloatArray) = FloatArray(size) { this[it] * other[it] } |
| operator fun FloatArray.times(other: Float) = FloatArray(size) { this[it] * other } |
| operator fun FloatArray.plus(other: FloatArray) = FloatArray(size) { this[it] + other[it] } |
| operator fun FloatArray.minus(other: FloatArray) = FloatArray(size) { this[it] - other[it] } |
| |
| fun renderScriptVectorElementForU8(rs: RenderScript?, vectorSize: Int): Element { |
| when (vectorSize) { |
| 1 -> return Element.U8(rs) |
| 2 -> return Element.U8_2(rs) |
| 3 -> return Element.U8_3(rs) |
| 4 -> return Element.U8_4(rs) |
| } |
| throw java.lang.IllegalArgumentException("RenderScriptToolkit tests. Only vectors of size 1-4 are supported. $vectorSize provided.") |
| } |
| |
| fun renderScriptVectorElementForI32(rs: RenderScript?, vectorSize: Int): Element { |
| when (vectorSize) { |
| 1 -> return Element.I32(rs) |
| 2 -> return Element.I32_2(rs) |
| 3 -> return Element.I32_3(rs) |
| 4 -> return Element.I32_4(rs) |
| } |
| throw java.lang.IllegalArgumentException("RenderScriptToolkit tests. Only vectors of size 1-4 are supported. $vectorSize provided.") |
| } |
| |
| /* When we'll handle floats |
| fun renderScriptVectorElementForF32(rs: RenderScript?, vectorSize: Int): Element { |
| when (vectorSize) { |
| 1 -> return Element.F32(rs) |
| 2 -> return Element.F32_2(rs) |
| 3 -> return Element.F32_3(rs) |
| 4 -> return Element.F32_4(rs) |
| } |
| throw java.lang.IllegalArgumentException("RenderScriptToolkit tests. Only vectors of size 1-4 are supported. $vectorSize provided.") |
| }*/ |
| |
| fun renderScriptElementForBitmap(context: RenderScript, bitmap: Bitmap): Element { |
| return when (val config = bitmap.config) { |
| Bitmap.Config.ALPHA_8 -> Element.A_8(context) |
| Bitmap.Config.ARGB_8888 -> Element.RGBA_8888(context) |
| else -> throw IllegalArgumentException("RenderScript Toolkit can't support bitmaps with config $config.") |
| } |
| } |
| |
| fun getBitmapBytes(bitmap: Bitmap): ByteArray { |
| val buffer: ByteBuffer = ByteBuffer.allocate(bitmap.byteCount) |
| bitmap.copyPixelsToBuffer(buffer) |
| return buffer.array() |
| } |
| |
| fun vectorSizeOfBitmap(bitmap: Bitmap): Int { |
| return when (val config = bitmap.config) { |
| Bitmap.Config.ALPHA_8 -> 1 |
| Bitmap.Config.ARGB_8888 -> 4 |
| else -> throw IllegalArgumentException("RenderScript Toolkit can't support bitmaps with config $config.") |
| } |
| } |
| |
| fun duplicateBitmap(original: Bitmap): Bitmap { |
| val copy = Bitmap.createBitmap(original.width, original.height, original.config) |
| val canvas = Canvas(copy) |
| canvas.drawBitmap(original, 0f, 0f, null) |
| return copy |
| } |
| |
| @ExperimentalUnsignedTypes |
| fun logArray(prefix: String, array: ByteArray, number: Int = 20) { |
| val values = array.joinToString(limit = number) { it.toUByte().toString() } |
| println("$prefix[${array.size}] $values}\n") |
| } |
| |
| fun logArray(prefix: String, array: IntArray, number: Int = 20) { |
| val values = array.joinToString(limit = number) |
| println("$prefix[${array.size}] $values}\n") |
| } |
| |
| fun logArray(prefix: String, array: FloatArray?, number: Int = 20) { |
| val values = array?.joinToString(limit = number) { "%.2f".format(it) } ?: "(null)" |
| println("$prefix[${array?.size}] $values}\n") |
| } |
| |
| fun roundUpTo16(value: Int): Int { |
| require(value >= 0) |
| return (value + 15) and 15.inv() |
| } |