Skip to content

DCHECK failing on debug engines ( clip_depth assert ) #193124

Closed

Description

@gaaclarke

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.

Activity

  1. added
    c: crashStack traces logged to the console
    P2Important issues not at the top of the work list
    on Sep 21, 2026
  2. gaaclarke commented on Sep 21, 2026

    @gaaclarke
    MemberAuthor

    @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.

  3. gaaclarke commented on Sep 21, 2026

    @gaaclarke
    MemberAuthor

    It's not #191096, I reverted that and still got the error.

  4. changed the title [-]DCHECK failing on[/-] [+]DCHECK failing on debug engines ( clip_depth assert )[/+] on Sep 21, 2026
  5. ahmadwyne commented on Sep 22, 2026

    @ahmadwyne
    Contributor

    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 calls Save(0), so the card's contents get no depth budget. The first thing drawn inside the card lands one past clip_depth, which lines up with the 135 <=? 134 in the log.

    The normal saveLayer path reserves current_depth_ + total_content_depth, so I think changing that to Save(total_content_depth) should fix it. The backdrop draw uses the layer's own depth slot, and Restore just 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.

  6. gaaclarke commented on Sep 22, 2026

    @gaaclarke
    MemberAuthor

    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 =)

  7. ahmadwyne commented on Sep 22, 2026

    @ahmadwyne
    Contributor

    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. :))

  8. ahmadwyne commented on Sep 22, 2026

    @ahmadwyne
    Contributor

    @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 <=? 1 at the same spot in AddRenderEntityToCurrentPass) 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. :))

  9. github-actions commented on Oct 8, 2026

    @github-actions

    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 -v and a minimal reproduction of the issue.

  10. locked as resolved and limited conversation to collaborators on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: crashStack traces logged to the consoleteam-engineOwned by Engine team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions