Add RuntimeShader API to RenderEffect

Bug: 201546136
Test: atest CtsUiRenderingTestCases:RuntimeShaderTests
Change-Id: I96ad4fcfc0486f340653878519efcc4a793191a2
diff --git a/core/api/current.txt b/core/api/current.txt
index f84322e..4b9f57e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -16518,6 +16518,7 @@
     method @NonNull public static android.graphics.RenderEffect createColorFilterEffect(@NonNull android.graphics.ColorFilter);
     method @NonNull public static android.graphics.RenderEffect createOffsetEffect(float, float);
     method @NonNull public static android.graphics.RenderEffect createOffsetEffect(float, float, @NonNull android.graphics.RenderEffect);
+    method @NonNull public static android.graphics.RenderEffect createRuntimeShaderEffect(@NonNull android.graphics.RuntimeShader, @NonNull String);
     method @NonNull public static android.graphics.RenderEffect createShaderEffect(@NonNull android.graphics.Shader);
   }
 
diff --git a/graphics/java/android/graphics/RenderEffect.java b/graphics/java/android/graphics/RenderEffect.java
index ad4c3fe..b8a4685 100644
--- a/graphics/java/android/graphics/RenderEffect.java
+++ b/graphics/java/android/graphics/RenderEffect.java
@@ -290,6 +290,22 @@
         return new RenderEffect(nativeCreateShaderEffect(shader.getNativeInstance()));
     }
 
+    /**
+     * Create a {@link RenderEffect} that executes the provided {@link RuntimeShader} and passes
+     * the contents of the {@link android.graphics.RenderNode} that this RenderEffect is installed
+     * on as an input to the shader.
+     * @param shader the runtime shader that will bind the inputShaderName to the RenderEffect input
+     * @param uniformShaderName the uniform name defined in the RuntimeShader's program to which
+     *                         the contents of the RenderNode will be bound
+     */
+    @NonNull
+    public static RenderEffect createRuntimeShaderEffect(
+            @NonNull RuntimeShader shader, @NonNull String uniformShaderName) {
+        return new RenderEffect(
+                nativeCreateRuntimeShaderEffect(shader.getNativeShaderBuilder(),
+                        uniformShaderName));
+    }
+
     private final long mNativeRenderEffect;
 
     /* only constructed from static factory methods */
@@ -318,5 +334,7 @@
     private static native long nativeCreateBlendModeEffect(long dst, long src, int blendmode);
     private static native long nativeCreateChainEffect(long outer, long inner);
     private static native long nativeCreateShaderEffect(long shader);
+    private static native long nativeCreateRuntimeShaderEffect(
+            long shaderBuilder, String inputShaderName);
     private static native long nativeGetFinalizer();
 }
diff --git a/libs/hwui/jni/RenderEffect.cpp b/libs/hwui/jni/RenderEffect.cpp
index a48d7f7..213f35a 100644
--- a/libs/hwui/jni/RenderEffect.cpp
+++ b/libs/hwui/jni/RenderEffect.cpp
@@ -127,6 +127,32 @@
     return reinterpret_cast<jlong>(shaderFilter.release());
 }
 
+static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) {
+    va_list args;
+    va_start(args, fmt);
+    int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args);
+    va_end(args);
+    return ret;
+}
+
+static jlong createRuntimeShaderEffect(JNIEnv* env, jobject, jlong shaderBuilderHandle,
+                                       jstring inputShaderName) {
+    SkRuntimeShaderBuilder* builder =
+            reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilderHandle);
+    ScopedUtfChars name(env, inputShaderName);
+
+    if (builder->child(name.c_str()).fChild == nullptr) {
+        ThrowIAEFmt(env,
+                    "unable to find a uniform with the name '%s' of the correct "
+                    "type defined by the provided RuntimeShader",
+                    name.c_str());
+        return 0;
+    }
+
+    sk_sp<SkImageFilter> filter = SkImageFilters::RuntimeShader(*builder, name.c_str(), nullptr);
+    return reinterpret_cast<jlong>(filter.release());
+}
+
 static void RenderEffect_safeUnref(SkImageFilter* filter) {
     SkSafeUnref(filter);
 }
@@ -136,15 +162,16 @@
 }
 
 static const JNINativeMethod gRenderEffectMethods[] = {
-    {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer},
-    {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect},
-    {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect},
-    {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect},
-    {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect},
-    {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect},
-    {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect},
-    {"nativeCreateShaderEffect", "(J)J", (void*)createShaderEffect}
-};
+        {"nativeGetFinalizer", "()J", (void*)getRenderEffectFinalizer},
+        {"nativeCreateOffsetEffect", "(FFJ)J", (void*)createOffsetEffect},
+        {"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect},
+        {"nativeCreateBitmapEffect", "(JFFFFFFFF)J", (void*)createBitmapEffect},
+        {"nativeCreateColorFilterEffect", "(JJ)J", (void*)createColorFilterEffect},
+        {"nativeCreateBlendModeEffect", "(JJI)J", (void*)createBlendModeEffect},
+        {"nativeCreateChainEffect", "(JJ)J", (void*)createChainEffect},
+        {"nativeCreateShaderEffect", "(J)J", (void*)createShaderEffect},
+        {"nativeCreateRuntimeShaderEffect", "(JLjava/lang/String;)J",
+         (void*)createRuntimeShaderEffect}};
 
 int register_android_graphics_RenderEffect(JNIEnv* env) {
     android::RegisterMethodsOrDie(env, "android/graphics/RenderEffect",
diff --git a/tests/HwAccelerationTest/Android.bp b/tests/HwAccelerationTest/Android.bp
index e618ed1..51848f2 100644
--- a/tests/HwAccelerationTest/Android.bp
+++ b/tests/HwAccelerationTest/Android.bp
@@ -32,6 +32,10 @@
         "**/*.java",
         "**/*.kt",
     ],
+    static_libs: [
+        "androidx.cardview_cardview",
+    ],
+
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index 22fe424..b0ccbd1 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -789,6 +789,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="RenderEffectViewActivity"
+                  android:label="RenderEffect/View"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="com.android.test.hwui.TEST"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name="StretchShaderActivity"
                   android:label="RenderEffect/Stretch"
                   android:exported="true">
diff --git a/tests/HwAccelerationTest/res/drawable-nodpi/scratches.png b/tests/HwAccelerationTest/res/drawable-nodpi/scratches.png
new file mode 100644
index 0000000..cc8adf1
--- /dev/null
+++ b/tests/HwAccelerationTest/res/drawable-nodpi/scratches.png
Binary files differ
diff --git a/tests/HwAccelerationTest/res/drawable-nodpi/weather_2.jpg b/tests/HwAccelerationTest/res/drawable-nodpi/weather_2.jpg
new file mode 100644
index 0000000..b5aff10
--- /dev/null
+++ b/tests/HwAccelerationTest/res/drawable-nodpi/weather_2.jpg
Binary files differ
diff --git a/tests/HwAccelerationTest/res/layout/view_runtime_shader.xml b/tests/HwAccelerationTest/res/layout/view_runtime_shader.xml
new file mode 100644
index 0000000..b91377d
--- /dev/null
+++ b/tests/HwAccelerationTest/res/layout/view_runtime_shader.xml
@@ -0,0 +1,205 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <LinearLayout
+        android:id="@+id/TopLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingBottom="8dp"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:text="Sample Card #1"/>
+
+        <androidx.cardview.widget.CardView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="8dp"
+            android:layout_marginBottom="16dp"
+            android:clickable="true"
+            android:focusable="true"
+            android:minHeight="148dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="16dp"
+                    android:paddingBottom="8dp">
+
+                    <ImageView
+                        android:layout_width="50dp"
+                        android:layout_height="50dp"
+                        android:layout_marginEnd="8dp"
+                        android:layout_marginRight="8dp"
+                        android:contentDescription="Logo"
+                        android:src="@drawable/icon"/>
+
+                    <LinearLayout
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1.0"
+                        android:layout_marginStart="8dp"
+                        android:layout_marginLeft="8dp"
+                        android:orientation="vertical">
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:textStyle="bold"
+                            android:text="Image Transition"/>
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:ellipsize="end"
+                            android:maxLines="1"
+                            android:text="Touch the image to trigger the animation"/>
+                    </LinearLayout>
+                </LinearLayout>
+
+                <com.android.test.hwui.BitmapTransitionView
+                    android:layout_width="match_parent"
+                    android:layout_height="194dp"
+                    android:padding="8dp"/>
+
+            </LinearLayout>
+
+        </androidx.cardview.widget.CardView>
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="16dp"
+            android:text="Sample Card #2"/>
+
+        <androidx.cardview.widget.CardView
+            android:id="@+id/CardView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="8dp"
+            android:layout_marginBottom="16dp"
+            android:clickable="true"
+            android:focusable="true"
+            android:minHeight="148dp">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="16dp"
+                android:orientation="vertical">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="16dp"
+                    android:paddingBottom="8dp">
+
+                    <ImageView
+                        android:layout_width="50dp"
+                        android:layout_height="50dp"
+                        android:layout_marginEnd="8dp"
+                        android:layout_marginRight="8dp"
+                        android:contentDescription="Logo"
+                        android:src="@drawable/icon"/>
+
+                    <LinearLayout
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1.0"
+                        android:layout_marginStart="8dp"
+                        android:layout_marginLeft="8dp"
+                        android:orientation="vertical">
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:textStyle="bold"
+                            android:text="View Group Manipulation"/>
+
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:layout_marginTop="8dp"
+                            android:ellipsize="end"
+                            android:maxLines="1"
+                            android:text="Tap the card to trigger the animation"/>
+                    </LinearLayout>
+                </LinearLayout>
+
+                <ImageView
+                    android:layout_width="match_parent"
+                    android:layout_height="194dp"
+                    android:background="@android:color/transparent"
+                    android:src="@drawable/weather_2"/>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:padding="16dp"
+                    android:paddingBottom="8dp"
+                    android:orientation="vertical">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content">
+                        <RatingBar
+                            android:layout_width="wrap_content"
+                            android:layout_height="wrap_content"
+                            android:contentDescription="Card Rating"
+                            android:isIndicator="true"
+                            android:numStars="5"
+                            android:rating="4.5"
+
+                            android:stepSize="0.5"/>
+                        <TextView
+                            android:layout_width="wrap_content"
+                            android:layout_height="match_parent"
+                            android:gravity="center_vertical"
+                            android:text="Category 4.5 Storm"/>
+                    </LinearLayout>
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginTop="8dp"
+                        android:maxLines="3"
+                        android:textIsSelectable="true"
+                        android:text="Lorem ipsum dolor sit amet, nec no nominavi scaevola. Per et
+                        sint sapientem, nobis perpetua salutandi mei te. Quo tamquam probatus
+                        reprehendunt in. Eos esse purto eruditi ea. Enim tation persius ut sea,
+                        eos ad consul populo. Ne eum solet altera. Cibo eligendi et est, electram
+                        theophrastus te vel eu."/>
+
+                </LinearLayout>
+
+            </LinearLayout>
+
+        </androidx.cardview.widget.CardView>
+    </LinearLayout>
+</ScrollView>
diff --git a/tests/HwAccelerationTest/res/values/styles.xml b/tests/HwAccelerationTest/res/values/styles.xml
index fa5437f..55f4dd69 100644
--- a/tests/HwAccelerationTest/res/values/styles.xml
+++ b/tests/HwAccelerationTest/res/values/styles.xml
@@ -41,4 +41,5 @@
         <item name="android:spotShadowAlpha">1</item>
         -->
     </style>
+
 </resources>
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapTransitionView.kt b/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapTransitionView.kt
new file mode 100644
index 0000000..d3ad9e8
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapTransitionView.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.android.test.hwui
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.ImageDecoder
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.RuntimeShader
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.view.View
+
+class BitmapTransitionView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+    private val mPaint = Paint()
+    private val mImageA = ImageDecoder.decodeBitmap(
+            ImageDecoder.createSource(context.resources, R.drawable.large_photo))
+    private val mImageB = ImageDecoder.decodeBitmap(
+            ImageDecoder.createSource(context.resources, R.drawable.very_large_photo))
+    private val mShaderA = BitmapShader(mImageA, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+    private val mShaderB = BitmapShader(mImageB, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+    private val mShader = RuntimeShader(AGSL, false)
+    private var mCurrentProgress = -1f
+    private var mForwardProgress = true
+    private var mCurrentAnimator = ValueAnimator.ofFloat(-1f, 1f)
+
+    init {
+        isClickable = true
+
+        mCurrentAnimator.duration = 1500
+        mCurrentAnimator.addUpdateListener { animation ->
+            mCurrentProgress = animation.animatedValue as Float
+            postInvalidate()
+        }
+    }
+
+    override fun performClick(): Boolean {
+        if (super.performClick()) return true
+
+        if (mCurrentAnimator.isRunning) {
+            mCurrentAnimator.reverse()
+            return true
+        }
+
+        if (mForwardProgress) {
+            mCurrentAnimator.setFloatValues(-1f, 1f)
+            mForwardProgress = false
+        } else {
+            mCurrentAnimator.setFloatValues(1f, -1f)
+            mForwardProgress = true
+        }
+
+        mCurrentAnimator.start()
+        postInvalidate()
+        return true
+    }
+
+    override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
+        val matrixA = Matrix()
+        val matrixB = Matrix()
+
+        matrixA.postScale(width.toFloat() / mImageA.width, height.toFloat() / mImageA.height)
+        matrixB.postScale(width.toFloat() / mImageB.width, height.toFloat() / mImageB.height)
+
+        mShaderA.setLocalMatrix(matrixA)
+        mShaderB.setLocalMatrix(matrixB)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        mShader.setInputShader("imageA", mShaderA)
+        mShader.setInputShader("imageB", mShaderB)
+        mShader.setIntUniform("imageDimensions", width, height)
+        mShader.setFloatUniform("progress", mCurrentProgress)
+
+        mPaint.shader = mShader
+        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mPaint)
+    }
+
+    private companion object {
+        const val AGSL = """
+        uniform shader imageA;
+        uniform shader imageB;
+        uniform ivec2 imageDimensions;
+        uniform float progress;
+
+        const vec2 iSize = vec2(48.0, 48.0);
+        const float iDir = 0.5;
+        const  float iRand = 0.81;
+
+        float hash12(vec2 p) {
+            vec3 p3  = fract(vec3(p.xyx) * .1031);
+            p3 += dot(p3, p3.yzx + 33.33);
+            return fract((p3.x + p3.y) * p3.z);
+        }
+
+        float ramp(float2 p) {
+          return mix(hash12(p),
+                     dot(p/vec2(imageDimensions), float2(iDir, 1 - iDir)),
+                     iRand);
+        }
+
+        half4 main(float2 p) {
+          float2 lowRes = p / iSize;
+          float2 cellCenter = (floor(lowRes) + 0.5) * iSize;
+          float2 posInCell = fract(lowRes) * 2 - 1;
+
+          float v = ramp(cellCenter) + progress;
+          float distToCenter = max(abs(posInCell.x), abs(posInCell.y));
+
+          return distToCenter > v ? imageA.eval(p).rgb1 : imageB.eval(p).rgb1;
+        }
+        """
+    }
+}
\ No newline at end of file
diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/RenderEffectViewActivity.kt b/tests/HwAccelerationTest/src/com/android/test/hwui/RenderEffectViewActivity.kt
new file mode 100644
index 0000000..06280d2
--- /dev/null
+++ b/tests/HwAccelerationTest/src/com/android/test/hwui/RenderEffectViewActivity.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.android.test.hwui
+
+import android.animation.ValueAnimator
+import android.app.Activity
+import android.graphics.Bitmap
+import android.graphics.BitmapShader
+import android.graphics.ImageDecoder
+import android.graphics.Matrix
+import android.graphics.Shader
+import android.graphics.RenderEffect
+import android.graphics.RuntimeShader
+import android.os.Bundle
+import android.view.View
+
+class RenderEffectViewActivity : Activity() {
+
+    private val mDropsShader = RuntimeShader(dropsAGSL, false)
+    private var mDropsAnimator = ValueAnimator.ofFloat(0f, 1f)
+    private var mStartTime = System.currentTimeMillis()
+    private lateinit var mScratchesImage: Bitmap
+    private lateinit var mScratchesShader: BitmapShader
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.view_runtime_shader)
+
+        val dropsView = findViewById<View>(R.id.CardView)!!
+        dropsView.isClickable = true
+        dropsView.setOnClickListener {
+            if (mDropsAnimator.isRunning) {
+                mDropsAnimator.cancel()
+                dropsView.setRenderEffect(null)
+            } else {
+                mDropsAnimator.start()
+            }
+        }
+
+        val imgSource = ImageDecoder.createSource(resources, R.drawable.scratches)
+        mScratchesImage = ImageDecoder.decodeBitmap(imgSource)
+        mScratchesShader = BitmapShader(mScratchesImage,
+                                        Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+
+        mDropsAnimator.duration = 1000
+        mDropsAnimator.repeatCount = ValueAnimator.INFINITE
+        mDropsAnimator.addUpdateListener { _ ->
+            val viewWidth = dropsView.width.toFloat()
+            val viewHeight = dropsView.height.toFloat()
+            val scratchesMatrix = Matrix()
+            scratchesMatrix.postScale(viewWidth / mScratchesImage.width,
+                    viewHeight / mScratchesImage.height)
+            mScratchesShader.setLocalMatrix(scratchesMatrix)
+
+            mDropsShader.setInputShader("scratches", mScratchesShader)
+            mDropsShader.setFloatUniform("elapsedSeconds",
+                                    (System.currentTimeMillis() - mStartTime) / 1000f)
+            mDropsShader.setFloatUniform("viewDimensions", viewWidth, viewHeight)
+
+            val dropsEffect = RenderEffect.createRuntimeShaderEffect(mDropsShader, "background")
+            val blurEffect = RenderEffect.createBlurEffect(10f, 10f, Shader.TileMode.CLAMP)
+
+            dropsView.setRenderEffect(RenderEffect.createChainEffect(dropsEffect, blurEffect))
+        }
+    }
+
+    private companion object {
+        const val dropsAGSL = """
+            uniform float elapsedSeconds;
+            uniform vec2 viewDimensions;
+            uniform shader background;
+            uniform shader scratches;
+
+            vec2 dropsUV(vec2 fragCoord ) {
+                vec2 uv = fragCoord.xy / viewDimensions.xy; // 0 <> 1
+                vec2 offs = vec2(0.);
+                return (offs + uv).xy;
+            }
+
+            const vec3  iFrostColorRGB = vec3(0.5, 0.5, 0.5);
+            const float iFrostColorAlpha = .3;
+
+            half4 main(float2 fragCoord) {
+                half4 bg = background.eval(dropsUV(fragCoord)*viewDimensions.xy);
+                float2 scratchCoord = fragCoord.xy / viewDimensions.xy;;
+                scratchCoord += 1.5;
+                scratchCoord = mod(scratchCoord, 1);
+                half scratch = scratches.eval(scratchCoord*viewDimensions.xy).r;
+                bg.rgb = mix(bg.rgb, iFrostColorRGB, iFrostColorAlpha);
+                bg.rgb = mix(bg.rgb, half3(1), pow(scratch,3));
+                return bg;
+            }
+        """
+    }
+}
\ No newline at end of file