Repository navigation
CupertinoSheet top corners render square on Impeller when the sheet body has both a BackdropFilter and a persistent Material Ink decoration #190602
Description
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:
- a
BackdropFilter(any "glass"/blur surface), and - a persistent Material
Inkdecoration (Ink(decoration: ShapeDecoration(...))— exactly what everyListTilewraps its body in,list_tile.dart:999).
With either ingredient alone, the corners are correctly rounded. Only the combination triggers it.
- Run the sample below on a physical iOS 26 device or an iOS 26 simulator (Impeller is the default renderer).
- Tap Open sheet.
- Look at the top corners of the presented sheet — they are square.
- Set
kInkInSheet(orkBackdropInSheet) tofalseand 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
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
suryaKommana2662 commentedon Aug 5, 2026
@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
kBackdropInSheetandkInkInSheetset totrue, the sheet rendered with square top corners. Setting either flag tofalserestored the expected rounded corners. Running the sample withflutter run --no-enable-impelleralso resulted in correctly rounded corners. Routing this for theteam-enginefor further investigation.Flutter Doctor Output:
andywolff commentedon Aug 14, 2026
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
AppBaris drawn, the clipping state in the active render pass is missing the route's superellipse clip. This happens through an interaction of two bugs:Inkitems finish rendering and callRestore, an engine bug inEntityPassClipStack::RecordRestoremistakenly 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 theBackdropFilterglass 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.Canvas::kMaxDepth(1 << 24, or 16,777,216) is too large forEntity::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:
DisplayList, it precalculatestotal_depth()based on the number of drawing operations inside that subtree.Canvas::Save(total_content_depth)allocates a clip depth budget:clip_depth = current_depth_ + total_content_depth.clip_depththreshold. Subsequent draw calls only render if their depth does not exceed the clip budget (draw_depth <= clip_depth).In the reproduction app:
ShapeDecorationfills andBorderstrokes in the 12 list items increase the route'stotal_depth()to 82.total_depth()of 24.Rendering Flow in the Reproduction Case
In a reproduction app based on the original comment's code sample,
showCupertinoSheetwraps the sheet content in aClipRSuperellipseto provide rounded top corners. Inside the sheet, there areListViewitems withInkdecorations, a bottom glass bar containing aBackdropFilter, and anAppBarat 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_heightis the nesting depth of clips in the active hierarchy. When the sheet route begins, Impeller records theClipRSuperellipseas a root parent clip atclip_height = 1.Inkdecorations push deeper child clips withclip_height > 1. When each child element finishes and restores back to the parent level (restore_height = 1),EntityPassClipStack::RecordRestoreonly checkscoverage.has_value()and!rendered_clip_entities.empty()before callingpop_back(). It does not compare the targetrestore_heightagainst the height at which the replay entity was originally recorded. As a result, restoring a child clip consumes an entry fromrendered_clip_entitieseven when that entry belongs to a parent clip atclip_height = 1. Because sibling items sequentially restore, each child's restore pops a parent clip untilrendered_clip_entitiesis completely empty before the backdrop filter runs.ClipRSuperellipseremains intact on the replay stack and is successfully replayed after the backdrop filter.2. Depth Exhaustion and Pass Split across
Canvas::FlipBackdropBackdropFiltertriggers a render pass split insideCanvas::FlipBackdrop, starting a new render pass with a fresh depth buffer.Canvas::FlipBackdrop, clip replay checksif (replay.clip_depth <= current_depth) continue;. Ascurrent_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.Entity::GetShaderClipDepth, integer depth is converted to a normalized float via1.0 - (depth + 1) * kDepthEpsilon. BecauseEntity::kDepthEpsilonis1.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::kMaxDepthwas set to1 << 24(16,777,216). When unconstrained layers restore,current_depth_jumps tokMaxDepth, overflowing the 262,143 capacity and saturating the depth equation. ReducingCanvas::kMaxDepthto 262,143 aligns the canvas budget with shader capacity without risking GPU precision regressions.current_depth_heavily (to 78+). Becausecurrent_depth_was not reset whenFlipBackdropstarted the new render pass, the subsequentAppBardraw (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, theAppBar's white background draws unclipped into the top-left corner as a 90° square edge.current_depth_remains low (14), well below the clip budget (24). Even without a pass depth reset,draw_depth <= clip_depthstill holds, so outside fragments are discarded and corners stay rounded.Step-by-Step State Traces
1. Current Flawed Behavior (With Ink)
clip_heightrendered_clip_entitiescurrent_depth_showCupertinoSheetpushesClipRSuperellipse[ClipRSuperellipse (height 1)]ListViewrows withInkpush and pop decorations[](prematurely cleared by unconditional pop)BackdropFiltertriggersFlipBackdroppass split[]AppBarpaints top header rectangle[]2. Current Behavior (Without Ink / Baseline)
clip_heightrendered_clip_entitiescurrent_depth_showCupertinoSheetpushesClipRSuperellipse[ClipRSuperellipse (height 1)]ListViewrows render without child clips[ClipRSuperellipse (height 1)](retained)BackdropFiltertriggersFlipBackdroppass split[ClipRSuperellipse (height 1)]AppBarpaints top header rectangle[ClipRSuperellipse (height 1)]current_depth_ <= 24; depth test discards outside pixels → smooth rounded corners3. Expected Behavior with Fix (With Ink)
clip_heightrendered_clip_entitiescurrent_depth_showCupertinoSheetpushesClipRSuperellipse[ClipRSuperellipse (height 1)]ListViewrows withInkpush and pop decorations[ClipRSuperellipse (height 1)](parent clip preserved)BackdropFiltertriggersFlipBackdroppass split[ClipRSuperellipse (height 1)]AppBarpaints top header rectangle[ClipRSuperellipse (height 1)]draw_depth <= clip_depth; depth test discards outside pixels → smooth rounded cornersFix square corner issue (flutter#191096)
Revert: Fix square corner issue (flutter#193189)
github-actions commentedon Sep 30, 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.andywolff commentedon Oct 2, 2026
I'm still actively working on this. Trying to land #193497 before re-landing the fix for this issue
andywolff commentedon Oct 7, 2026
@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
zemanux commentedon Oct 7, 2026
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!