Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImageFiltered Render Issue #105674

Closed
JsouLiang opened this issue Jun 9, 2022 · 25 comments · Fixed by flutter/engine#33981
Closed

ImageFiltered Render Issue #105674

JsouLiang opened this issue Jun 9, 2022 · 25 comments · Fixed by flutter/engine#33981
Labels
c: rendering UI glitches reported at the engine/skia rendering level engine flutter/engine repository. See also e: labels. found in release: 3.0 Found to occur in 3.0 found in release: 3.1 Found to occur in 3.1 has reproducible steps The issue has been confirmed reproducible and is ready to work on P0 Critical issues such as a build break or regression platform-ios iOS applications specifically r: fixed Issue is closed as already fixed in a newer version

Comments

@JsouLiang
Copy link
Contributor

JsouLiang commented Jun 9, 2022

The scenario is like this:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightBlue,
      body: ListView(
        controller: _controller,
        children: <Widget>[
          ImageFiltered(
            imageFilter: ImageFilter.blur(
              sigmaX: 4,
              sigmaY: 4,
            ),
            child: Container(
              width: 50,
              height: 50,
              color: Colors.red,
            ),
          ),
          Container(
            height: 800,
          ),
        ],
      ),
    );
  }

this is the behavior:

2022-06-09.14.00.36.mov

The engine is the latest version.
And I found this issue may be because of Partial repaint

The second scenario is like this:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.lightBlue,
      body: SingleChildScrollView(
        child: Row(
          children: [ImageFiltered(
            imageFilter: ImageFilter.blur(
              sigmaX: 4,
              sigmaY: 4,
            ),
            child: Container(
              width: 50,
              height: 50,
              color: Colors.red,
            ),
          ),
            Container(
              height: 800,
            ),],
        ),
      ),
    );
  }
2022-06-09.15.45.49.mov

The second scenario and the first scenario may not be the same problem.
Because the second scenario I set the RasterCahe is nullptr, and don't use the partial repaint

// set the clip_rect is nullptr when Raster
CompositorContext::ScopedFrame::Raster() {
  std::optional<SkRect> clip_rect = std::nullopt;
  bool root_needs_readback = layer_tree.Preroll(
      *this, ignore_raster_cache, clip_rect ? *clip_rect : kGiantRect);
....
}

// set this when preoll and paint:
PrerollContext.raster_cache = nullptr;
PaintContext.raster_cache = nullptr;
@JsouLiang
Copy link
Contributor Author

It is the ImageFilter diff rect is calculated as an error value.
https://github.com/flutter/engine/blob/main/flow/layers/image_filter_layer.cc#L29

@rivella50
Copy link

Is this effect happen all the time?
Perhaps it has something to do with the flickering issue no one could reproduce so far:
#100522

@JsouLiang
Copy link
Contributor Author

@rivella50 Yeah, it happens all time. you can try my demo code to reproduce it.

@danagbemava-nc danagbemava-nc added the in triage Presently being triaged by the triage team label Jun 9, 2022
@danagbemava-nc
Copy link
Member

Issue is reproducible on stable and master.

In my testing, I was only able to reproduce this on iOS.

recordings
iOS macOS
Simulator.Screen.Recording.-.iPhone.13.-.2022-06-09.at.08.36.16.mp4
Screen.Recording.2022-06-09.at.08.59.43.mov
code sample
import 'dart:ui';

import 'package:flutter/material.dart';

class MyCustomScrollBehavior extends MaterialScrollBehavior {
  // Override behavior methods and getters like dragDevices
  @override
  Set<PointerDeviceKind> get dragDevices => {
    PointerDeviceKind.touch,
    PointerDeviceKind.mouse,
    // etc.
  };
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scrollBehavior: MyCustomScrollBehavior(),
      home: const Scaffold(
        backgroundColor: Colors.black,
        body: BugTest(),
      ),
    );
  }
}

class BugTest extends StatefulWidget {
  const BugTest({Key? key}) : super(key: key);

  @override
  State<BugTest> createState() => _BugTestState();
}

class _BugTestState extends State<BugTest> {
  final ScrollController _controller = ScrollController();

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      backgroundColor: Colors.lightBlue,
      body: ListView(
        physics: AlwaysScrollableScrollPhysics(),
        controller: _controller,
        children: <Widget>[
          ImageFiltered(
            imageFilter: ImageFilter.blur(
              sigmaX: 4,
              sigmaY: 4,
            ),
            child: Container(
              width: 50,
              height: 50,
              color: Colors.red,
            ),
          ),
          Container(
            height: 800,
          ),
        ],
      ),
    );
  }
}
flutter doctor -v
[✓] Flutter (Channel stable, 3.0.1, on macOS 12.3.1 21E258 darwin-arm, locale en-GB)
    • Flutter version 3.0.1 at /Users/nexus/dev/sdks/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision fb57da5f94 (3 weeks ago), 2022-05-19 15:50:29 -0700
    • Engine revision caaafc5604
    • Dart version 2.17.1
    • DevTools version 2.12.2

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-32, build-tools 31.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.67.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.42.0

[✓] Connected device (3 available)
    • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 12 (API 31) (emulator)
    • macOS (desktop)             • macos         • darwin-arm64   • macOS 12.3.1 21E258 darwin-arm
    • Chrome (web)                • chrome        • web-javascript • Google Chrome 102.0.5005.61
    ! Error: Nexus is not connected. Xcode will continue when Nexus is connected. (code -13)

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
[✓] Flutter (Channel master, 3.1.0-0.0.pre.1187, on macOS 12.3.1 21E258 darwin-arm, locale en-GB)
    • Flutter version 3.1.0-0.0.pre.1187 at /Users/nexus/dev/sdks/flutters
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision c951d2d7a0 (2 hours ago), 2022-06-09 07:38:08 +0200
    • Engine revision d5eff9c6a6
    • Dart version 2.18.0 (build 2.18.0-170.0.dev)
    • DevTools version 2.14.0

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-32, build-tools 31.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2021.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.67.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.42.0

[✓] Connected device (3 available)
    • sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 12 (API 31) (emulator)
    • macOS (desktop)             • macos         • darwin-arm64   • macOS 12.3.1 21E258 darwin-arm
    • Chrome (web)                • chrome        • web-javascript • Google Chrome 102.0.5005.61
    ! Error: Nexus is not connected. Xcode will continue when Nexus is connected. (code -13)

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

@danagbemava-nc danagbemava-nc added platform-ios iOS applications specifically engine flutter/engine repository. See also e: labels. c: rendering UI glitches reported at the engine/skia rendering level has reproducible steps The issue has been confirmed reproducible and is ready to work on found in release: 3.0 Found to occur in 3.0 found in release: 3.1 Found to occur in 3.1 and removed in triage Presently being triaged by the triage team labels Jun 9, 2022
@JsouLiang
Copy link
Contributor Author

JsouLiang commented Jun 9, 2022

I may know why.
The reasons may be the ImageFilterLayer AutoSaveLayer bounds problem:

void ImageFilterLayer::Paint(PaintContext& context) const {
  ....
 // This change `child_paint_bounds` to `paint_bounds`
  Layer::AutoSaveLayer save_layer =
      Layer::AutoSaveLayer::Create(context, paint_bounds(), &paint);
  PaintChildren(context);
}

This is behavior:

2022-06-09.17.24.55.mov

Hello Jim, do you the why use child_paint_bounds?
cc @flar

@zanderso zanderso added the P2 label Jun 9, 2022
@flar
Copy link
Contributor

flar commented Jun 9, 2022

I may know why. The reasons may be the ImageFilterLayer AutoSaveLayer bounds problem:

void ImageFilterLayer::Paint(PaintContext& context) const {
  ....
 // This change `child_paint_bounds` to `paint_bounds`
  Layer::AutoSaveLayer save_layer =
      Layer::AutoSaveLayer::Create(context, paint_bounds(), &paint);
  PaintChildren(context);
}

This is behavior:

2022-06-09.17.24.55.mov
Hello Jim, do you the why use child_paint_bounds? cc @flar

The filter is applied to the contents of the save layer, so it needs an input with those dimensions. paint_bounds is the expanded dimensions that include all of the output of the filter so it is usually too large.

This isn't just an optimization, consider applying a blur filter to a content that is filled with a rectangle. The edge conditions of the blur need to be applied to the edge of that rectangle so the filter input needs to be the size of that rectangle. If we used paint_bounds then the input would have transparent pixel padding around that rectangle and thus the edge conditions would apply to the transparent pixels rather than the rectangle background.

@flar
Copy link
Contributor

flar commented Jun 9, 2022

Is there an older version that doesn't have this issue? It could be something we've done with Diff bounds or various caching changes, but it could also be a Skia boundary condition issue.

If there is an older version that doesn't have the problem, bisecting might help pin down when it was added. Maybe try a version 4-6 months ago before we made a lot of recent changes to bounds code.

@jonahwilliams
Copy link
Member

Shouldn't the diff method of ImageFilterLayer (and ColorFilterLayer for that matter) update the transform of the diff context to the pixel snapped version?

OpacityLayer:

https://github.com/flutter/engine/blob/main/flow/layers/opacity_layer.cc#L25-L28

ImageFilterLayer:

https://github.com/flutter/engine/blob/main/flow/layers/image_filter_layer.cc#L14

@JsouLiang
Copy link
Contributor Author

JsouLiang commented Jun 10, 2022

I may know why. The reasons may be the ImageFilterLayer AutoSaveLayer bounds problem:

void ImageFilterLayer::Paint(PaintContext& context) const {
  ....
 // This change `child_paint_bounds` to `paint_bounds`
  Layer::AutoSaveLayer save_layer =
      Layer::AutoSaveLayer::Create(context, paint_bounds(), &paint);
  PaintChildren(context);
}

This is behavior:
2022-06-09.17.24.55.mov
Hello Jim, do you the why use child_paint_bounds? cc @flar

The filter is applied to the contents of the save layer, so it needs an input with those dimensions. paint_bounds is the expanded dimensions that include all of the output of the filter so it is usually too large.

This isn't just an optimization, consider applying a blur filter to a content that is filled with a rectangle. The edge conditions of the blur need to be applied to the edge of that rectangle so the filter input needs to be the size of that rectangle. If we used paint_bounds then the input would have transparent pixel padding around that rectangle and thus the edge conditions would apply to the transparent pixels rather than the rectangle background.

@flar hi, a question that if I use the ImageFilterLayer to Blur the red rect which picture behaves normally?

@JsouLiang
Copy link
Contributor Author

JsouLiang commented Jun 10, 2022

Shouldn't the diff method of ImageFilterLayer (and ColorFilterLayer for that matter) update the transform of the diff context to the pixel snapped version?

OpacityLayer:

https://github.com/flutter/engine/blob/main/flow/layers/opacity_layer.cc#L25-L28

ImageFilterLayer:

https://github.com/flutter/engine/blob/main/flow/layers/image_filter_layer.cc#L14

@jonahwilliams do you mean this:

void ImageFilterLayer::Diff(DiffContext* context, const Layer* old_layer) {
  ...
  #ifndef SUPPORT_FRACTIONAL_TRANSLATION
  context->SetTransform(
        RasterCache::GetIntegralTransCTM(context->GetTransform()));
  #endif
  DiffChildren(context, prev);
  ....
}

@flar
Copy link
Contributor

flar commented Jun 10, 2022

@flar hi, a question that if I use the ImageFilterLayer to Blur the red rect which picture behaves normally?

If this is the code in the original description then it should be just a large red rectangle because the default tile mode for the blur is clamp. The one on the right (oops) left looks like it was rendered with a decal tile mode.

Skia attempts to ensure proper edge conditions for blur filters, but they still have some bugs that sometimes crop up.

@flar
Copy link
Contributor

flar commented Jun 10, 2022

Shouldn't the diff method of ImageFilterLayer (and ColorFilterLayer for that matter) update the transform of the diff context to the pixel snapped version?

OpacityLayer:

https://github.com/flutter/engine/blob/main/flow/layers/opacity_layer.cc#L25-L28

ImageFilterLayer:

https://github.com/flutter/engine/blob/main/flow/layers/image_filter_layer.cc#L14

That sounds like an issue. Paging @knopp

@JsouLiang
Copy link
Contributor Author

JsouLiang commented Jun 10, 2022

@flar Yeah, you are right, the default blue tile mode is clamp.

@knopp
Copy link
Member

knopp commented Jun 10, 2022

There seem to be a few things that are causing this:

  • ImageFilterLayer::Preroll doesn't apply GetIntegralTransCTM before calling TryToPrepareRasterCache (see OpacityLayer).
  • ImageFilterLayer::Paint doesn't apply GetIntegralTransCTM before painting cached layer (also see OpacityLayer)
  • ImageFilterLayer::paint_bounds in Preroll is calculated subtly wrong (filter should be applied in device coordinates so that it is identical with filter bounds adjustment in ::Diff. But I think this one shouldn't cause rendering artifacts.
  • GetIntegralTransCTM seem to be missing in other layers that cache things (i.e. ColorFilterLayer, ShaderMaskLayer and TryToPrepareRasterCache).

@knopp
Copy link
Member

knopp commented Jun 10, 2022

Shouldn't the diff method of ImageFilterLayer (and ColorFilterLayer for that matter) update the transform of the diff context to the pixel snapped version?
OpacityLayer:
https://github.com/flutter/engine/blob/main/flow/layers/opacity_layer.cc#L25-L28
ImageFilterLayer:
https://github.com/flutter/engine/blob/main/flow/layers/image_filter_layer.cc#L14

That sounds like an issue. Paging @knopp

Not in ::Diff (ImageFilterLayer doesn't have any offset like OpacityLayer), but everywhere else (before TryToPrepareRasterCache and painting) the matrix needs to be adjusted to pixel snapped version.

@flar
Copy link
Contributor

flar commented Jun 10, 2022

I think the Integral CTM probably matters more in cases where there might be a 1:1 pixel blit (opacity, dl_picture, sk_picture) but ImageFilter won't be blitting the children, it will be using them as an input. If a layer is caching itself it probably matters more as that cache draw won't apply any filters (potentially just an alpha in the case of OpacityLayer).

@knopp
Copy link
Member

knopp commented Jun 10, 2022

I think the Integral CTM probably matters more in cases where there might be a 1:1 pixel blit (opacity, dl_picture, sk_picture) but ImageFilter won't be blitting the children, it will be using them as an input. If a layer is caching itself it probably matters more as that cache draw won't apply any filters (potentially just an alpha in the case of OpacityLayer).

The issue is that DiffContext takes integral CTM into account when computing bounds of each layer. The coordinates calculated in Diff must match exactly coordinates where layer will be painted. That means using integral CTM in TryToPrepareRasterCache and Integral CTM before painting cached layer. If we don't do this the layer will be painted on slightly different position when using raster cache as it would be without, which causes these artefacts.

You can verify this by

  • Disabling raster cache
  • Snapping the matrix to integer in Preroll and Paint like in OpacityLayer

Any of these will fix the issue. I'll work on a PR.

@flar
Copy link
Contributor

flar commented Jun 11, 2022

I think the Integral CTM probably matters more in cases where there might be a 1:1 pixel blit (opacity, dl_picture, sk_picture) but ImageFilter won't be blitting the children, it will be using them as an input. If a layer is caching itself it probably matters more as that cache draw won't apply any filters (potentially just an alpha in the case of OpacityLayer).

The issue is that DiffContext takes integral CTM into account when computing bounds of each layer. The coordinates calculated in Diff must match exactly coordinates where layer will be painted. That means using integral CTM in TryToPrepareRasterCache and Integral CTM before painting cached layer. If we don't do this the layer will be painted on slightly different position when using raster cache as it would be without, which causes these artefacts.

You can verify this by

  • Disabling raster cache
  • Snapping the matrix to integer in Preroll and Paint like in OpacityLayer

Any of these will fix the issue. I'll work on a PR.

My point was not about bounds agreement between the passes, I was saying that we care about exact pixel alignment in cache::draw otherwise we can leave a fair bit of performance on the table on lower end devices.

Integer CTM was created to help with that performance consideration. The headaches about how to get all of the bounds in all of the passes to agree are just side effects of having to use this performance trick.

Having said that, I'm not sure anyone has verified if the performance still matters (and there are potentially other ways to achieve the "pixel for pixel blit" without having to slam all of the CTMs).

OpacityLayer ends up with a cache entry that can match pixels from the cache image to the destination surface corner-by-corner center-by-center exactly and so it needs the Integer CTM to ensure performance.

Layers that mostly are used for blur operations only use the cache entry as an input to a filter and so they never measure the pixels of the cache entry against the pixels on the destination because there is a complicated filtering system that is in the way which will dice and slice the pixels anyway.

Thus, Integer CTM mangling matters to OpacityLayer in terms of performance. But it doesn't so much matter to the other layers.

Having said that, once we've chosen that a particular layer cares about Integer CTM, then we need to make sure it matches across the various passes and methods on the layer.

@flar
Copy link
Contributor

flar commented Jun 11, 2022

I just noticed that Dl_layer and SkP_layer also use it just for rendering. I think that was a boon for trying to avoid AA rectangles all over the place, but I'm not sure again how much that matters. So, DL/SkP layers also inherit that practice for their cached blits should they be cached.

@JsouLiang
Copy link
Contributor Author

I think the Integral CTM probably matters more in cases where there might be a 1:1 pixel blit (opacity, dl_picture, sk_picture) but ImageFilter won't be blitting the children, it will be using them as an input. If a layer is caching itself it probably matters more as that cache draw won't apply any filters (potentially just an alpha in the case of OpacityLayer).

The issue is that DiffContext takes integral CTM into account when computing bounds of each layer. The coordinates calculated in Diff must match exactly coordinates where layer will be painted. That means using integral CTM in TryToPrepareRasterCache and Integral CTM before painting cached layer. If we don't do this the layer will be painted on slightly different position when using raster cache as it would be without, which causes these artefacts.

You can verify this by

  • Disabling raster cache
  • Snapping the matrix to integer in Preroll and Paint like in OpacityLayer

Any of these will fix the issue. I'll work on a PR.

I try to use the Integer CTM to fix the issue. And then this scenario is normal.

2022-06-11.19.33.47.mov

@flar
Copy link
Contributor

flar commented Jun 13, 2022

I disagree with the solution. Whereas caching can vary the result subtly from uncached rendering, that can be fixed in ways other than slamming all CTMs to integer translations.

@knopp
Copy link
Member

knopp commented Jun 13, 2022

But raster cache already does that. Every time you paint raster cached layer the translation is already slammed to integer. The only difference being is that when painting raster cached layer the integer coordinates are determined through SkRect::roundOut (RasterCache::GetDeviceBounds), whether as RasterCache::GetIntegralTransCTM uses SkScalarRoundToScalar.

@knopp
Copy link
Member

knopp commented Jun 13, 2022

Also if you don't raster snap before rasterizing layer you'll get subtly different cached layer depending on current scroll offset for example.

Don't get me wrong, I'm all for removing the GetIntegralTransCTM altogether, it just may need some rethinking of how raster cache paints.

@flar
Copy link
Contributor

flar commented Jun 13, 2022

(Conversation was continued on flutter/engine#33981)

@danagbemava-nc danagbemava-nc added r: fixed Issue is closed as already fixed in a newer version and removed waiting for PR to land (fixed) A fix is in flight labels Jun 14, 2022
@github-actions
Copy link

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.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 28, 2022
@flutter-triage-bot flutter-triage-bot bot added P0 Critical issues such as a build break or regression and removed P2 labels Jun 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
c: rendering UI glitches reported at the engine/skia rendering level engine flutter/engine repository. See also e: labels. found in release: 3.0 Found to occur in 3.0 found in release: 3.1 Found to occur in 3.1 has reproducible steps The issue has been confirmed reproducible and is ready to work on P0 Critical issues such as a build break or regression platform-ios iOS applications specifically r: fixed Issue is closed as already fixed in a newer version
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants