509 lines
17 KiB
Kotlin
509 lines
17 KiB
Kotlin
/*
|
|
* 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()
|
|
}
|