Repository navigation
ImageFilter.blur breaks ImageFilter.shader in a BackdropFilter together #170820
Copy link
Copy link
Closed
Closed
Copy link
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.32Found to occur in 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33Found to occur in 3.33has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Description
Steps to reproduce
When trying to find a workaround for #170792 I stumbled on another roadblock.
In an Impeller build, do the following:
- create a
BackdropFilterLayerwith its filter set toImageFilter.combined, with inner being a blur and outer being a shader - When you change
sigmaXandsigmaY, the size of the texture that's passed to the shader seems to change, but not in a uniform way
Check the code sample for a reliable repro.
Expected results
The red circle stays in the same position with the same size the entire time.
Actual results
See video
Code sample
Code sample
// main.dart
import 'dart:ui';
import 'package:flutter/material.dart';
void main() async {
final shader = await FragmentProgram.fromAsset('shaders/circle.frag');
runApp(MainApp(shader: shader));
}
class MainApp extends StatefulWidget {
const MainApp({super.key, required this.shader});
final FragmentProgram shader;
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
late final animationController = AnimationController(
vsync: this,
lowerBound: 0,
upperBound: 10,
duration: Duration(seconds: 2),
)..repeat(reverse: true);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ListenableBuilder(
listenable: animationController,
builder: (context, child) {
return Scaffold(
body: Stack(
children: [
const Center(child: FlutterLogo(size: 100)),
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return BackdropFilter(
filter: ImageFilter.compose(
inner: ImageFilter.blur(
sigmaX: animationController.value,
sigmaY: animationController.value,
tileMode: TileMode.clamp,
),
outer: ImageFilter.shader(
widget.shader.fragmentShader()
..setFloat(2, 100) // uCirclePos.x (center)
..setFloat(3, 100) // uCirclePos.y (center)
..setFloat(4, 100.0), // uCircleRadius
),
),
child: Center(
child: Text(
animationController.value.toStringAsFixed(2),
),
),
);
},
),
),
],
),
);
},
),
);
}
}
With this fragment shader:
#version 320 es
precision mediump float;
uniform vec2 uResolution;
uniform vec2 uCirclePos;
uniform float uCircleRadius;
uniform sampler2D uTexture;
out vec4 fragColor;
#include <flutter/runtime_effect.glsl>
float sdfCircle(vec2 p, float radius) {
return length(p) - radius;
}
void main() {
vec2 fragCoord = FlutterFragCoord().xy;
vec2 screenUV = fragCoord / uResolution;
vec2 normalizedCoord = (fragCoord - uResolution * 0.5) / min(uResolution.x, uResolution.y);
vec2 normalizedCirclePos = (uCirclePos - uResolution * 0.5) / min(uResolution.x, uResolution.y);
float normalizedRadius = uCircleRadius / min(uResolution.x, uResolution.y);
float distance = sdfCircle(normalizedCoord - normalizedCirclePos, normalizedRadius);
float circleAlpha = 1.0 - smoothstep(-0.01, 0.01, distance);
vec4 backgroundColor = texture(uTexture, screenUV);
vec4 circleColor = vec4(1.0, 0.0, 0.0, circleAlpha);
fragColor = mix(backgroundColor, circleColor, circleAlpha * 0.8);
}Screenshots or Video
Screenshots / Video demonstration
CleanShot.2025-06-18.at.19.51.11.mp4
Logs
No response
Flutter Doctor output
Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.101.0)
[✓] Connected device (2 available)
! Error: Browsing on the local area network for iPhone von mir. Ensure the device is
unlocked and attached with a cable or associated with the same local area network
as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
! Error: Browsing on the local area network for Tim’s iPad. Ensure the device is
unlocked and attached with a cable or associated with the same local area network
as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resourcesReactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work liste: impellerImpeller rendering backend issues and features requestsImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.flutter/engine related. See also e: labels.found in release: 3.32Found to occur in 3.32Found to occur in 3.32found in release: 3.33Found to occur in 3.33Found to occur in 3.33has reproducible stepsThe issue has been confirmed reproducible and is ready to work onThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Activity
dkwingsmt commentedon Jun 18, 2025
cc @jonahwilliams
jonahwilliams commentedon Jun 18, 2025
Probably what is happening is that we're dropping and/or not accounting for the scaling factor from the backdrop blur.
timcreatedit commentedon Jun 18, 2025
Hi @jonahwilliams, thanks for the swift response! Could you perhaps point me to where that scaling factor is calculated, so I can work around the issue until it is fixed?
darshankawar commentedon Jun 19, 2025
Thanks for the report. Seeing the same behavior with latest sdk versions.
sangddn commentedon Jun 20, 2025
I found this temporary workaround which looks fine to me. You can push 2 separate layers instead of using
ImageFilter.compose:Screen.Recording.2025-06-20.at.11.18.21.AM.mov
timcreatedit commentedon Jun 20, 2025
Thanks @sangddn, that's a smart solution but it's functionally not quite the same. The crucial thing I'm looking for is for my shader to be able to
discardpixels from the blur based on a mask. Your workaround works for the repro, but it also leads to the blur always being painted, no matter what the shader does after.arihant23 commentedon Jun 23, 2025
jonahwilliams commentedon Jun 25, 2025
I don't have it tested yet, and I confirm this is different than #170973
If the snapshot transform has a translation, we need to render to texture first. note that this diff also scales back up but that is unnecessary.
gaaclarke commentedon Oct 27, 2025
So, the reason this happens is because the blur will return different sized images depending on the sigma. When you blur something a lot there is no reason to perform the blur in a high resolution because the result is blurry. So for performance reasons we do the blur in lower resolutions. Thus the normalization math are getting different results.
I attempted a workaround by sending the uniforms for the circle as normalized already. This doesn't really work because as the sigma is increased the circle will drift away from the origin because there is also an offset baked into those textures for the blur radius. The animation also gets a bit chunky presumably because of the lower resolution.
I think jonah's estimation may be correct that we should snapshot in this case. In his comment though he was talking about snapshotting if the transform is not identity. We've already shown that we can render it correctly without snapshotting when the transform is just a translation since that was the thing we fixed in #177510. I think in this case a better heuristic may be something like "if the coverage of the input to inner doesn't match the coverage of the output to inner, do a snapshot"? Or maybe "does the inner output transform have a non-translation transform"?
Alternatives considered:
gaaclarke commentedon Oct 27, 2025
Side note: I don't think what the op is attempting here is going to be portable because of high dpi screens. They'll probably have to query the
devicePixelRatio. edit: or send in the coordinates as normalized coordinates?timcreatedit commentedon Oct 28, 2025
That's usually what I do, but it was besides the point here, this example is really just to show that the coordinate system changes where it shouldn't. Not anything real that I'm working on :)
Fixes RuntimeEffect when used with ImageFilter.compose and gaussian b…
github-actions commentedon Nov 14, 2025
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.Fixes RuntimeEffect when used with ImageFilter.compose and gaussian b…
Fixes RuntimeEffect when used with ImageFilter.compose and gaussian b…