Bitmap / ColourWheel / Custom Controls / RenderScript

Colour Wheel – Part 3

In Android Weekly issue # 297 there was a link to a library named ColorPickerPreference which I found interesting. One aspect of it was a colour picker which included a colour wheel but I was mildly disappointed to see that it did not include a mechanism of adjusting the brightness value, and was implemented using a bitmap resource which was only provided at a single density, meaning that it will not necessarily scale well on different devices. In this short series we’ll look at how we can actually render a colour wheel dynamically.

In the previous article we created our RenderScript script for generating our colour wheel graphic, but now we need to actually call it from the BitmapGenerator class that we created in Part 1. This requires a little effort because we have to we are having to marshall things to and from the native environment in which the RenderScript engine will run.

Before we get stuck in to that, there is a utility class that will help to simplify our code:

internal class AutoCreate<out T>(
        private val cleanup: (T.() -> Unit)? = null,
        private val creator: () -> T
) {

    private var _backing: T? = null

    val value: T
        get() {
            if (_backing == null) {
                _backing = creator()
                assert(_backing != null)
            }
            @Suppress("UNCHECKED_CAST")
            return _backing as T
        }

    fun clear() {
        _backing?.also {
            cleanup?.invoke(it)
        }
        _backing = null
    }
}

This has an immutable, non-null property named value along with a private, mutable, nullable property named _backing. Whenever value is retrieved, we first check if _backing is null and if so call a creator() function (that was passed to the constructor) in order to create a new instance and store it in _backing. For subsequent gets the value from _backing will be returned. On the face of it, this looks like we could just use a lazy delegate, however it is the clear() function which adds the extra functionality that we need. When it is called, it will invoke a cleanup() function (if one was supplied in the constructor), and then set _backing to null. So the next retrieval of value will cause a new instance of the object to be created and store in _backing.

Hopefully, a quick example will show why this is useful:

class BitmapGenerator(
        private val androidContext: Context,
        private val config: Bitmap.Config,
        private val observer: BitmapObserver
) : ReadWriteProperty<Any, Byte> {
    
    private val size = Size(0, 0)

    var brightness = Byte.MAX_VALUE
    ...
    private val generated = AutoCreate(Bitmap::recycle) {
        Bitmap.createBitmap(size.width, size.height, config)
    }
    ...
    override fun getValue(thisRef: Any, property: KProperty<*>): Byte =
            brightness

    override fun setValue(thisRef: Any, property: KProperty<*>, value: Byte) {
        brightness = value
        generate()
    }

    fun setSize(width: Int, height: Int) {
        size.takeIf { it.width != width || it.height != height }?.also {
            generated.clear()
        }
        size.width = width
        size.height = height
        generate()
    }
    ...
    private data class Size(var width: Int, var height: Int) {
        val hasDimensions
            get() = width > 0 && height > 0
    }
}

Here we have a property named generated which is an AutoCreate instance. Initially the _backing property is null, but when we retrieve generated.value a new bitmap of the correct size will be generated because the lambda on the AutoCreate constructor will be called: Bitmap.createBitmap(size.width, size.height, config). When the size changes (in setSize()) we call generated.clear() which will invoke recycle() on the bitmap (the argument on the AutoCreate constructor), and the _backing property will be set to null. When when we retrieve generated.value next, then a new bitmap will be created with the new size.

We also create AutoCreate instances for the Allocation which is used to pass the bitmap data in to the RenderScript kernel, and the script instance itself:

class BitmapGenerator(
        private val androidContext: Context,
        private val config: Bitmap.Config,
        private val observer: BitmapObserver
) : ReadWriteProperty<Any, Byte> {
    ...
    private val generatedAllocation = AutoCreate(Allocation::destroy) {
        Allocation.createFromBitmap(renderscript,
                generated.value,
                Allocation.MipmapControl.MIPMAP_NONE,
                Allocation.USAGE_SCRIPT)
    }

    private val colourWheelScript = AutoCreate(ScriptC_ColourWheel::destroy) {
        ScriptC_ColourWheel(renderscript)
    }
    ...
    fun setSize(width: Int, height: Int) {
        size.takeIf { it.width != width || it.height != height }?.also {
            generated.clear()
            generatedAllocation.clear()
        }
        size.width = width
        size.height = height
        generate()
    }
    ...
}

generatedAllocation is created from the bitmap dimensions that are retrieved from bitmap.vale (which is our AutoCreate instance that we just looked at).

Currently this will not run because the creator lambda for colourWheelScript requires a variable named renderscript. This is actually the RenderScript context that the script will be run within and it is fairly costly to setup and tear down, so we will keep a single instance alive for the lifetime of the BitmapCreator instance to avoid any unnecessary overhead:

class BitmapGenerator(
        private val androidContext: Context,
        private val config: Bitmap.Config,
        private val observer: BitmapObserver
) : ReadWriteProperty<Any, Byte> {
    ...
    private var rsCreation: Deferred<RenderScript> = async(CommonPool) {
        RenderScript.create(androidContext).also {
            _renderscript = it
        }
    }

    private var _renderscript: RenderScript? = null
    private val renderscript: RenderScript
        get() {
            assert(rsCreation.isCompleted)
            return _renderscript as RenderScript
        }
    ...
    fun stop() {
        generated.clear()
        generatedAllocation.clear()
        colourWheelScript.clear()
        _renderscript?.destroy()
        rsCreation.takeIf { it.isActive }?.cancel()
    }
    ...
}

The creation of the RenderScript context actually begins as soon as the BitmapGenerator instance is created. This is done through a coroutine wish runs on CommonPool (i.e. a background task) and the Deferred object is stored as a val named rsCreation. Once the RenderScript context had been created it is stored to the property named _renderscript.

The rsCreation instance is quite useful because is allows us to wait for the background task to complete if it hasn’t already. The generate() function shows this:

class BitmapGenerator(
        private val androidContext: Context,
        private val config: Bitmap.Config,
        private val observer: BitmapObserver
) : ReadWriteProperty<Any, Byte> {
    ...
    private fun generate() {
        if (size.hasDimensions && generateProcess?.isCompleted != false) {
            generateProcess = launch(CommonPool) {
                rsCreation.await()
                generated.value.also {
                    draw(it)
                    launch(UI) {
                        observer.bitmapChanged(it)
                    }
                }
            }
        }
    }
    ...
    interface BitmapObserver {
        fun bitmapChanged(bitmap: Bitmap)
    }
    ...
}

Depending on how soon the generate() function is called after the BitmapGenerator instance has been created then the RenderScript context creation may have completed, or it may still be in progress. By calling await() from a background task means that when it returns, the rsCreation task will be complete. This helps us to avoid a race condition when we try and use the RenderScript context before the background task to create it has completed.

So once we hit the following line, we know that RenderScript is ready to go. We first get the Bitmap we want to draw to (which may dynamically create it thanks to our AutoCreate class), and then perform the drawing operation – we’ll look at this in detail in a moment. Once the draw is complete then we make a callback on the UI thread to an observer to indicate that the Bitmap has changed – this observer is our custom View, and will update the image.

The final piece is the draw() function:

class BitmapGenerator(
        private val androidContext: Context,
        private val config: Bitmap.Config,
        private val observer: BitmapObserver
) : ReadWriteProperty<Any, Byte> {
    ...
    private fun draw(bitmap: Bitmap) {
        generatedAllocation.value.apply {
            copyFrom(bitmap)
            colourWheelScript.value.invoke_colourWheel(
                    colourWheelScript.value,
                    this,
                    brightness.toFloat() / Byte.MAX_VALUE.toFloat()
            )
            copyTo(bitmap)
        }
    }
    ...
}

generatedAllocation is an AutoCreate instance which will create the Allocation on demand. The Allocation is what gets passed to our script as the second argument to the colourWheel() function that we looked at in the previous post. We then copy the contents of the Bitmap in to the Allocation instance.

Next we have colourWheelScript which is am AutoCreate instance. Once again this will be created on demand but unlike the Bitmap and Allocation objects this will not need to be re-created if the size of the bitmap changes. However, like the RenderScript context, it can be quite expensive to keep creating and destroying each time we need it, so we use an AutoCreate object to enable us to create it as we need it, but also properly clean it up when we’re done with it.

The ScriptC_ColourWheel class is a Java wrapper around our RenderScript script that is generated by the RenderScript compiler (you can take a look at the generated code in app/build/generated/source/rs after building the project), and this means we don’t have to directly call native code from within our Java / Kotlin – we just use the wrapper. The invoke_colourWheel() function is a wrapper around the colourWheel() function in our RenderScript script, and takes the same three arguments: the first is a reference to the script object itself, the second is our allocation, and the third is our brightness value in the range 0.0-1.0. This invokes the kernel for each pixel in the bitmap which was copied to the allocation, and our colour wheel graphic is created.

Finally we need to copy the contents of the Allocation back to the bitmap and we’re done.

So if we run this we see the following behaviour (there are some compression artefacts on the video, but the real things is blemish-free, I promise!):

So we can instantly see a vast improvement upon the two seconds that it was taking to render each brightness change by performing this in the JVM, but just how much? I did some benchmarking and on the Pixel XL it was taking just over 40ms to generate an 896×896 image. So while this is still not fast enough for buttery smooth 60fps animations, it is actually more than adequate to update in a timely manner when the user changes the value of the brightness slider, so I’m happy that this is now good enough.

The source code for this article is available here.

© 2018, Mark Allison. All rights reserved.

Copyright © 2018 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

1 Comment

  1. Hi Mark,
    Great series so far. Toying with renderscript has been in my queue of things to do, and today you inspired me.

    First: Your source link goes to a private repo. You have not pushed Part3 to the public one.

    Second: I managed to get a 4x improvement on my Pixel 2, going from ~22 ms per BitmapGenerator::draw to ~4 ms. Heres how:
    1. On minSdk 14 theres no need to use the support RenderScript. This along did not affect performance but it did enable some other optimizations.
    2. In ColorWheel.rs add #pragma rs_fp_relaxed and change from using hypot to calculating pythagorean theorem by hand. (A guess: sqrt uses some SIMD instructions that hypot does not)

    Source: https://github.com/saik0/ColourWheel/tree/joel/Part3-optimized
    Also has micro optimization of using radius squared to determine whether or not inside the circle, though it didnt affect perf much.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.