Search code examples
androidandroid-jetpack-composefragment-shaderandroid-graphics

How to create a feedback shader in android AGSL?


I'm on Android 13 (tiramisu) and use the new RuntimeShader API to write a pixel shader in AGSL (Android Graphic Shader Language), in order to use it in a composable as background. So far all works.

Now I want to create a feedback effect, where the shader can eval pixel of it's previous rendered result. This requires to prepare an writable input buffer to be passed to the shader. The shader looking like this

@Language("AGSL")
val SHADER = """
    uniform shader feedback;
    half4 main(float2 coord) {
      return feedback.eval(coord).bgra; // simple swap
    }
""".trimIndent()

the buffer would be the uniform variable feedback.

First I tried to create a bitmap, to use it as a BitmapShader as proposed here and tried to use it as

val shader = RuntimeShader(SHADER)
val shaderBrush = ShaderBrush(shader)
shader.setInputBuffer("feedback", BitmapShader(???))

However, the bitmap is the wrong mechanism, since the shader requires hardware rendering (as pointed out by https://twitter.com/chethaase).

Next attempt was to create a RenderNode, and draw the shader into a recording canvas, and attach the shader as a render effect.

myRenderNode.setRenderEffect(RenderEffect.createRuntimeShaderEffect(shader, "feedback")) 

So far this works for static render nodes, it is elaborated more in this article. But the moment I want to re-use the same render node, and overwrite it in the next rendering cycle, it crashes.

// keep a full screen copy
val feedbackCanvas = myRenderNode.beginRecording()
feedbackCanvas.drawRenderNode(myRenderNode)
myRenderNode.endRecording()

The crash is something lo-level, containing no information I can interpret:

Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR) in RenderThread.

I tried also double buffering with the same result.

My question is, how to repeatedly save render result and pass it to the shader? Any hint would be great


Solution

  • I found a solution, using the BitmapShader but with a bitmap which remains on the hardware. This bitmap can be created using Picture. To make a copy of the screen

    val paint = Paint()
    val shader = RuntimeShader(SHADER)
    val shaderBrush = ShaderBrush(shader)
    shaderBrush.applyTo(size, paint, 0.5f)
    
    val picture = Picture()
    val canvas = picture.beginRecording(rect.width(), rect.height())
    canvas.drawRect(rect, paint.asFrameworkPaint())
    picture.endRecording()
    
    // this is a bitmap with Config.HARDWARE
    hardwareBitmap = Bitmap.createBitmap(picture)
    

    and then to pass the hardwareBitmap to the shader

    shader.setInputBuffer("feedback",
         BitmapShader(hardwareBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
    

    the performance is a little underwhelming but fair enough for my case, but maybe there is still some copying of data between CPU and GPU, which could be optimized