Skip to content

Commit

Permalink
Add runtime shader for progressive blur (#368)
Browse files Browse the repository at this point in the history
This drastically improves performance of progressive on platforms which
support a runtime shader (Android SDK 33+ and all Skiko platforms).

On Android, the difference between the old 'multiple GraphicsLayer'
implementation, and a single runtime shader is around 2x faster.
  • Loading branch information
chrisbanes authored Oct 21, 2024
1 parent f713b1a commit 5197c26
Show file tree
Hide file tree
Showing 31 changed files with 598 additions and 335 deletions.
25 changes: 9 additions & 16 deletions haze/api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,39 +108,32 @@ package dev.chrisbanes.haze {
}

public sealed interface HazeProgressive {
method public int getSteps();
property public abstract int steps;
field public static final dev.chrisbanes.haze.HazeProgressive.Companion Companion;
field public static final int STEPS_AUTO_BALANCED = -1; // 0xffffffff
}

public static final class HazeProgressive.Companion {
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient horizontalGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional float startX, optional float startIntensity, optional float endX, optional float endIntensity);
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient verticalGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional float startY, optional float startIntensity, optional float endY, optional float endIntensity);
field public static final int STEPS_AUTO_BALANCED = -1; // 0xffffffff
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient horizontalGradient(optional androidx.compose.animation.core.Easing easing, optional float startX, optional float startIntensity, optional float endX, optional float endIntensity);
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient verticalGradient(optional androidx.compose.animation.core.Easing easing, optional float startY, optional float startIntensity, optional float endY, optional float endIntensity);
}

public static final class HazeProgressive.LinearGradient implements dev.chrisbanes.haze.HazeProgressive {
ctor public HazeProgressive.LinearGradient(optional int steps, optional androidx.compose.animation.core.Easing easing, optional long start, optional float startIntensity, optional long end, optional float endIntensity);
method public int component1();
method public androidx.compose.animation.core.Easing component2();
method public long component3-F1C5BW0();
method public float component4();
method public long component5-F1C5BW0();
method public float component6();
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient copy-owaVgss(int steps, androidx.compose.animation.core.Easing easing, long start, float startIntensity, long end, float endIntensity);
ctor public HazeProgressive.LinearGradient(optional androidx.compose.animation.core.Easing easing, optional long start, optional float startIntensity, optional long end, optional float endIntensity);
method public androidx.compose.animation.core.Easing component1();
method public long component2-F1C5BW0();
method public float component3();
method public long component4-F1C5BW0();
method public float component5();
method public dev.chrisbanes.haze.HazeProgressive.LinearGradient copy-umk_asQ(androidx.compose.animation.core.Easing easing, long start, float startIntensity, long end, float endIntensity);
method public androidx.compose.animation.core.Easing getEasing();
method public long getEnd();
method public float getEndIntensity();
method public long getStart();
method public float getStartIntensity();
method public int getSteps();
property public final androidx.compose.animation.core.Easing easing;
property public final long end;
property public final float endIntensity;
property public final long start;
property public final float startIntensity;
property public int steps;
}

@androidx.compose.runtime.Stable public final class HazeState {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2024, Christopher Banes and the Haze project contributors
// SPDX-License-Identifier: Apache-2.0

package dev.chrisbanes.haze

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.platform.LocalGraphicsContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.takeOrElse
import dev.chrisbanes.haze.HazeChildNode.Companion.TAG
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min

@RequiresApi(31)
internal actual fun HazeChildNode.drawLinearGradientProgressiveEffect(
drawScope: DrawScope,
progressive: HazeProgressive.LinearGradient,
contentLayer: GraphicsLayer,
) {
if (Build.VERSION.SDK_INT >= 33) {
with(drawScope) {
contentLayer.renderEffect = createRenderEffect(
blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx(),
noiseFactor = resolveNoiseFactor(),
tints = resolveTints(),
contentSize = size,
contentOffset = contentOffset,
layerSize = layerSize,
mask = mask,
progressive = progressive.asBrush(),
)
contentLayer.alpha = alpha

// Finally draw the layer
drawLayer(contentLayer)
}
} else {
drawLinearGradientProgressiveEffectUsingLayers(
drawScope = drawScope,
progressive = progressive,
contentLayer = contentLayer,
)
}
}

private fun HazeChildNode.drawLinearGradientProgressiveEffectUsingLayers(
drawScope: DrawScope,
progressive: HazeProgressive.LinearGradient,
contentLayer: GraphicsLayer,
) = with(drawScope) {
require(progressive.startIntensity in 0f..1f)
require(progressive.endIntensity in 0f..1f)

// Here we're going to calculate an appropriate amount of steps for the length.
// We use a calculation of 60dp per step, which is a good balance between
// quality vs performance
val stepHeightPx = with(drawContext.density) { 60.dp.toPx() }
val length = calculateLength(progressive.start, progressive.end, size)
val steps = ceil(length / stepHeightPx).toInt().coerceAtLeast(2)

val graphicsContext = currentValueOf(LocalGraphicsContext)

val seq = when {
progressive.endIntensity >= progressive.startIntensity -> 0..steps
else -> steps downTo 0
}

val tints = resolveTints()
val noiseFactor = resolveNoiseFactor()
val blurRadiusPx = resolveBlurRadius().takeOrElse { 0.dp }.toPx()

for (i in seq) {
val fraction = i / steps.toFloat()
val intensity = lerp(
progressive.startIntensity,
progressive.endIntensity,
progressive.easing.transform(fraction),
)

val layer = graphicsContext.createGraphicsLayer()
layer.record(contentLayer.size) {
drawLayer(contentLayer)
}

log(TAG) {
"drawProgressiveEffect. " +
"step=$i, " +
"fraction=$fraction, " +
"intensity=$intensity"
}

val min = min(progressive.startIntensity, progressive.endIntensity)
val max = max(progressive.startIntensity, progressive.endIntensity)

layer.renderEffect = createRenderEffect(
blurRadiusPx = intensity * blurRadiusPx,
noiseFactor = noiseFactor,
tints = tints,
tintAlphaModulate = intensity,
contentSize = size,
contentOffset = contentOffset,
layerSize = layerSize,
mask = Brush.linearGradient(
lerp(min, max, (i - 2f) / steps) to Color.Transparent,
lerp(min, max, (i - 1f) / steps) to Color.Black,
lerp(min, max, (i + 0f) / steps) to Color.Black,
lerp(min, max, (i + 1f) / steps) to Color.Transparent,
start = progressive.start,
end = progressive.end,
),
)
layer.alpha = alpha

// Since we included a border around the content, we need to translate so that
// we don't see it (but it still affects the RenderEffect)
drawLayer(layer)

graphicsContext.releaseGraphicsLayer(layer)
}
}
Loading

0 comments on commit 5197c26

Please sign in to comment.