Skip to content

CupertinoSheet top corners render square on Impeller when the sheet body has both a BackdropFilter and a persistent Material Ink decoration #190602

Open

Description

@sLm-s

Steps to reproduce

CupertinoSheet rounds its top corners with a ClipRSuperellipse. On Impeller (iOS 26), that clip collapses to its bounding rectangle — the top corners become square — when the sheet body contains two overlapping compositing layers:

  1. a BackdropFilter (any "glass"/blur surface), and
  2. a persistent Material Ink decoration (Ink(decoration: ShapeDecoration(...)) — exactly what every ListTile wraps its body in, list_tile.dart:999).

With either ingredient alone, the corners are correctly rounded. Only the combination triggers it.

  1. Run the sample below on a physical iOS 26 device or an iOS 26 simulator (Impeller is the default renderer).
  2. Tap Open sheet.
  3. Look at the top corners of the presented sheet — they are square.
  4. Set kInkInSheet (or kBackdropInSheet) to false and re-run — the corners become correctly rounded, proving both ingredients are required.

Does not reproduce with Skia (--no-enable-impeller). No third-party packages and no PlatformView are involved.

Expected results

The CupertinoSheet top corners are rounded (superellipse), regardless of the sheet body content.

Actual results

When the sheet body contains both a BackdropFilter and a persistent Material Ink decoration, the top corners render as a sharp rectangle. The ClipRSuperellipse applied by CupertinoSheet degrades to its bounding rect on Impeller once the clipped subtree is forced onto the layer-based (compositing) clip path by the two overlapping compositing layers. A regular ClipRRect (rounded-rect, not superellipse) over the same content clips correctly, which is why wrapping the sheet child in ClipRRect works around it.

Code sample

Code sample
import 'dart:ui' show ImageFilter;

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

// Both true -> SQUARE top corners (bug). Either false -> correctly ROUNDED.
const bool kBackdropInSheet = true;
const bool kInkInSheet = true;

void main() => runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: _Home()));

class _Home extends StatelessWidget {
  const _Home();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: FilledButton(
          onPressed: () => showCupertinoSheet<void>(
            context: context,
            scrollableBuilder: (context, controller) => const _Sheet(),
          ),
          child: const Text('Open sheet — look at its top corners'),
        ),
      ),
    );
  }
}

class _Sheet extends StatelessWidget {
  const _Sheet();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Filters')),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(child: ListView(children: List.generate(12, (i) => _row('Option $i')))),
            Container(
              decoration: const BoxDecoration(color: Color(0xFFF2F2F7)),
              padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
              child: _glassBar(),
            ),
          ],
        ),
      ),
    );
  }

  // Ingredient 2: a persistent Material Ink decoration (same as ListTile's body).
  Widget _row(String text) {
    final content = Padding(padding: const EdgeInsets.all(16), child: Text(text));
    if (!kInkInSheet) return content;
    return Ink(
      decoration: const ShapeDecoration(shape: Border(), color: Color(0x11000000)),
      child: content,
    );
  }

  // Ingredient 1: a BackdropFilter (blur).
  Widget _glassBar() {
    const label = Center(
      child: Padding(padding: EdgeInsets.symmetric(vertical: 16), child: Text('Show products')),
    );
    if (!kBackdropInSheet) {
      return DecoratedBox(
        decoration: BoxDecoration(color: Colors.orange, borderRadius: BorderRadius.circular(14)),
        child: label,
      );
    }
    return ClipRRect(
      borderRadius: BorderRadius.circular(14),
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
        child: DecoratedBox(
          decoration: BoxDecoration(color: Colors.orange.withValues(alpha: 0.6)),
          child: label,
        ),
      ),
    );
  }
}

Screenshots or Video

Image Image
Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.44.8, on macOS 26.5.2 25F84 darwin-arm64, locale ru-RU)
    • Flutter version 3.44.8 on channel stable at /Users/slim/Documents/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 058e0af2c2 (13 days ago), 2026-07-23 10:56:21 -0700
    • Engine revision 0cd610717b
    • Dart version 3.12.2
    • DevTools version 2.57.0

[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
    • Android SDK at /Users/slim/Library/Android/sdk
    • Platform android-36, build-tools 36.0.0
    • Java version OpenJDK Runtime Environment (build 21.0.9+-14649483-b1163.86)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.6)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17F113
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web

[✓] Connected device (5 available)
    • sdk gphone16k arm64 (mobile)     • emulator-5554 • android-arm64  • Android 17 (API 37) (emulator)
    • Sl-Phone (wireless) (mobile)     • 00008150-000A30421EF2401C • ios • iOS 26.5.2 23F84
    • iPhone 17 Pro Max Fresh (mobile) • 531927E5-3842-4767-84E5-5C89C3D32E66 • ios • iOS-26-5 (simulator)
    • macOS (desktop)                  • macos • darwin-arm64 • macOS 26.5.2 25F84 darwin-arm64
    • Chrome (web)                     • chrome • web-javascript • Google Chrome 150.0.7871.130

[✓] Network resources

• No issues found!

Activity

  1. suryaKommana2662 commented on Aug 5, 2026

    @suryaKommana2662

    @sLm-s Thanks for the detailed report and the reproducible sample.

    I was able to reproduce the reported behavior on an iPhone 17 simulator and android emulator using the provided sample. With both kBackdropInSheet and kInkInSheet set to true, the sheet rendered with square top corners. Setting either flag to false restored the expected rounded corners. Running the sample with flutter run --no-enable-impeller also resulted in correctly rounded corners. Routing this for the team-engine for further investigation.

    Image Image

    Flutter Doctor Output:

    [✓] Flutter (Channel stable, 3.44.8, on macOS 26.4 25E246 darwin-arm64, locale en-IN) [288ms]
        • Flutter version 3.44.8 on channel stable at /Users/suryakommana/develop/flutter
        • Upstream repository https://github.com/flutter/flutter.git
        • Framework revision 058e0af2c2 (13 days ago), 2026-07-23 10:56:21 -0700
        • Engine revision 0cd610717b
        • Dart version 3.12.2
        • DevTools version 2.57.0
        • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
          enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
    
    [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,746ms]
        • Android SDK at /Users/suryakommana/Library/Android/sdk
        • Emulator version 37.1.11.0 (build_id 15917651) (CL:N/A)
        • Platform android-37.0, build-tools 36.0.0
        • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
          This is the JDK bundled with the latest Android Studio installation on this machine.
          To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
        • Java version OpenJDK Runtime Environment (build 25.0.2+-15348964-b329.117)
        • All Android licenses accepted.
    
    [✓] Xcode - develop for iOS and macOS (Xcode 26.6) [676ms]
        • Xcode at /Applications/Xcode.app/Contents/Developer
        • Build 17F113
        • CocoaPods version 1.17.0
    
    [✓] Chrome - develop for the web [4ms]
        • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
    
    [✓] Connected device (4 available) [5.8s]
        • sdk gphone16k arm64 (mobile) • emulator-5554                        • android-arm64  • Android 17 (API 37) (emulator)
        • iPhone 17 (mobile)           • E354ECD5-7984-44E0-9C38-082B39E72C88 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-26-5 (simulator)
        • macOS (desktop)              • macos                                • darwin-arm64   • macOS 26.4 25E246 darwin-arm64
        • Chrome (web)                 • chrome                               • web-javascript • Google Chrome 151.0.7922.76
    
    [✓] Network resources [990ms]
        • All expected network resources are available.
    
    • No issues found!
  2. added
    engineflutter/engine related. See also e: labels.
    p: cupertino_uicupertino_ui package in flutter/packages
    c: renderingUI glitches reported at the engine/skia or impeller rendering level
    e: impellerImpeller rendering backend issues and features requests
    and removed
    in triagePresently being triaged by the triage team
    on Aug 5, 2026
  3. added
    P1High-priority issues at the top of the work list
    on Aug 10, 2026
  4. andywolff commented on Aug 14, 2026

    @andywolff
    Contributor

    I took a look and I think I understand the root cause. I'm still learning the engine code, so please forgive me if this is an over-explanation.

    The corner is square because by the time the AppBar is drawn, the clipping state in the active render pass is missing the route's superellipse clip. This happens through an interaction of two bugs:

    1. Clip Replay Stack Loss: When Ink items finish rendering and call Restore, an engine bug in EntityPassClipStack::RecordRestore mistakenly pops the parent route's superellipse clip off the replay stack (see [Impeller] entitypass clip stack should pop the appropriate number of clips on restore. #162172). When the BackdropFilter glass bar subsequently renders, it splits the render pass and clears the depth buffer, but because the replay stack was emptied by the Inks, the parent clip is never replayed into the new pass.
    2. Depth Saturation: Even if the clip was replayed, Canvas::kMaxDepth (1 << 24, or 16,777,216) is too large for Entity::kDepthEpsilon (which only supports 262,143 discrete levels). When unconstrained layers restore, the depth budget saturates, causing the depth test to pass everywhere and ignoring the clip boundary.

    I have a set of local changes which fixes both of these issues and makes the corner round in the code sample. I'll work on cleaning it up into a PR for review.

    More detail below:

    Regarding clip depth budgets:

    Impeller applies clips using a depth-based stencil-and-cover approach:

    • When a layer or widget hierarchy records into a DisplayList, it precalculates total_depth() based on the number of drawing operations inside that subtree.
    • When applying a clip, Canvas::Save(total_content_depth) allocates a clip depth budget: clip_depth = current_depth_ + total_content_depth.
    • Geometry outside the clip is written to the depth buffer at this clip_depth threshold. Subsequent draw calls only render if their depth does not exceed the clip budget (draw_depth <= clip_depth).

    In the reproduction app:

    • With Ink: The additional ShapeDecoration fills and Border strokes in the 12 list items increase the route's total_depth() to 82.
    • Without Ink: Plain text and padding items require fewer operations, resulting in a smaller total_depth() of 24.

    Rendering Flow in the Reproduction Case

    In a reproduction app based on the original comment's code sample, showCupertinoSheet wraps the sheet content in a ClipRSuperellipse to provide rounded top corners. Inside the sheet, there are ListView items with Ink decorations, a bottom glass bar containing a BackdropFilter, and an AppBar at the top.

    I see two problems which interact in such a way that they cause the problem:

    1. Sibling & Child Restores Prematurely Clear Parent Clips

    I think this is the problem described in #162172

    • clip_height is the nesting depth of clips in the active hierarchy. When the sheet route begins, Impeller records the ClipRSuperellipse as a root parent clip at clip_height = 1.
    • With Ink: Individual list items and nested widgets with Ink decorations push deeper child clips with clip_height > 1. When each child element finishes and restores back to the parent level (restore_height = 1), EntityPassClipStack::RecordRestore only checks coverage.has_value() and !rendered_clip_entities.empty() before calling pop_back(). It does not compare the target restore_height against the height at which the replay entity was originally recorded. As a result, restoring a child clip consumes an entry from rendered_clip_entities even when that entry belongs to a parent clip at clip_height = 1. Because sibling items sequentially restore, each child's restore pops a parent clip until rendered_clip_entities is completely empty before the backdrop filter runs.
    • Without Ink (Baseline): Plain list items (text and padding) push no child clips, so no child clip restorations occur. The root ClipRSuperellipse remains intact on the replay stack and is successfully replayed after the backdrop filter.

    2. Depth Exhaustion and Pass Split across Canvas::FlipBackdrop

    • When the bottom glass bar renders, its BackdropFilter triggers a render pass split inside Canvas::FlipBackdrop, starting a new render pass with a fresh depth buffer.
    • In Canvas::FlipBackdrop, clip replay checks if (replay.clip_depth <= current_depth) continue;. As current_depth_ accumulates across child layer restores, it can exceed the clip's recorded threshold, causing the clip replay to be skipped even if it was retained.
    • In Entity::GetShaderClipDepth, integer depth is converted to a normalized float via 1.0 - (depth + 1) * kDepthEpsilon. Because Entity::kDepthEpsilon is 1.0f / 262144.0 (chosen to avoid GPU precision loss and Z-fighting), the depth buffer supports exactly 262,143 discrete levels (1 / kDepthEpsilon - 1). However, Canvas::kMaxDepth was set to 1 << 24 (16,777,216). When unconstrained layers restore, current_depth_ jumps to kMaxDepth, overflowing the 262,143 capacity and saturating the depth equation. Reducing Canvas::kMaxDepth to 262,143 aligns the canvas budget with shader capacity without risking GPU precision regressions.
    • With Ink: The multiple child layers advance current_depth_ heavily (to 78+). Because current_depth_ was not reset when FlipBackdrop started the new render pass, the subsequent AppBar draw (which adds its own operations) exceeds the remaining clip budget (draw_depth > clip_depth). Furthermore, because the replay stack was emptied in step 1, no clip was written to the new depth buffer. As a result, the AppBar's white background draws unclipped into the top-left corner as a 90° square edge.
    • Without Ink (Baseline): Minimal drawing operations occur, so current_depth_ remains low (14), well below the clip budget (24). Even without a pass depth reset, draw_depth <= clip_depth still holds, so outside fragments are discarded and corners stay rounded.

    Step-by-Step State Traces

    1. Current Flawed Behavior (With Ink)

    Stage Operation clip_height rendered_clip_entities current_depth_ Resulting State
    1. Root Route showCupertinoSheet pushes ClipRSuperellipse 1 [ClipRSuperellipse (height 1)] 0 Root superellipse clip active at depth budget 82
    2. List Items ListView rows with Ink push and pop decorations 2 → 1 [] (prematurely cleared by unconditional pop) 0 → 78 Replay stack becomes empty; depth advances to 78
    3. Glass Bar BackdropFilter triggers FlipBackdrop pass split 1 [] 78 Starts new render pass; 0 clips replayed; depth remains 78
    4. AppBar AppBar paints top header rectangle 1 [] 82+ Depth buffer has no clip recorded (and depth budget is exhausted) → square corner overdraw

    2. Current Behavior (Without Ink / Baseline)

    Stage Operation clip_height rendered_clip_entities current_depth_ Resulting State
    1. Root Route showCupertinoSheet pushes ClipRSuperellipse 1 [ClipRSuperellipse (height 1)] 0 Root superellipse clip active at depth budget 24
    2. List Items Plain ListView rows render without child clips 1 [ClipRSuperellipse (height 1)] (retained) 0 → 12 No child clip restores occur; replay stack remains intact
    3. Glass Bar BackdropFilter triggers FlipBackdrop pass split 1 [ClipRSuperellipse (height 1)] 14 Starts new render pass; replays root clip into new depth buffer
    4. AppBar AppBar paints top header rectangle 1 [ClipRSuperellipse (height 1)] 16 current_depth_ <= 24; depth test discards outside pixels → smooth rounded corners

    3. Expected Behavior with Fix (With Ink)

    Stage Operation clip_height rendered_clip_entities current_depth_ Resulting State
    1. Root Route showCupertinoSheet pushes ClipRSuperellipse 1 [ClipRSuperellipse (height 1)] 0 Root superellipse clip recorded and active
    2. List Items ListView rows with Ink push and pop decorations 2 → 1 [ClipRSuperellipse (height 1)] (parent clip preserved) 0 → 78 Child clips restored without popping height-1 parent
    3. Glass Bar BackdropFilter triggers FlipBackdrop pass split 1 [ClipRSuperellipse (height 1)] 0 (reset for new pass) Starts new render pass; replays root clip with full pass depth budget
    4. AppBar AppBar paints top header rectangle 1 [ClipRSuperellipse (height 1)] 1..2 draw_depth <= clip_depth; depth test discards outside pixels → smooth rounded corners
  5. github-actions commented on Sep 30, 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.

  6. locked as resolved and limited conversation to collaborators on Sep 30, 2026
  7. andywolff commented on Oct 2, 2026

    @andywolff
    Contributor

    I'm still actively working on this. Trying to land #193497 before re-landing the fix for this issue

  8. unlocked this conversation on Oct 4, 2026
  9. andywolff commented on Oct 7, 2026

    @andywolff
    Contributor

    @zemanux this issue is not ready to be closed yet because the fix got reverted. I'll close it after I re-land the fix

  10. zemanux commented on Oct 7, 2026

    @zemanux
    Member

    @zemanux this issue is not ready to be closed yet because the fix got reverted. I'll close it after I re-land the fix

    Hi @andywolff,

    It was completely unintentional! I'm not even following this issue, but I was doing a bunch of rebases about 30 minutes ago, so a commit keyword must have triggered the close by mistake.

    Sorry for the noise and inconvenience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P1High-priority issues at the top of the work listc: renderingUI glitches reported at the engine/skia or impeller rendering levele: impellerImpeller rendering backend issues and features requestsengineflutter/engine related. See also e: labels.p: cupertino_uicupertino_ui package in flutter/packagesteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions