Repository navigation
UI glitches when Impeller is enabled on some Android devices. #171772
Copy link
Copy link
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: device-specificOnly manifests on certain devicesOnly manifests on certain devicese: 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.platform-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionslimpellerEngine binary size reduction. go/slimpellerEngine binary size reduction. go/slimpellerteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Description
Steps to reproduce
- Launch the app with Impeller rendering enabled (reproduced on Samsung S25 Ultra)
- On the main screen, scroll up and down repeatedly
- Open the second page
Expected results
Absence of visual glitches, matching the behaviour observed with Impeller disabled
Actual results
UI glitches occur while scrolling or overscrolling, and also when opening the second screen
Code sample
Code sample
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Impeller Glitch Reproduction',
theme: ThemeData(primarySwatch: Colors.blue),
home: const ChatPage(),
);
}
}
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final List<MessageData> messages = [];
final ScrollController _scrollController = ScrollController();
final Set<String> _placedItems = {};
@override
void initState() {
super.initState();
for (var i = 0; i < 50; i++) {
_addMessage();
}
}
void _addMessage() {
final newMessage = MessageData(
id: 'msg_${messages.length}',
text: 'Message ${messages.length + 1}',
isFromUser: messages.length % 2 == 0,
);
setState(() {
messages.insert(0, newMessage);
});
}
void _onItemPlaced(String itemId) {
setState(() {
_placedItems.add(itemId);
});
}
void _navigateToBlurScreen() {
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const BlurEffectsScreen(),
transitionDuration: const Duration(milliseconds: 800),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return Stack(
children: [
BackdropFilter(
filter: ImageFilter.blur(
sigmaX: animation.value * 15,
sigmaY: animation.value * 15,
),
child: Container(color: Colors.black.withValues(alpha: animation.value * 0.3)),
),
SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: FadeTransition(opacity: animation, child: child),
),
],
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey,
appBar: AppBar(
title: const Text('Impeller Glitch Test'),
actions: [IconButton(icon: const Icon(Icons.blur_on), onPressed: _navigateToBlurScreen)],
),
body: Stack(
children: [
// Essential shader mask with gradient
Padding(
padding: const EdgeInsets.only(top: 48, bottom: 60),
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black,
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.2),
Colors.black.withValues(alpha: 0),
],
stops: const [0, 0.5, 0.65, 0.9],
).createShader(Rect.fromLTWH(0, 0, bounds.width, 130)),
blendMode: BlendMode.dstOut,
child: ShaderMask(
shaderCallback: (bounds) => LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black,
Colors.black.withValues(alpha: 0.6),
Colors.black.withValues(alpha: 0.2),
Colors.black.withValues(alpha: 0),
],
stops: const [0, 0.5, 0.65, 0.9],
).createShader(Rect.fromLTRB(0, bounds.height - 24, bounds.width, bounds.height)),
blendMode: BlendMode.dstOut,
child: ListView.builder(
controller: _scrollController,
reverse: true,
padding: const EdgeInsets.only(top: 154, bottom: 60),
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[index];
final isPlaced = _placedItems.contains(message.id);
final isLast = index == 0;
if (!isPlaced && mounted) {
Future.delayed(Duration(milliseconds: 100 + (index * 50)), () {
if (mounted) _onItemPlaced(message.id);
});
}
return MessageBubble(
key: ValueKey(message.id),
message: message,
maxWidth: MediaQuery.of(context).size.width * 0.75,
placed: isPlaced,
last: isLast,
);
},
),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addMessage,
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
class BlurEffectsScreen extends StatefulWidget {
const BlurEffectsScreen({super.key});
@override
State<BlurEffectsScreen> createState() => _BlurEffectsScreenState();
}
class _BlurEffectsScreenState extends State<BlurEffectsScreen> with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: const Duration(seconds: 2), vsync: this)..repeat();
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Essential backdrop filter that triggers glitches
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 15 + (_animation.value * 10),
sigmaY: 15 + (_animation.value * 10),
),
child: Container(color: Colors.white.withValues(alpha: 0.1)),
);
},
),
Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.auto_awesome, color: Colors.white, size: 60),
const SizedBox(height: 20),
const Text(
'Blur Effects Screen',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back to Chat'),
),
],
),
);
},
),
),
Positioned(
top: MediaQuery.of(context).padding.top + 10,
right: 16,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.of(context).pop(),
),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class MessageData {
final String id;
final String text;
final bool isFromUser;
MessageData({required this.id, required this.text, required this.isFromUser});
}
class MessageBubble extends StatefulWidget {
const MessageBubble({
required this.message,
required this.maxWidth,
required this.placed,
required this.last,
super.key,
});
final MessageData message;
final double maxWidth;
final bool placed;
final bool last;
@override
State<MessageBubble> createState() => _MessageBubbleState();
}
class _MessageBubbleState extends State<MessageBubble> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
lowerBound: widget.placed ? 1 : 0.000001,
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = _animationController.drive(CurveTween(curve: Curves.linearToEaseOut));
Future.delayed(const Duration(milliseconds: 100)).whenComplete(() {
if (mounted) {
if (!widget.placed) {
_animationController.forward();
} else {
_animationController.value = 1;
}
}
});
}
@override
Widget build(BuildContext context) {
final alignment = widget.message.isFromUser ? Alignment.centerRight : Alignment.centerLeft;
final color = widget.message.isFromUser
? Colors.blue.withValues(alpha: 0.9)
: Colors.grey.shade200;
return Align(
alignment: alignment,
child: AnimatedBuilder(
animation: _animation,
builder: (context, _) {
return Padding(
padding: EdgeInsets.only(top: 5, bottom: widget.last ? 20 * _animation.value : 0),
child: FittedBox(
fit: BoxFit.scaleDown,
child: SizeTransition(
sizeFactor: _animation,
child: ScaleTransition(
scale: _animation,
alignment: alignment,
child: FadeTransition(
opacity: _animation,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5),
child: _BubbleContent(
message: widget.message,
color: color,
maxWidth: widget.maxWidth,
),
),
),
),
),
),
);
},
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}
class _BubbleContent extends StatelessWidget {
const _BubbleContent({required this.message, required this.color, required this.maxWidth});
final MessageData message;
final Color color;
final double maxWidth;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
right: message.isFromUser ? 15 : 10,
left: message.isFromUser ? 14 : 15,
),
child: ClipPath(
clipper: BubbleClipper(fromRobot: !message.isFromUser),
child: DecoratedBox(
decoration: BoxDecoration(color: color),
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth * 0.8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text(
message.text,
style: TextStyle(color: message.isFromUser ? Colors.white : Colors.black),
),
),
),
),
);
}
}
class BubbleClipper extends CustomClipper<Path> {
const BubbleClipper({required this.fromRobot});
final bool fromRobot;
@override
Path getClip(Size size) {
final path = Path();
const radius = 20.0;
const tailSize = 8.0;
if (fromRobot) {
path.addRRect(
RRect.fromLTRBAndCorners(
tailSize,
0,
size.width,
size.height,
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: const Radius.circular(radius),
bottomRight: const Radius.circular(radius),
),
);
path.moveTo(0, size.height - tailSize);
path.lineTo(tailSize, size.height - tailSize);
path.lineTo(tailSize, size.height);
path.close();
} else {
path.addRRect(
RRect.fromLTRBAndCorners(
0,
0,
size.width - tailSize,
size.height,
topLeft: const Radius.circular(radius),
topRight: const Radius.circular(radius),
bottomLeft: const Radius.circular(radius),
bottomRight: const Radius.circular(radius),
),
);
path.moveTo(size.width - tailSize, size.height - tailSize);
path.lineTo(size.width - tailSize, size.height);
path.lineTo(size.width, size.height - tailSize);
path.close();
}
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}Screenshots or Video
Screenshots / Video demonstration
Impeller disabled:telegram-cloud-document-2-5433647737025362042.mp4
Impeller enabled
telegram-cloud-document-2-5433647737025362045.mp4
Logs
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.32.5, on macOS 15.5 24F74 darwin-arm64, locale en-RU)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[!] Xcode - develop for iOS and macOS (Xcode 16.4)
! CocoaPods 1.15.0 out of date (1.16.2 is recommended).
CocoaPods is a package manager for iOS or macOS platform code.
Without CocoaPods, plugins will not work on iOS or macOS.
For more info, see https://flutter.dev/to/platform-plugins
To update CocoaPods, see
https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.3)
[✓] Android Studio (version 2024.1)
[✓] Android Studio (version 2024.2.2)
[✓] IntelliJ IDEA Community Edition (version 2024.2.3)
[✓] VS Code (version 1.101.2)
[✓] Connected device (2 available)
[✓] 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 listc: renderingUI glitches reported at the engine/skia or impeller rendering levelUI glitches reported at the engine/skia or impeller rendering levele: device-specificOnly manifests on certain devicesOnly manifests on certain devicese: 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.platform-androidAndroid applications specificallyAndroid applications specificallyr: fixedIssue is closed as already fixed in a newer versionIssue is closed as already fixed in a newer versionslimpellerEngine binary size reduction. go/slimpellerEngine binary size reduction. go/slimpellerteam-engineOwned by Engine teamOwned by Engine teamtriaged-engineTriaged by Engine teamTriaged by Engine team
Type
Projects
- StatusShow more project fieldsDone
Activity
darshankawar commentedon Jul 9, 2025
https://www.gsmarena.com/samsung_galaxy_s25_ultra-13322.php
GPU : Adreno 830
Chipset : Qualcomm SM8750-AB Snapdragon 8 Elite
darshankawar commentedon Jul 9, 2025
I was unable to replicate this on S10+ device. Maybe this is related to #159876 but it was fixed in previous version already.
@gvozditskiy Can you please switch to latest master and re-run to check if the behavior still persist ?
gvozditskiy commentedon Jul 9, 2025
@darshankawar Flutter
3.34.0-0.1.pre- glitch persists[-]UI glitches when Impeller is enabled on Android[/-][+]UI glitches when Impeller is enabled on Android (on Samsung S25 Ultra)[/+]gvozditskiy commentedon Jul 10, 2025
@darshankawar also reproduced on:
I'll test it on a few other devices as well, once I have access to them
15 remaining items
gaaclarke commentedon Aug 19, 2025
I tried out 6a368d8 and
6a368d8^1, which is the roll that included the reland of flutter-team-archive/engine@04e4989, with the reproduction steps from #171772 (comment).6a368d8^1)So, I don't think 6a368d8 introduced the bug, but it definitely had an effect on it.
gaaclarke commentedon Aug 19, 2025
Here's the reproduction in renderdoc. It looks like the render pass isn't set to clear. That would explain why the artifacts are ghostly, looking like they are incorporating content from old renders.
gaaclarke commentedon Aug 19, 2025
As suspected, the following PR removes the glitch (but is obviously wasteful and not the correct fix):
gaaclarke commentedon Aug 19, 2025
This appears to be the source of the problem. This fixes the problem but is contrary to the comment in the code. Maybe it's out of date?
gaaclarke commentedon Aug 20, 2025
The x-factor that describes devices that demonstrate this bug seems to be
supports_read_from_resolve_. The reason there wasn't aClearhere was thatEntityPassTarget::Flipwas supposed to leave the buffer in a state where it didn't need aClear.The bug doesn't present itself on my iPhone which does support "read from resolve" providing further evidence of this.
Here is all the logic that executes when
supports_read_from_resolve_==false:flutter/engine/src/flutter/impeller/entity/entity_pass_target.cc
Line 40 in 960d107
One suspicious thing is that
EntityPassTarget::Flipreturns astd::shared_ptr<Texture>. Originally that was used: https://github.com/flutter/flutter/blame/74eadd80c5e89d81d99fbfe8afeddef43d603f06/engine/src/flutter/impeller/entity/inline_pass_context.cc#L125After flutter-team-archive/engine#55943 though the result was ignored.
We could switch the logic to only do Clear when we don't support read from resolve, that would at least partially maintain the optimization and would favor new devices.
gaaclarke commentedon Aug 20, 2025
I verified:
gaaclarke commentedon Aug 20, 2025
Here's the frame debugger for the same render pass in #171772 (comment) as far as I can tell. It seems like vulkan is doing DontCare on the color attachment while Metal is doing Clear.
DontCare seems to be used effectively in render passes leading up to that. But that doesn't seem to match what I saw in vulkan.
frame capture:
Runner.gputrace.zip
gvozditskiy commentedon Aug 21, 2025
This is very frustrating. The ability to build with a precompiled shader was removed and Impeller was enabled by default. Now we either have to build with Impeller disabled, which causes numerous freezes, or use an older version of Flutter, which prevents us from using new framework features. Building with Impeller is simply not an option, because the entire application ends up covered in artifacts.
[Impeller] Make sure inline passes always do a clear action. (#174083)
gaaclarke commentedon Aug 26, 2025
Closed via #174083
I was using the reproduction steps in #171772 (comment) which is a derivative of the description's reproduction. The descriptions reproduction only partially showed the artifacts on my device. So it would be helpful if someone could double check to make sure there wasn't 2 issues being reported. If some of the artifacts still show, please file a separate issue and feel free to tag me.
github-actions commentedon Sep 10, 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.