Skip to content

Commit

Permalink
Cache RenderEffects (#402)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes authored Nov 10, 2024
1 parent 1594535 commit a5f34f7
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 80 deletions.
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ androidx-benchmark = "1.3.3"
androidx-media3 = "1.4.1"
androidx-test-ext-junit = "1.2.1"
assertk = "0.28.1"
cache4k = "0.13.0"
coil = "3.0.0"
compose-multiplatform = "1.7.0"
kotlinx-coroutines-swing = "1.9.0"
Expand Down Expand Up @@ -35,7 +36,6 @@ mavenpublish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
[libraries]
androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" }
androidx-core = "androidx.core:core-ktx:1.13.1"
androidx-collection = "androidx.collection:collection:1.4.5"
androidx-activity-compose = "androidx.activity:activity-compose:1.9.3"
androidx-compose-ui-test-manifest = "androidx.compose.ui:ui-test-manifest:1.7.5"
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" }
Expand All @@ -49,6 +49,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation

assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" }

cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }

Expand Down
3 changes: 1 addition & 2 deletions haze/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ kotlin {
dependencies {
api(compose.ui)
implementation(compose.foundation)
implementation(libs.cache4k)
}
}

Expand All @@ -48,8 +49,6 @@ kotlin {
// Can remove this once CMP goes stable
api(libs.androidx.compose.ui)
implementation(libs.androidx.compose.foundation)

implementation(libs.androidx.collection)
implementation(libs.androidx.core)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect(
) {
if (Build.VERSION.SDK_INT >= 33) {
with(drawScope) {
contentLayer.renderEffect = createRenderEffect(
contentLayer.renderEffect = getOrCreateRenderEffect(
blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(),
noiseFactor = resolveNoiseFactor(),
tints = resolveTints(),
Expand All @@ -44,7 +44,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect(
}
} else if (progressive.preferPerformance) {
with(drawScope) {
contentLayer.renderEffect = createRenderEffect(
contentLayer.renderEffect = getOrCreateRenderEffect(
blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(),
noiseFactor = resolveNoiseFactor(),
tints = resolveTints(),
Expand Down Expand Up @@ -116,7 +116,7 @@ private fun HazeChildNode.drawLinearGradientProgressiveEffectUsingLayers(
val min = min(progressive.startIntensity, progressive.endIntensity)
val max = max(progressive.startIntensity, progressive.endIntensity)

layer.renderEffect = createRenderEffect(
layer.renderEffect = getOrCreateRenderEffect(
blurRadiusPx = intensity * blurRadiusPx,
noiseFactor = noiseFactor,
tints = tints,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package dev.chrisbanes.haze

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapShader
Expand All @@ -15,7 +16,6 @@ import android.graphics.Shader
import android.graphics.Shader.TileMode.REPEAT
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.collection.lruCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
Expand All @@ -30,54 +30,39 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalContext
import io.github.reactivecircus.cache4k.Cache
import kotlin.concurrent.getOrSet
import kotlin.math.roundToInt

internal actual fun HazeChildNode.createRenderEffect(
blurRadiusPx: Float,
noiseFactor: Float,
tints: List<HazeTint>,
tintAlphaModulate: Float,
contentSize: Size,
contentOffset: Offset,
layerSize: Size,
mask: Brush?,
progressive: Brush?,
): RenderEffect? {
internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect? {
if (Build.VERSION.SDK_INT < 31) return null

log(HazeChildNode.TAG) {
"createRenderEffect. " +
"blurRadiusPx=$blurRadiusPx, " +
"noiseFactor=$noiseFactor, " +
"tints=$tints, " +
"contentSize=$contentSize, " +
"contentOffset=$contentOffset, " +
"layerSize=$layerSize"
}
require(params.blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" }

require(blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" }

val progressiveShader = progressive?.toShader(contentSize)
val progressiveShader = params.progressive?.toShader(params.contentSize)

val blur = when {
blurRadiusPx >= 0.005f && Build.VERSION.SDK_INT >= 33 && progressiveShader != null -> {
params.blurRadiusPx >= 0.005f && Build.VERSION.SDK_INT >= 33 && progressiveShader != null -> {
// If we've been provided with a progressive/gradient blur shader, we need to use
// our custom blur via a runtime shader
createBlurImageFilterWithMask(
blurRadiusPx = blurRadiusPx,
bounds = Rect(contentOffset, contentSize),
blurRadiusPx = params.blurRadiusPx,
bounds = Rect(params.contentOffset, params.contentSize),
mask = progressiveShader,
)
}

blurRadiusPx >= 0.005f -> {
params.blurRadiusPx >= 0.005f -> {
try {
AndroidRenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, Shader.TileMode.CLAMP)
AndroidRenderEffect.createBlurEffect(
params.blurRadiusPx,
params.blurRadiusPx,
Shader.TileMode.CLAMP,
)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException(
"Error whilst calling RenderEffect.createBlurEffect. " +
"This is likely because this device does not support a blur radius of $blurRadiusPx px",
"This is likely because this device does not support a blur radius of ${params.blurRadiusPx} px",
e,
)
}
Expand All @@ -89,38 +74,42 @@ internal actual fun HazeChildNode.createRenderEffect(
}

return blur
.withNoise(noiseFactor)
.withTints(tints, tintAlphaModulate, progressiveShader, contentOffset)
.withMask(mask, contentSize, contentOffset)
.withNoise(currentValueOf(LocalContext), params.noiseFactor)
.withTints(params.tints, params.tintAlphaModulate, progressiveShader, params.contentOffset)
.withMask(params.mask, params.contentSize, params.contentOffset)
.asComposeRenderEffect()
}

internal actual fun DrawScope.useGraphicLayers(): Boolean {
return Build.VERSION.SDK_INT >= 32 && drawContext.canvas.nativeCanvas.isHardwareAccelerated
}

private val noiseTextureCache = lruCache<Int, Bitmap>(3)
private val noiseTextureCache by lazy {
Cache.Builder<Int, Bitmap>()
.maximumCacheSize(3)
.build()
}

context(CompositionLocalConsumerModifierNode)
private fun getNoiseTexture(noiseFactor: Float): Bitmap {
private fun Context.getNoiseTexture(noiseFactor: Float): Bitmap {
val noiseAlphaInt = (noiseFactor * 255).roundToInt().coerceIn(0, 255)
val cached = noiseTextureCache[noiseAlphaInt]
val cached = noiseTextureCache.get(noiseAlphaInt)
if (cached != null && !cached.isRecycled) {
return cached
}

// We draw the noise with the given opacity
val resources = currentValueOf(LocalContext).resources
return BitmapFactory.decodeResource(resources, R.drawable.haze_noise)
.withAlpha(noiseAlphaInt)
.also { noiseTextureCache.put(noiseAlphaInt, it) }
}

context(CompositionLocalConsumerModifierNode)
@RequiresApi(31)
private fun AndroidRenderEffect.withNoise(noiseFactor: Float): AndroidRenderEffect = when {
private fun AndroidRenderEffect.withNoise(
context: Context,
noiseFactor: Float,
): AndroidRenderEffect = when {
noiseFactor >= 0.005f -> {
val noiseShader = BitmapShader(getNoiseTexture(noiseFactor), REPEAT, REPEAT)
val noiseShader = BitmapShader(context.getNoiseTexture(noiseFactor), REPEAT, REPEAT)
AndroidRenderEffect.createBlendModeEffect(
AndroidRenderEffect.createShaderEffect(noiseShader), // dst
this, // src
Expand Down
52 changes: 49 additions & 3 deletions haze/src/commonMain/kotlin/dev/chrisbanes/haze/HazeChildNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.unit.takeOrElse
import androidx.compose.ui.unit.toSize
import io.github.reactivecircus.cache4k.Cache

/**
* The [Modifier.Node] implementation used by [Modifier.hazeChild].
Expand Down Expand Up @@ -358,7 +359,7 @@ class HazeChildNode(

private fun DrawScope.updateRenderEffectIfDirty() {
if (renderEffectDirty) {
renderEffect = createRenderEffect(
renderEffect = getOrCreateRenderEffect(
blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(),
noiseFactor = resolveNoiseFactor(),
tints = resolveTints(),
Expand Down Expand Up @@ -495,7 +496,25 @@ sealed interface HazeProgressive {
}
}

internal expect fun HazeChildNode.createRenderEffect(
private val renderEffectCache by lazy {
Cache.Builder<RenderEffectParams, RenderEffect>()
.maximumCacheSize(10)
.build()
}

internal data class RenderEffectParams(
val blurRadiusPx: Float,
val noiseFactor: Float,
val tints: List<HazeTint> = emptyList(),
val tintAlphaModulate: Float = 1f,
val contentSize: Size,
val contentOffset: Offset,
val layerSize: Size,
val mask: Brush? = null,
val progressive: Brush? = null,
)

internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect(
blurRadiusPx: Float,
noiseFactor: Float,
tints: List<HazeTint> = emptyList(),
Expand All @@ -505,7 +524,34 @@ internal expect fun HazeChildNode.createRenderEffect(
layerSize: Size,
mask: Brush? = null,
progressive: Brush? = null,
): RenderEffect?
): RenderEffect? = getOrCreateRenderEffect(
RenderEffectParams(
blurRadiusPx = blurRadiusPx,
noiseFactor = noiseFactor,
tints = tints,
tintAlphaModulate = tintAlphaModulate,
contentSize = contentSize,
contentOffset = contentOffset,
layerSize = layerSize,
mask = mask,
progressive = progressive,
),
)

internal fun CompositionLocalConsumerModifierNode.getOrCreateRenderEffect(params: RenderEffectParams): RenderEffect? {
log(HazeChildNode.TAG) { "getOrCreateRenderEffect: $params" }
val cached = renderEffectCache.get(params)
if (cached != null) {
log(HazeChildNode.TAG) { "getOrCreateRenderEffect. Returning cached: $params" }
return cached
}

log(HazeChildNode.TAG) { "getOrCreateRenderEffect. Creating: $params" }
return createRenderEffect(params)
?.also { renderEffectCache.put(params, it) }
}

internal expect fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect?

internal expect fun HazeChildNode.drawLinearGradientProgressiveEffect(
drawScope: DrawScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect(
progressive: HazeProgressive.LinearGradient,
contentLayer: GraphicsLayer,
) = with(drawScope) {
contentLayer.renderEffect = createRenderEffect(
contentLayer.renderEffect = getOrCreateRenderEffect(
blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(),
noiseFactor = resolveNoiseFactor(),
tints = resolveTints(),
Expand Down
39 changes: 10 additions & 29 deletions haze/src/skikoMain/kotlin/dev/chrisbanes/haze/RenderEffect.skiko.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import org.jetbrains.skia.BlendMode
import org.jetbrains.skia.ColorFilter
import org.jetbrains.skia.FilterTileMode
Expand All @@ -25,45 +26,25 @@ import org.jetbrains.skia.Shader

internal actual fun DrawScope.useGraphicLayers(): Boolean = true

internal actual fun HazeChildNode.createRenderEffect(
blurRadiusPx: Float,
noiseFactor: Float,
tints: List<HazeTint>,
tintAlphaModulate: Float,
contentSize: Size,
contentOffset: Offset,
layerSize: Size,
mask: Brush?,
progressive: Brush?,
): RenderEffect? {
log(HazeChildNode.TAG) {
"createRenderEffect. " +
"blurRadiusPx=$blurRadiusPx, " +
"noiseFactor=$noiseFactor, " +
"tints=$tints, " +
"contentSize=$contentSize, " +
"contentOffset=$contentOffset, " +
"layerSize=$layerSize"
}

require(blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" }
internal actual fun CompositionLocalConsumerModifierNode.createRenderEffect(params: RenderEffectParams): RenderEffect? {
require(params.blurRadiusPx >= 0f) { "blurRadius needs to be equal or greater than 0f" }

val compositeShaderBuilder = RuntimeShaderBuilder(RUNTIME_SHADER).apply {
uniform("noiseFactor", noiseFactor.coerceIn(0f, 1f))
uniform("noiseFactor", params.noiseFactor.coerceIn(0f, 1f))
child("noise", NOISE_SHADER)
}

val progressiveShader = progressive?.toShader(contentSize)
val progressiveShader = params.progressive?.toShader(params.contentSize)
val blur = if (progressiveShader != null) {
// If we've been provided with a progressive/gradient blur shader, we need to use
// our custom blur via a runtime shader
createBlurImageFilterWithMask(
blurRadiusPx = blurRadiusPx,
bounds = Rect(contentOffset, contentSize),
blurRadiusPx = params.blurRadiusPx,
bounds = Rect(params.contentOffset, params.contentSize),
mask = progressiveShader,
)
} else {
createBlurImageFilter(blurRadiusPx = blurRadiusPx, bounds = layerSize.toRect())
createBlurImageFilter(blurRadiusPx = params.blurRadiusPx, bounds = params.layerSize.toRect())
}

return ImageFilter
Expand All @@ -72,8 +53,8 @@ internal actual fun HazeChildNode.createRenderEffect(
shaderNames = arrayOf("content", "blur"),
inputs = arrayOf(null, blur),
)
.withTints(tints, tintAlphaModulate, progressiveShader, contentOffset)
.withMask(mask, contentSize, contentOffset)
.withTints(params.tints, params.tintAlphaModulate, progressiveShader, params.contentOffset)
.withMask(params.mask, params.contentSize, params.contentOffset)
.asComposeRenderEffect()
}

Expand Down

0 comments on commit a5f34f7

Please sign in to comment.