Repository navigation
DCHECK failing on debug engines ( clip_depth assert ) #193124
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: crashStack traces logged to the consoleStack traces logged to the consoleteam-engineOwned by Engine teamOwned by Engine team
Description
reproductioon
Just run the following with a locally built debug engine, it crashes at startup.
Details
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(const BlurryApp());
}
/// Root application widget.
class BlurryApp extends StatelessWidget {
const BlurryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Backdrop Filter Groups Demo',
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark,
theme: ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.indigo,
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0C0E17),
colorSchemeSeed: Colors.cyan,
useMaterial3: true,
),
home: const BackdropFilterGalleryScreen(),
);
}
}
/// Main gallery screen displaying 3 infinite scrolling columns of backdrop filters.
class BackdropFilterGalleryScreen extends StatefulWidget {
const BackdropFilterGalleryScreen({super.key});
@override
State<BackdropFilterGalleryScreen> createState() =>
_BackdropFilterGalleryScreenState();
}
class _BackdropFilterGalleryScreenState
extends State<BackdropFilterGalleryScreen>
with TickerProviderStateMixin {
bool _useBlurGroups = true;
bool _useBoundedFilter = false;
double _blurSigma = 16.0;
bool _autoScroll = true;
double _scrollSpeed = 1.0;
bool _animateShapes = true;
final bool _showFps = true;
// Controllers for 3 columns of infinite scrolling
final ScrollController _scrollController0 = ScrollController();
final ScrollController _scrollController1 = ScrollController();
final ScrollController _scrollController2 = ScrollController();
late final AnimationController _shapesAnimationController;
late final Ticker _scrollTicker;
// FPS tracking
int _frameCount = 0;
final ValueNotifier<double> _fpsNotifier = ValueNotifier<double>(60.0);
Duration _lastFpsCalculationTime = Duration.zero;
Duration _lastFrameTime = Duration.zero;
@override
void initState() {
super.initState();
_shapesAnimationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 20),
)..repeat();
_scrollTicker = createTicker(_onScrollTick);
if (_autoScroll) {
_scrollTicker.start();
}
}
void _onScrollTick(Duration elapsed) {
final double rawDt = _lastFrameTime == Duration.zero
? 0.0
: (elapsed - _lastFrameTime).inMicroseconds / 1000000.0;
_lastFrameTime = elapsed;
// Clamp dt to avoid huge position jumps if the frame stalls
final double dt = math.min(rawDt, 0.05);
// Calculate FPS
_frameCount++;
if (elapsed - _lastFpsCalculationTime >=
const Duration(milliseconds: 500)) {
final double elapsedSec =
(elapsed - _lastFpsCalculationTime).inMicroseconds / 1000000.0;
if (elapsedSec > 0 && mounted) {
_fpsNotifier.value = _frameCount / elapsedSec;
_frameCount = 0;
_lastFpsCalculationTime = elapsed;
}
}
if (!_autoScroll || dt <= 0.0) return;
const double baseSpeedPixelsPerSec = 75.0;
final double speed = baseSpeedPixelsPerSec * _scrollSpeed;
_advanceScroll(_scrollController0, speed * 0.9 * dt);
_advanceScroll(_scrollController1, speed * 1.3 * dt);
_advanceScroll(_scrollController2, speed * 1.0 * dt);
}
void _advanceScroll(ScrollController controller, double delta) {
if (controller.hasClients && controller.position.haveDimensions) {
final double newOffset = controller.offset + delta;
controller.jumpTo(newOffset);
}
}
@override
void dispose() {
_scrollTicker.dispose();
_shapesAnimationController.dispose();
_scrollController0.dispose();
_scrollController1.dispose();
_scrollController2.dispose();
_fpsNotifier.dispose();
super.dispose();
}
void _toggleAutoScroll(bool value) {
setState(() {
_autoScroll = value;
if (_autoScroll) {
_lastFrameTime = Duration.zero;
if (!_scrollTicker.isActive) {
_scrollTicker.start();
}
} else if (!_autoScroll && _scrollTicker.isActive) {
_scrollTicker.stop();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: false,
appBar: AppBar(
backgroundColor: const Color(0xFF131724),
elevation: 4,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.cyanAccent, Colors.purpleAccent],
),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.blur_on, color: Colors.black, size: 20),
),
const SizedBox(width: 10),
const Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Backdrop Filter Groups',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
Text(
'3 Columns Infinite Scroll',
style: TextStyle(fontSize: 10, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
actions: [
// Grouping Mode Badge
Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _useBlurGroups
? Colors.tealAccent.withValues(alpha: 0.15)
: Colors.amberAccent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _useBlurGroups ? Colors.tealAccent : Colors.amberAccent,
width: 1.2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_useBlurGroups ? Icons.layers : Icons.layers_clear,
size: 13,
color: _useBlurGroups
? Colors.tealAccent
: Colors.amberAccent,
),
const SizedBox(width: 5),
Text(
_useBlurGroups ? 'Grouped (1 pass)' : 'Individual (N passes)',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _useBlurGroups
? Colors.tealAccent
: Colors.amberAccent,
),
),
],
),
),
// Bounded Blur Badge
Container(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _useBoundedFilter
? Colors.purpleAccent.withValues(alpha: 0.15)
: Colors.blueAccent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _useBoundedFilter
? Colors.purpleAccent
: Colors.blueAccent,
width: 1.2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_useBoundedFilter ? Icons.crop_square : Icons.crop_free,
size: 13,
color: _useBoundedFilter
? Colors.purpleAccent
: Colors.blueAccent,
),
const SizedBox(width: 5),
Text(
_useBoundedFilter ? 'Bounded' : 'Unbounded',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _useBoundedFilter
? Colors.purpleAccent
: Colors.blueAccent,
),
),
],
),
),
if (_showFps)
ValueListenableBuilder<double>(
valueListenable: _fpsNotifier,
builder: (context, fps, _) {
return Container(
margin: const EdgeInsets.only(left: 4, right: 12),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.white24),
),
child: Text(
'${fps.toStringAsFixed(0)} FPS',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
color: fps >= 55
? Colors.greenAccent
: (fps >= 30
? Colors.amberAccent
: Colors.redAccent),
),
),
);
},
),
],
),
body: Column(
children: [
// Control Panel at the top
_buildControlPanel(),
// Main View: Stack with Background Primitive Shapes + Foreground 3-Column BackdropFilters
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
// Layer 0: Drawn Primitive Shapes
AnimatedBuilder(
animation: _shapesAnimationController,
builder: (context, child) {
return CustomPaint(
painter: PrimitiveShapesPainter(
animationValue: _animateShapes
? _shapesAnimationController.value
: 0.0,
),
size: Size.infinite,
);
},
),
// Layer 1: 3 Columns of Backdrop Filters
_buildBackdropGroupContent(),
],
),
),
],
),
);
}
/// Builds the top controls with the Blur Groups toggle, Bounded Blur toggle, blur intensity slider, and scrolling settings.
Widget _buildControlPanel() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: const BoxDecoration(
color: Color(0xFF131724),
border: Border(
bottom: BorderSide(color: Color(0xFF242A3E), width: 1.0),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Top Row: Blur Groups Toggle, Bounded Blur Toggle, and Action Buttons
Row(
children: [
// Main Toggle 1: Blur Groups
Expanded(
child: InkWell(
onTap: () {
setState(() {
_useBlurGroups = !_useBlurGroups;
});
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _useBlurGroups
? const Color(0xFF1A3338)
: const Color(0xFF332A1A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _useBlurGroups
? Colors.tealAccent.withValues(alpha: 0.6)
: Colors.amberAccent.withValues(alpha: 0.6),
width: 1.5,
),
),
child: Row(
children: [
Switch(
value: _useBlurGroups,
activeThumbColor: Colors.tealAccent,
activeTrackColor: Colors.teal.withValues(alpha: 0.5),
inactiveThumbColor: Colors.amberAccent,
inactiveTrackColor: Colors.amber.withValues(
alpha: 0.3,
),
onChanged: (val) {
setState(() {
_useBlurGroups = val;
});
},
),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Use Blur Groups',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.white,
),
),
Text(
_useBlurGroups
? 'BackdropGroup active'
: 'BackdropGroup disabled',
style: TextStyle(
fontSize: 10,
color: _useBlurGroups
? Colors.tealAccent
: Colors.amberAccent,
),
),
],
),
),
],
),
),
),
),
const SizedBox(width: 10),
// Main Toggle 2: Bounded Blur
Expanded(
child: InkWell(
onTap: () {
setState(() {
_useBoundedFilter = !_useBoundedFilter;
});
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: _useBoundedFilter
? const Color(0xFF321E3B)
: const Color(0xFF17293E),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _useBoundedFilter
? Colors.purpleAccent.withValues(alpha: 0.6)
: Colors.blueAccent.withValues(alpha: 0.6),
width: 1.5,
),
),
child: Row(
children: [
Switch(
value: _useBoundedFilter,
activeThumbColor: Colors.purpleAccent,
activeTrackColor: Colors.purple.withValues(
alpha: 0.5,
),
inactiveThumbColor: Colors.blueAccent,
inactiveTrackColor: Colors.blue.withValues(
alpha: 0.3,
),
onChanged: (val) {
setState(() {
_useBoundedFilter = val;
});
},
),
const SizedBox(width: 6),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Bounded Blur',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.white,
),
),
Text(
_useBoundedFilter
? 'bounded: true (iOS style)'
: 'bounded: false (unbounded)',
style: TextStyle(
fontSize: 10,
color: _useBoundedFilter
? Colors.purpleAccent
: Colors.blueAccent,
),
),
],
),
),
],
),
),
),
),
const SizedBox(width: 10),
// Auto Scroll Toggle
IconButton.filledTonal(
tooltip: _autoScroll
? 'Pause Auto-Scroll'
: 'Start Auto-Scroll',
onPressed: () => _toggleAutoScroll(!_autoScroll),
icon: Icon(
_autoScroll ? Icons.pause : Icons.play_arrow,
color: _autoScroll ? Colors.cyanAccent : Colors.white70,
),
),
const SizedBox(width: 4),
// Animate Shapes Background Toggle
IconButton.outlined(
tooltip: _animateShapes
? 'Pause Background Animation'
: 'Animate Background',
onPressed: () {
setState(() {
_animateShapes = !_animateShapes;
if (_animateShapes) {
_shapesAnimationController.repeat();
} else {
_shapesAnimationController.stop();
}
});
},
icon: Icon(
_animateShapes ? Icons.animation : Icons.motion_photos_off,
color: _animateShapes ? Colors.purpleAccent : Colors.white54,
size: 20,
),
),
const SizedBox(width: 4),
// Info Dialog
IconButton(
tooltip: 'How Blur Groups & Bounded Blur Work',
icon: const Icon(Icons.info_outline, size: 20),
onPressed: _showInfoDialog,
),
],
),
const SizedBox(height: 8),
// Secondary Row: Blur Sigma Slider & Scroll Speed
Row(
children: [
// Blur Sigma Slider
const Icon(Icons.blur_linear, size: 16, color: Colors.cyanAccent),
const SizedBox(width: 6),
Text(
'Sigma: ${_blurSigma.toStringAsFixed(1)}',
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: Colors.white70,
),
),
Expanded(
child: Slider(
value: _blurSigma,
min: 1.0,
max: 40.0,
divisions: 39,
activeColor: Colors.cyanAccent,
inactiveColor: Colors.white12,
label: '${_blurSigma.toStringAsFixed(1)}σ',
onChanged: (val) {
setState(() {
_blurSigma = val;
});
},
),
),
const SizedBox(width: 12),
// Speed Selector
const Text(
'Speed: ',
style: TextStyle(fontSize: 11, color: Colors.white60),
),
SegmentedButton<double>(
segments: const [
ButtonSegment(
value: 0.5,
label: Text('0.5x', style: TextStyle(fontSize: 10)),
),
ButtonSegment(
value: 1.0,
label: Text('1x', style: TextStyle(fontSize: 10)),
),
ButtonSegment(
value: 2.0,
label: Text('2x', style: TextStyle(fontSize: 10)),
),
],
selected: {_scrollSpeed},
showSelectedIcon: false,
style: const ButtonStyle(
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onSelectionChanged: (set) {
setState(() {
_scrollSpeed = set.first;
});
},
),
],
),
],
),
);
}
/// Builds the 3-column infinite scrolling area, conditionally wrapped in BackdropGroup.
Widget _buildBackdropGroupContent() {
final Widget columnsWidget = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: _buildInfiniteScrollColumn(
columnIndex: 0,
controller: _scrollController0,
colorAccent: const Color(0xFF00E5FF),
),
),
Container(width: 1, color: Colors.white10),
Expanded(
child: _buildInfiniteScrollColumn(
columnIndex: 1,
controller: _scrollController1,
colorAccent: const Color(0xFFFF4081),
),
),
Container(width: 1, color: Colors.white10),
Expanded(
child: _buildInfiniteScrollColumn(
columnIndex: 2,
controller: _scrollController2,
colorAccent: const Color(0xFF7C4DFF),
),
),
],
);
// If _useBlurGroups is enabled, wrap inside BackdropGroup.
if (_useBlurGroups) {
return BackdropGroup(child: columnsWidget);
}
return columnsWidget;
}
/// Builds an infinite scrolling column of BackdropFilter cards.
Widget _buildInfiniteScrollColumn({
required int columnIndex,
required ScrollController controller,
required Color colorAccent,
}) {
return ListView.builder(
controller: controller,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 16),
itemBuilder: (context, index) {
return _BackdropCard(
columnIndex: columnIndex,
itemIndex: index,
blurSigma: _blurSigma,
useBlurGroups: _useBlurGroups,
useBoundedFilter: _useBoundedFilter,
colorAccent: colorAccent,
);
},
);
}
void _showInfoDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: const Color(0xFF1B2032),
title: const Row(
children: [
Icon(Icons.layers, color: Colors.cyanAccent),
SizedBox(width: 8),
Text('BackdropFilter & Groups Guide'),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'BackdropFilter Grouping & Bounded Blur Overview',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.tealAccent.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.tealAccent.withValues(alpha: 0.4),
),
),
child: const Text(
'✓ Grouped Blur: Wraps subtree in BackdropGroup and uses BackdropFilter.grouped. The Flutter engine shares the same BackdropKey across all filters, calculating the backdrop blur once per frame for the whole group instead of repeating for every card.',
style: TextStyle(fontSize: 12, height: 1.4),
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.purpleAccent.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.purpleAccent.withValues(alpha: 0.4),
),
),
child: const Text(
'◩ Bounded Blur (ImageFilterConfig.blur(bounded: true)): Restricts sampling to the bounding rectangle of the object, preventing outside colors from bleeding into the frosted glass effect.',
style: TextStyle(fontSize: 12, height: 1.4),
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.amberAccent.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.amberAccent.withValues(alpha: 0.4),
),
),
child: const Text(
'✗ Ungrouped Blur: Each BackdropFilter creates its own independent texture capture and blur pass, resulting in N passes across the 3 columns.',
style: TextStyle(fontSize: 12, height: 1.4),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Close',
style: TextStyle(color: Colors.cyanAccent),
),
),
],
);
},
);
}
}
/// A card widget containing a BackdropFilter with frosted glass styling and primitive card accents.
class _BackdropCard extends StatelessWidget {
const _BackdropCard({
required this.columnIndex,
required this.itemIndex,
required this.blurSigma,
required this.useBlurGroups,
required this.useBoundedFilter,
required this.colorAccent,
});
final int columnIndex;
final int itemIndex;
final double blurSigma;
final bool useBlurGroups;
final bool useBoundedFilter;
final Color colorAccent;
@override
Widget build(BuildContext context) {
// Varied heights for visual rhythm
final double cardHeight = 130.0 + (itemIndex % 3) * 25.0;
final List<IconData> icons = [
Icons.auto_awesome,
Icons.water_drop,
Icons.category,
Icons.circle_outlined,
Icons.interests,
Icons.grain,
];
final IconData icon = icons[(columnIndex * 3 + itemIndex) % icons.length];
final ImageFilterConfig filterConfig = ImageFilterConfig.blur(
sigmaX: blurSigma,
sigmaY: blurSigma,
bounded: useBoundedFilter,
);
final Widget cardContent = Container(
height: cardHeight,
padding: const EdgeInsets.all(14.0),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(16.0),
border: Border.all(
color: Colors.white.withValues(alpha: 0.18),
width: 1.2,
),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withValues(alpha: 0.15),
// blurRadius: 8,
// offset: const Offset(0, 4),
// ),
// ],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Card Top Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: colorAccent.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: colorAccent.withValues(alpha: 0.5),
width: 1,
),
),
child: Icon(icon, size: 16, color: colorAccent),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'#${columnIndex + 1}-${(itemIndex % 1000).toString().padLeft(3, '0')}',
style: const TextStyle(
fontSize: 10,
fontFamily: 'monospace',
color: Colors.white70,
),
),
),
],
),
// Card Middle Body
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Backdrop Filter',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white.withValues(alpha: 0.9),
),
),
const SizedBox(height: 2),
Text(
'ImageFilterConfig.blur(σ=${blurSigma.toStringAsFixed(1)}, bounded=$useBoundedFilter)',
style: const TextStyle(
fontSize: 9.5,
fontFamily: 'monospace',
color: Colors.white60,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
// Card Footer with Group & Bounded Status Tags
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: useBlurGroups
? Colors.tealAccent
: Colors.amberAccent,
),
),
const SizedBox(width: 4),
Text(
useBlurGroups ? 'Grouped' : 'Individual',
style: TextStyle(
fontSize: 9,
fontWeight: FontWeight.w600,
color: useBlurGroups
? Colors.tealAccent
: Colors.amberAccent,
),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration(
color: useBoundedFilter
? Colors.purpleAccent.withValues(alpha: 0.2)
: Colors.blueAccent.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
child: Text(
useBoundedFilter ? 'Bounded' : 'Unbounded',
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.bold,
color: useBoundedFilter
? Colors.purpleAccent
: Colors.blueAccent,
),
),
),
],
),
Text(
'Col ${columnIndex + 1}',
style: TextStyle(
fontSize: 9,
color: colorAccent.withValues(alpha: 0.8),
fontWeight: FontWeight.bold,
),
),
],
),
],
),
);
// If useBlurGroups is true, use BackdropFilter.grouped constructor.
// Otherwise, use standard BackdropFilter constructor.
final Widget filtered = useBlurGroups
? BackdropFilter.grouped(filterConfig: filterConfig, child: cardContent)
: BackdropFilter(filterConfig: filterConfig, child: cardContent);
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: filtered,
),
);
}
}
/// CustomPainter that renders vibrant primitive shapes behind the backdrop filters.
/// Includes circles, rounded rects, rotated squares, triangles, stars, grids, and glowing rings.
class PrimitiveShapesPainter extends CustomPainter {
PrimitiveShapesPainter({required this.animationValue});
final double animationValue;
@override
void paint(Canvas canvas, Size size) {
// Dark gradient background canvas
final Rect backgroundRect = Offset.zero & size;
final Paint bgPaint = Paint()
..shader = const RadialGradient(
center: Alignment(0.0, -0.2),
radius: 1.2,
colors: [Color(0xFF191D33), Color(0xFF0F1221), Color(0xFF07080E)],
).createShader(backgroundRect);
canvas.drawRect(backgroundRect, bgPaint);
final double phase = animationValue * 2 * math.pi;
// Draw background subtle grid lines
_drawGrid(canvas, size);
// List of primitive shape definitions with movement offsets
_drawGlowingOrbs(canvas, size, phase);
_drawRotatedPolygons(canvas, size, phase);
_drawGeometricLinesAndArcs(canvas, size, phase);
_drawFloatingStarsAndTriangles(canvas, size, phase);
_drawSolidColorTiles(canvas, size, phase);
}
void _drawGrid(Canvas canvas, Size size) {
final Paint gridPaint = Paint()
..color = const Color(0x12FFFFFF)
..strokeWidth = 1.0;
const double step = 60.0;
for (double x = 0; x < size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
for (double y = 0; y < size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}
}
void _drawGlowingOrbs(Canvas canvas, Size size, double phase) {
final List<_OrbData> orbs = [
_OrbData(
baseX: 0.2,
baseY: 0.25,
radius: 110,
color1: const Color(0xFFFF007F),
color2: const Color(0xFFFF5252),
driftX: 40,
driftY: 30,
speedMultiplier: 1.0,
),
_OrbData(
baseX: 0.8,
baseY: 0.3,
radius: 130,
color1: const Color(0xFF00E5FF),
color2: const Color(0xFF2979FF),
driftX: -35,
driftY: 45,
speedMultiplier: 1.2,
),
_OrbData(
baseX: 0.5,
baseY: 0.65,
radius: 150,
color1: const Color(0xFFFFD600),
color2: const Color(0xFFFF6D00),
driftX: 50,
driftY: -35,
speedMultiplier: 0.8,
),
_OrbData(
baseX: 0.15,
baseY: 0.8,
radius: 100,
color1: const Color(0xFF76FF03),
color2: const Color(0xFF00E676),
driftX: -30,
driftY: -40,
speedMultiplier: 1.1,
),
_OrbData(
baseX: 0.85,
baseY: 0.85,
radius: 120,
color1: const Color(0xFFD500F9),
color2: const Color(0xFF651FFF),
driftX: 40,
driftY: 25,
speedMultiplier: 0.9,
),
];
for (final orb in orbs) {
final double cx =
size.width * orb.baseX +
math.cos(phase * orb.speedMultiplier) * orb.driftX;
final double cy =
size.height * orb.baseY +
math.sin(phase * orb.speedMultiplier) * orb.driftY;
final Offset center = Offset(cx, cy);
final Paint orbPaint = Paint()
..shader = RadialGradient(
colors: [
orb.color1.withValues(alpha: 0.85),
orb.color2.withValues(alpha: 0.4),
orb.color2.withValues(alpha: 0.0),
],
stops: const [0.0, 0.65, 1.0],
).createShader(Rect.fromCircle(center: center, radius: orb.radius));
canvas.drawCircle(center, orb.radius, orbPaint);
// Concentric sharp ring
final Paint ringPaint = Paint()
..color = orb.color1.withValues(alpha: 0.7)
..style = PaintingStyle.stroke
..strokeWidth = 3.0;
canvas.drawCircle(center, orb.radius * 0.5, ringPaint);
}
}
void _drawRotatedPolygons(Canvas canvas, Size size, double phase) {
// Draw rotated rounded rectangles and squares
final List<_PolyData> polygons = [
_PolyData(
x: size.width * 0.35,
y: size.height * 0.18,
width: 140,
height: 140,
color: const Color(0xFF00B0FF),
rotation: phase * 0.7,
cornerRadius: 24,
),
_PolyData(
x: size.width * 0.65,
y: size.height * 0.48,
width: 160,
height: 100,
color: const Color(0xFFFF1744),
rotation: -phase * 0.5,
cornerRadius: 18,
),
_PolyData(
x: size.width * 0.3,
y: size.height * 0.88,
width: 130,
height: 130,
color: const Color(0xFF00E676),
rotation: phase * 0.6,
cornerRadius: 30,
),
_PolyData(
x: size.width * 0.7,
y: size.height * 0.12,
width: 110,
height: 110,
color: const Color(0xFFFFEA00),
rotation: -phase * 0.8,
cornerRadius: 16,
),
];
for (final poly in polygons) {
canvas.save();
canvas.translate(poly.x, poly.y);
canvas.rotate(poly.rotation);
final Rect rect = Rect.fromCenter(
center: Offset.zero,
width: poly.width,
height: poly.height,
);
final Paint fillPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
poly.color.withValues(alpha: 0.8),
poly.color.withValues(alpha: 0.3),
],
).createShader(rect);
final RRect rrect = RRect.fromRectAndRadius(
rect,
Radius.circular(poly.cornerRadius),
);
canvas.drawRRect(rrect, fillPaint);
final Paint strokePaint = Paint()
..color = Colors.white.withValues(alpha: 0.6)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawRRect(rrect, strokePaint);
canvas.restore();
}
}
void _drawGeometricLinesAndArcs(Canvas canvas, Size size, double phase) {
// Dynamic intersecting angled bars
final Paint barPaint1 = Paint()
..color = const Color(0xFFFF0055).withValues(alpha: 0.6)
..strokeWidth = 16.0
..strokeCap = StrokeCap.round;
final Paint barPaint2 = Paint()
..color = const Color(0xFF00F0FF).withValues(alpha: 0.6)
..strokeWidth = 12.0
..strokeCap = StrokeCap.round;
final double yOffset1 = math.sin(phase) * 40;
final double yOffset2 = math.cos(phase) * 50;
canvas.drawLine(
Offset(-50, size.height * 0.4 + yOffset1),
Offset(size.width + 50, size.height * 0.55 + yOffset1),
barPaint1,
);
canvas.drawLine(
Offset(-50, size.height * 0.6 + yOffset2),
Offset(size.width + 50, size.height * 0.35 + yOffset2),
barPaint2,
);
}
void _drawFloatingStarsAndTriangles(Canvas canvas, Size size, double phase) {
// Star 1
_drawStar(
canvas: canvas,
center: Offset(
size.width * 0.5 + math.sin(phase * 0.9) * 60,
size.height * 0.35 + math.cos(phase * 0.9) * 40,
),
points: 5,
outerRadius: 65,
innerRadius: 28,
color: const Color(0xFFFFD700),
rotation: phase * 0.6,
);
// Star 2
_drawStar(
canvas: canvas,
center: Offset(
size.width * 0.15 + math.cos(phase * 1.1) * 30,
size.height * 0.5 + math.sin(phase * 1.1) * 50,
),
points: 6,
outerRadius: 50,
innerRadius: 22,
color: const Color(0xFFFF4081),
rotation: -phase * 0.7,
);
// Triangle 1
_drawTriangle(
canvas: canvas,
center: Offset(
size.width * 0.85 + math.sin(phase * 0.7) * 40,
size.height * 0.65 + math.cos(phase * 0.7) * 30,
),
size: 90,
color: const Color(0xFF64FFDA),
rotation: phase * 0.5,
);
}
void _drawSolidColorTiles(Canvas canvas, Size size, double phase) {
// Crisp geometric diamond
canvas.save();
canvas.translate(
size.width * 0.5 + math.cos(phase * 1.3) * 40,
size.height * 0.85 + math.sin(phase * 1.3) * 30,
);
canvas.rotate(phase + math.pi / 4);
final Paint diamondPaint = Paint()
..color = const Color(0xFFFF9100).withValues(alpha: 0.75)
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromCenter(center: Offset.zero, width: 80, height: 80),
diamondPaint,
);
canvas.restore();
}
void _drawStar({
required Canvas canvas,
required Offset center,
required int points,
required double outerRadius,
required double innerRadius,
required Color color,
required double rotation,
}) {
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(rotation);
final Path path = Path();
final double step = math.pi / points;
for (int i = 0; i < 2 * points; i++) {
final double r = i.isEven ? outerRadius : innerRadius;
final double angle = i * step - math.pi / 2;
final double x = r * math.cos(angle);
final double y = r * math.sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
final Paint fillPaint = Paint()
..color = color.withValues(alpha: 0.8)
..style = PaintingStyle.fill;
canvas.drawPath(path, fillPaint);
final Paint strokePaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawPath(path, strokePaint);
canvas.restore();
}
void _drawTriangle({
required Canvas canvas,
required Offset center,
required double size,
required Color color,
required double rotation,
}) {
canvas.save();
canvas.translate(center.dx, center.dy);
canvas.rotate(rotation);
final Path path = Path();
const double angleStep = 2 * math.pi / 3;
for (int i = 0; i < 3; i++) {
final double angle = i * angleStep - math.pi / 2;
final double x = size * 0.6 * math.cos(angle);
final double y = size * 0.6 * math.sin(angle);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
final Paint fillPaint = Paint()
..color = color.withValues(alpha: 0.8)
..style = PaintingStyle.fill;
canvas.drawPath(path, fillPaint);
final Paint strokePaint = Paint()
..color = Colors.white70
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawPath(path, strokePaint);
canvas.restore();
}
@override
bool shouldRepaint(covariant PrimitiveShapesPainter oldDelegate) {
return oldDelegate.animationValue != animationValue;
}
}
class _OrbData {
_OrbData({
required this.baseX,
required this.baseY,
required this.radius,
required this.color1,
required this.color2,
required this.driftX,
required this.driftY,
required this.speedMultiplier,
});
final double baseX;
final double baseY;
final double radius;
final Color color1;
final Color color2;
final double driftX;
final double driftY;
final double speedMultiplier;
}
class _PolyData {
_PolyData({
required this.x,
required this.y,
required this.width,
required this.height,
required this.color,
required this.rotation,
required this.cornerRadius,
});
final double x;
final double y;
final double width;
final double height;
final Color color;
final double rotation;
final double cornerRadius;
}
log
[FATAL:flutter/impeller/display_list/canvas.cc(2473)] Check failed: current_depth_ <= transform_stack_.back().clip_depth. 135 <=? 134
version
PS C:\Users\aaclarke\dev\flutter-examples\blurry> flutter doctor -v
[!] Flutter (Channel [user-branch], 3.48.0-1.0.pre-843, on Microsoft Windows [Version 10.0.26200.9445], locale en-US)
[972ms]
! Flutter version 3.48.0-1.0.pre-843 on channel [user-branch] at C:\Users\aaclarke\dev\flutter
Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
If that doesn't fix the issue, try deleting the 'bin/cache/flutter.version.json' file in your Flutter SDK
directory and then reinstall Flutter by following instructions at https://flutter.dev/setup.
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 124f1de69f (6 hours ago), 2026-09-21 15:55:08 +0000
• Engine revision 19946f91c8
• Dart version 3.14.0 (build 3.14.0-248.0.dev)
• DevTools version 2.61.0-dev.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android,
enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager,
omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration, enable-riscv64, enable-hcpp
• If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly
to perform update checks and upgrades.
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listc: crashStack traces logged to the consoleStack traces logged to the consoleteam-engineOwned by Engine teamOwned by Engine team
Activity
gaaclarke commentedon Sep 21, 2026
@andywolff this sounds likely related to #191096
I didn't get a chance to verify yet since you have to build a custom engine to get DCHECKs.
gaaclarke commentedon Sep 21, 2026
It's not #191096, I reverted that and still got the error.
[-]DCHECK failing on[/-][+]DCHECK failing on debug engines ( clip_depth assert )[/+]ahmadwyne commentedon Sep 22, 2026
I took a look at this and I think it's coming from the grouped backdrop fast path in
Canvas::SaveLayer. When the shared snapshot gets reused, it draws the backdrop with++current_depth_and then callsSave(0), so the card's contents get no depth budget. The first thing drawn inside the card lands one pastclip_depth, which lines up with the135 <=? 134in the log.The normal saveLayer path reserves
current_depth_ + total_content_depth, so I think changing that toSave(total_content_depth)should fix it. The backdrop draw uses the layer's own depth slot, andRestorejust pops a plain save without drawing, so nothing gets counted twice. The repro hits this every frame since all the cards share the same backdrop key and blur.I haven't been able to verify it on a debug engine yet. If you're not already on it, I'm happy to put up a PR with the fix and a test that draws two backdrop filters sharing an id and a draw inside each.
gaaclarke commentedon Sep 22, 2026
Thanks @ahmadwyne, if you want to try fixing it in the next couple of hours that sounds good to me. Otherwise, I'll circle back to it later today. It's kind of an impediment for me so I can't really wait around on it =)
ahmadwyne commentedon Sep 22, 2026
Thanks! Realistically I won't have a debug engine build going in the next couple of hours, so please go ahead since it's blocking you and I don't want to hinder your progress. If it helps, a quick test would be two saveLayers sharing a backdrop id with
all_filters_equal, and a draw inside each one. That should hit the DCHECK before the fix. Happy to review or check the repro on the PR once it's up. :))ahmadwyne commentedon Sep 22, 2026
@gaaclarke Update; I got a debug engine build working after all, so I tried the fix anyway in case I made it in time: #193176
It's the one-line change from my earlier comment, plus a regression test with two grouped backdrop layers that each draw something inside. The test hits the DCHECK before the fix (
2 <=? 1at the same spot inAddRenderEntityToCurrentPass) and passes after, on OpenGLES and Vulkan (SwiftShader with validation layers on). The rest of*Backdrop*and*ClipDepth*still pass.If you've already got a fix in flight, feel free to close mine and land yours; sorry for the back and forth either way. Otherwise I'd really appreciate it if you could take a look. :))
[Impeller] Reserve content depth in the shared backdrop fast path (fl…
github-actions commentedon Oct 8, 2026
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.