From 68f1d69cf37b52779293e01903c2a49f235904f1 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:29:19 -0800 Subject: [PATCH 1/2] [Impeller] moved tests to aiks_blur_unittests, added warning (#51274) issue: https://github.com/flutter/flutter/issues/140106 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat --- impeller/aiks/aiks_blur_unittests.cc | 183 +++++++++++++++++++++++++ impeller/aiks/aiks_unittests.cc | 193 ++------------------------- 2 files changed, 193 insertions(+), 183 deletions(-) diff --git a/impeller/aiks/aiks_blur_unittests.cc b/impeller/aiks/aiks_blur_unittests.cc index d6ad5ebe41884..44d05aa237ebe 100644 --- a/impeller/aiks/aiks_blur_unittests.cc +++ b/impeller/aiks/aiks_blur_unittests.cc @@ -5,6 +5,7 @@ #include "flutter/impeller/aiks/aiks_unittests.h" #include "impeller/aiks/canvas.h" +#include "impeller/entity/contents/filters/gaussian_blur_filter_contents.h" #include "impeller/entity/render_target_cache.h" #include "impeller/geometry/path_builder.h" #include "impeller/playground/widgets.h" @@ -672,5 +673,187 @@ TEST_P(AiksTest, GaussianBlurStyleSolid) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, GuassianBlurUpdatesMipmapContents) { + // This makes sure if mip maps are recycled across invocations of blurs the + // contents get updated each frame correctly. If they aren't updated the color + // inside the blur and outside the blur will be different. + // + // If there is some change to render target caching this could display a false + // positive in the future. Also, if the LOD that is rendered is 1 it could + // present a false positive. + int32_t count = 0; + auto callback = [&](AiksContext& renderer) -> std::optional { + Canvas canvas; + if (count++ == 0) { + canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); + } else { + canvas.DrawCircle({100, 100}, 50, {.color = Color::Chartreuse()}); + } + canvas.ClipRRect(Rect::MakeLTRB(75, 50, 375, 275), {20, 20}); + canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt, + ImageFilter::MakeBlur(Sigma(30.0), Sigma(30.0), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)); + canvas.Restore(); + return canvas.EndRecordingAsPicture(); + }; + + ASSERT_TRUE(OpenPlaygroundHere(callback)); +} + +TEST_P(AiksTest, GaussianBlurSetsMipCountOnPass) { + Canvas canvas; + canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); + canvas.SaveLayer({}, std::nullopt, + ImageFilter::MakeBlur(Sigma(3), Sigma(3), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)); + canvas.Restore(); + + Picture picture = canvas.EndRecordingAsPicture(); + EXPECT_EQ(4, picture.pass->GetRequiredMipCount()); +} + +TEST_P(AiksTest, GaussianBlurAllocatesCorrectMipCountRenderTarget) { + size_t blur_required_mip_count = + GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; + + Canvas canvas; + canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); + canvas.SaveLayer({}, std::nullopt, + ImageFilter::MakeBlur(Sigma(3), Sigma(3), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)); + canvas.Restore(); + + Picture picture = canvas.EndRecordingAsPicture(); + std::shared_ptr cache = + std::make_shared(GetContext()->GetResourceAllocator()); + AiksContext aiks_context(GetContext(), nullptr, cache); + picture.ToImage(aiks_context, {100, 100}); + + size_t max_mip_count = 0; + for (auto it = cache->GetRenderTargetDataBegin(); + it != cache->GetRenderTargetDataEnd(); ++it) { + max_mip_count = std::max(it->config.mip_count, max_mip_count); + } + EXPECT_EQ(max_mip_count, blur_required_mip_count); +} + +TEST_P(AiksTest, GaussianBlurMipMapNestedLayer) { + fml::testing::LogCapture log_capture; + size_t blur_required_mip_count = + GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; + + Canvas canvas; + canvas.DrawPaint({.color = Color::Wheat()}); + canvas.SaveLayer({.blend_mode = BlendMode::kMultiply}); + canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); + canvas.SaveLayer({}, std::nullopt, + ImageFilter::MakeBlur(Sigma(30), Sigma(30), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)); + canvas.DrawCircle({200, 200}, 50, {.color = Color::Chartreuse()}); + + Picture picture = canvas.EndRecordingAsPicture(); + std::shared_ptr cache = + std::make_shared(GetContext()->GetResourceAllocator()); + AiksContext aiks_context(GetContext(), nullptr, cache); + picture.ToImage(aiks_context, {100, 100}); + + size_t max_mip_count = 0; + for (auto it = cache->GetRenderTargetDataBegin(); + it != cache->GetRenderTargetDataEnd(); ++it) { + max_mip_count = std::max(it->config.mip_count, max_mip_count); + } + EXPECT_EQ(max_mip_count, blur_required_mip_count); + // The log is FML_DLOG, so only check in debug builds. +#ifndef NDEBUG + if (GetParam() != PlaygroundBackend::kOpenGLES) { + EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } else { + EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } +#endif +} + +TEST_P(AiksTest, GaussianBlurMipMapImageFilter) { + size_t blur_required_mip_count = + GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; + fml::testing::LogCapture log_capture; + Canvas canvas; + canvas.SaveLayer( + {.image_filter = ImageFilter::MakeBlur(Sigma(30), Sigma(30), + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)}); + canvas.DrawCircle({200, 200}, 50, {.color = Color::Chartreuse()}); + + Picture picture = canvas.EndRecordingAsPicture(); + std::shared_ptr cache = + std::make_shared(GetContext()->GetResourceAllocator()); + AiksContext aiks_context(GetContext(), nullptr, cache); + picture.ToImage(aiks_context, {1024, 768}); + + size_t max_mip_count = 0; + for (auto it = cache->GetRenderTargetDataBegin(); + it != cache->GetRenderTargetDataEnd(); ++it) { + max_mip_count = std::max(it->config.mip_count, max_mip_count); + } + EXPECT_EQ(max_mip_count, blur_required_mip_count); + // The log is FML_DLOG, so only check in debug builds. +#ifndef NDEBUG + if (GetParam() != PlaygroundBackend::kOpenGLES) { + EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } else { + EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } +#endif +} + +TEST_P(AiksTest, GaussianBlurMipMapSolidColor) { + size_t blur_required_mip_count = + GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; + fml::testing::LogCapture log_capture; + Canvas canvas; + canvas.DrawPath(PathBuilder{} + .MoveTo({100, 100}) + .LineTo({200, 100}) + .LineTo({150, 200}) + .LineTo({50, 200}) + .Close() + .TakePath(), + {.color = Color::Chartreuse(), + .image_filter = ImageFilter::MakeBlur( + Sigma(30), Sigma(30), FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)}); + + Picture picture = canvas.EndRecordingAsPicture(); + std::shared_ptr cache = + std::make_shared(GetContext()->GetResourceAllocator()); + AiksContext aiks_context(GetContext(), nullptr, cache); + picture.ToImage(aiks_context, {1024, 768}); + + size_t max_mip_count = 0; + for (auto it = cache->GetRenderTargetDataBegin(); + it != cache->GetRenderTargetDataEnd(); ++it) { + max_mip_count = std::max(it->config.mip_count, max_mip_count); + } + EXPECT_EQ(max_mip_count, blur_required_mip_count); + // The log is FML_DLOG, so only check in debug builds. +#ifndef NDEBUG + if (GetParam() != PlaygroundBackend::kOpenGLES) { + EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } else { + EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), + std::string::npos); + } +#endif +} + } // namespace testing } // namespace impeller diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 7a6328fdb9e8e..3c3ccd1dc4c08 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -21,7 +21,6 @@ #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/aiks/testing/context_spy.h" #include "impeller/core/capture.h" -#include "impeller/entity/contents/filters/gaussian_blur_filter_contents.h" #include "impeller/entity/contents/solid_color_contents.h" #include "impeller/entity/render_target_cache.h" #include "impeller/geometry/color.h" @@ -3288,188 +3287,6 @@ TEST_P(AiksTest, SubpassWithClearColorOptimization) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, GuassianBlurUpdatesMipmapContents) { - // This makes sure if mip maps are recycled across invocations of blurs the - // contents get updated each frame correctly. If they aren't updated the color - // inside the blur and outside the blur will be different. - // - // If there is some change to render target caching this could display a false - // positive in the future. Also, if the LOD that is rendered is 1 it could - // present a false positive. - int32_t count = 0; - auto callback = [&](AiksContext& renderer) -> std::optional { - Canvas canvas; - if (count++ == 0) { - canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); - } else { - canvas.DrawCircle({100, 100}, 50, {.color = Color::Chartreuse()}); - } - canvas.ClipRRect(Rect::MakeLTRB(75, 50, 375, 275), {20, 20}); - canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt, - ImageFilter::MakeBlur(Sigma(30.0), Sigma(30.0), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)); - canvas.Restore(); - return canvas.EndRecordingAsPicture(); - }; - - ASSERT_TRUE(OpenPlaygroundHere(callback)); -} - -TEST_P(AiksTest, GaussianBlurSetsMipCountOnPass) { - Canvas canvas; - canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); - canvas.SaveLayer({}, std::nullopt, - ImageFilter::MakeBlur(Sigma(3), Sigma(3), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)); - canvas.Restore(); - - Picture picture = canvas.EndRecordingAsPicture(); - EXPECT_EQ(4, picture.pass->GetRequiredMipCount()); -} - -TEST_P(AiksTest, GaussianBlurAllocatesCorrectMipCountRenderTarget) { - size_t blur_required_mip_count = - GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; - - Canvas canvas; - canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); - canvas.SaveLayer({}, std::nullopt, - ImageFilter::MakeBlur(Sigma(3), Sigma(3), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)); - canvas.Restore(); - - Picture picture = canvas.EndRecordingAsPicture(); - std::shared_ptr cache = - std::make_shared(GetContext()->GetResourceAllocator()); - AiksContext aiks_context(GetContext(), nullptr, cache); - picture.ToImage(aiks_context, {100, 100}); - - size_t max_mip_count = 0; - for (auto it = cache->GetRenderTargetDataBegin(); - it != cache->GetRenderTargetDataEnd(); ++it) { - max_mip_count = std::max(it->config.mip_count, max_mip_count); - } - EXPECT_EQ(max_mip_count, blur_required_mip_count); -} - -TEST_P(AiksTest, GaussianBlurMipMapNestedLayer) { - fml::testing::LogCapture log_capture; - size_t blur_required_mip_count = - GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; - - Canvas canvas; - canvas.DrawPaint({.color = Color::Wheat()}); - canvas.SaveLayer({.blend_mode = BlendMode::kMultiply}); - canvas.DrawCircle({100, 100}, 50, {.color = Color::CornflowerBlue()}); - canvas.SaveLayer({}, std::nullopt, - ImageFilter::MakeBlur(Sigma(30), Sigma(30), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)); - canvas.DrawCircle({200, 200}, 50, {.color = Color::Chartreuse()}); - - Picture picture = canvas.EndRecordingAsPicture(); - std::shared_ptr cache = - std::make_shared(GetContext()->GetResourceAllocator()); - AiksContext aiks_context(GetContext(), nullptr, cache); - picture.ToImage(aiks_context, {100, 100}); - - size_t max_mip_count = 0; - for (auto it = cache->GetRenderTargetDataBegin(); - it != cache->GetRenderTargetDataEnd(); ++it) { - max_mip_count = std::max(it->config.mip_count, max_mip_count); - } - EXPECT_EQ(max_mip_count, blur_required_mip_count); - // The log is FML_DLOG, so only check in debug builds. -#ifndef NDEBUG - if (GetParam() != PlaygroundBackend::kOpenGLES) { - EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } else { - EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } -#endif -} - -TEST_P(AiksTest, GaussianBlurMipMapImageFilter) { - size_t blur_required_mip_count = - GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; - fml::testing::LogCapture log_capture; - Canvas canvas; - canvas.SaveLayer( - {.image_filter = ImageFilter::MakeBlur(Sigma(30), Sigma(30), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)}); - canvas.DrawCircle({200, 200}, 50, {.color = Color::Chartreuse()}); - - Picture picture = canvas.EndRecordingAsPicture(); - std::shared_ptr cache = - std::make_shared(GetContext()->GetResourceAllocator()); - AiksContext aiks_context(GetContext(), nullptr, cache); - picture.ToImage(aiks_context, {1024, 768}); - - size_t max_mip_count = 0; - for (auto it = cache->GetRenderTargetDataBegin(); - it != cache->GetRenderTargetDataEnd(); ++it) { - max_mip_count = std::max(it->config.mip_count, max_mip_count); - } - EXPECT_EQ(max_mip_count, blur_required_mip_count); - // The log is FML_DLOG, so only check in debug builds. -#ifndef NDEBUG - if (GetParam() != PlaygroundBackend::kOpenGLES) { - EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } else { - EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } -#endif -} - -TEST_P(AiksTest, GaussianBlurMipMapSolidColor) { - size_t blur_required_mip_count = - GetParam() == PlaygroundBackend::kOpenGLES ? 1 : 4; - fml::testing::LogCapture log_capture; - Canvas canvas; - canvas.DrawPath(PathBuilder{} - .MoveTo({100, 100}) - .LineTo({200, 100}) - .LineTo({150, 200}) - .LineTo({50, 200}) - .Close() - .TakePath(), - {.color = Color::Chartreuse(), - .image_filter = ImageFilter::MakeBlur( - Sigma(30), Sigma(30), FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp)}); - - Picture picture = canvas.EndRecordingAsPicture(); - std::shared_ptr cache = - std::make_shared(GetContext()->GetResourceAllocator()); - AiksContext aiks_context(GetContext(), nullptr, cache); - picture.ToImage(aiks_context, {1024, 768}); - - size_t max_mip_count = 0; - for (auto it = cache->GetRenderTargetDataBegin(); - it != cache->GetRenderTargetDataEnd(); ++it) { - max_mip_count = std::max(it->config.mip_count, max_mip_count); - } - EXPECT_EQ(max_mip_count, blur_required_mip_count); - // The log is FML_DLOG, so only check in debug builds. -#ifndef NDEBUG - if (GetParam() != PlaygroundBackend::kOpenGLES) { - EXPECT_EQ(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } else { - EXPECT_NE(log_capture.str().find(GaussianBlurFilterContents::kNoMipsError), - std::string::npos); - } -#endif -} - TEST_P(AiksTest, ImageColorSourceEffectTransform) { // Compare with https://fiddle.skia.org/c/6cdc5aefb291fda3833b806ca347a885 @@ -3583,3 +3400,13 @@ TEST_P(AiksTest, EntityPassClipRecorderRestoresCancelOutClips) { } // namespace testing } // namespace impeller + +// █████████████████████████████████████████████████████████████████████████████ +// █ NOTICE: Before adding new tests to this file consider adding it to one of +// █ the subdivisions of AiksTest to avoid having one massive file. +// █ +// █ Subdivisions: +// █ - aiks_blur_unittests.cc +// █ - aiks_gradient_unittests.cc +// █ - aiks_path_unittests.cc +// █████████████████████████████████████████████████████████████████████████████ From bc4abcda6357c2d83a1aaee5fbe14548fcf59626 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 7 Mar 2024 17:36:09 -0800 Subject: [PATCH 2/2] [Impeller] Add mask blur style support to the RRect blur fast path. (#51250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves performance by avoiding the 2-pass Gaussian blur in more shadow drawing situations. The new golden also serves as a good reference for how mask blurs are supposed to work alongside various other paint properties such as color filters, color opacity, image filters, and blending. ![Screenshot 2024-03-06 at 11 47 04 PM](https://github.com/flutter/engine/assets/919017/dee1b56c-d2e4-40bd-9f50-aeba0cfd69f2) The top 5 shapes are various RRect cases and are rendering correctly via the new blur style implementation in this patch. The two bottom rows (the triangles and arcs) are non-rrect paths, so they're falling back to rendering using the 2-pass Gaussian blur. Rendering errors are circled in red below: ![Screenshot 2024-03-06 at 11 47 04 PM](https://github.com/flutter/engine/assets/919017/5ac27d71-95e6-4e6e-99d8-7436476e1aee) * Cases 1, 2, 7, and 9 all appear to rendering fine. * Cases 3, 4, 5, and 6 all have mask blur styles set to `BlurStyle::kSolid`. After the first clipped overlay has been drawn, subsequent clipped overlays aren't drawing. * Case 6 is also has the blend mode set to `BlendMode::kExclusion`. * Cases 8 and 10 are rendering with `BlurStyle::kInner` and `BlurStyle::kOuter` respectfully, but with a blur ImageFilter also set on the paint state. The ImageFilter needs to be applied to the rasterized mask blurred content. --- impeller/aiks/aiks_blur_unittests.cc | 166 +++++++++++++++++++++++ impeller/aiks/canvas.cc | 110 ++++++++++++--- impeller/aiks/canvas.h | 2 +- testing/impeller_golden_tests_output.txt | 30 ++++ 4 files changed, 291 insertions(+), 17 deletions(-) diff --git a/impeller/aiks/aiks_blur_unittests.cc b/impeller/aiks/aiks_blur_unittests.cc index 44d05aa237ebe..b808d4fa3366c 100644 --- a/impeller/aiks/aiks_blur_unittests.cc +++ b/impeller/aiks/aiks_blur_unittests.cc @@ -282,6 +282,172 @@ TEST_P(AiksTest, MaskBlurWithZeroSigmaIsSkipped) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +struct MaskBlurTestConfig { + FilterContents::BlurStyle style = FilterContents::BlurStyle::kNormal; + Scalar sigma = 1.0f; + Scalar alpha = 1.0f; + std::shared_ptr image_filter; + bool invert_colors = false; + BlendMode blend_mode = BlendMode::kSourceOver; +}; + +static Picture MaskBlurVariantTest(const AiksTest& test_context, + const MaskBlurTestConfig& config) { + Canvas canvas; + canvas.Scale(test_context.GetContentScale()); + canvas.Scale(Vector2{0.8f, 0.8f}); + Paint paint; + paint.mask_blur_descriptor = Paint::MaskBlurDescriptor{ + .style = FilterContents::BlurStyle::kNormal, + .sigma = Sigma{1}, + }; + + canvas.DrawPaint({.color = Color::AntiqueWhite()}); + + paint.mask_blur_descriptor->style = config.style; + paint.mask_blur_descriptor->sigma = Sigma{config.sigma}; + paint.image_filter = config.image_filter; + paint.invert_colors = config.invert_colors; + paint.blend_mode = config.blend_mode; + + const Scalar x = 50; + const Scalar radius = 20.0f; + const Scalar y_spacing = 100.0f; + + Scalar y = 50; + paint.color = Color::Crimson().WithAlpha(config.alpha); + canvas.DrawRect(Rect::MakeXYWH(x + 25 - radius / 2, y + radius / 2, // + radius, 60.0f - radius), + paint); + + y += y_spacing; + paint.color = Color::Blue().WithAlpha(config.alpha); + canvas.DrawCircle({x + 25, y + 25}, radius, paint); + + y += y_spacing; + paint.color = Color::Green().WithAlpha(config.alpha); + canvas.DrawOval(Rect::MakeXYWH(x + 25 - radius / 2, y + radius / 2, // + radius, 60.0f - radius), + paint); + + y += y_spacing; + paint.color = Color::Purple().WithAlpha(config.alpha); + canvas.DrawRRect(Rect::MakeXYWH(x, y, 60.0f, 60.0f), // + {radius, radius}, // + paint); + + y += y_spacing; + paint.color = Color::Orange().WithAlpha(config.alpha); + canvas.DrawRRect(Rect::MakeXYWH(x, y, 60.0f, 60.0f), // + {radius, 5.0f}, paint); + + y += y_spacing; + paint.color = Color::Maroon().WithAlpha(config.alpha); + canvas.DrawPath(PathBuilder{} + .MoveTo({x + 0, y + 60}) + .LineTo({x + 30, y + 0}) + .LineTo({x + 60, y + 60}) + .Close() + .TakePath(), + paint); + + y += y_spacing; + paint.color = Color::Maroon().WithAlpha(config.alpha); + canvas.DrawPath(PathBuilder{} + .AddArc(Rect::MakeXYWH(x + 5, y, 50, 50), + Radians{kPi / 2}, Radians{kPi}) + .AddArc(Rect::MakeXYWH(x + 25, y, 50, 50), + Radians{kPi / 2}, Radians{kPi}) + .Close() + .TakePath(), + paint); + + return canvas.EndRecordingAsPicture(); +} + +static const std::map kPaintVariations = { + // 1. Normal style, translucent, zero sigma. + {"NormalTranslucentZeroSigma", + {.style = FilterContents::BlurStyle::kNormal, + .sigma = 0.0f, + .alpha = 0.5f}}, + // 2. Normal style, translucent. + {"NormalTranslucent", + {.style = FilterContents::BlurStyle::kNormal, + .sigma = 8.0f, + .alpha = 0.5f}}, + // 3. Solid style, translucent. + {"SolidTranslucent", + {.style = FilterContents::BlurStyle::kSolid, + .sigma = 8.0f, + .alpha = 0.5f}}, + // 4. Solid style, opaque. + {"SolidOpaque", + {.style = FilterContents::BlurStyle::kSolid, .sigma = 8.0f}}, + // 5. Solid style, translucent, color & image filtered. + {"SolidTranslucentWithFilters", + {.style = FilterContents::BlurStyle::kSolid, + .sigma = 8.0f, + .alpha = 0.5f, + .image_filter = ImageFilter::MakeBlur(Sigma{3}, + Sigma{3}, + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp), + .invert_colors = true}}, + // 6. Solid style, translucent, exclusion blended. + {"SolidTranslucentExclusionBlend", + {.style = FilterContents::BlurStyle::kSolid, + .sigma = 8.0f, + .alpha = 0.5f, + .blend_mode = BlendMode::kExclusion}}, + // 7. Inner style, translucent. + {"InnerTranslucent", + {.style = FilterContents::BlurStyle::kInner, + .sigma = 8.0f, + .alpha = 0.5f}}, + // 8. Inner style, translucent, blurred. + {"InnerTranslucentWithBlurImageFilter", + {.style = FilterContents::BlurStyle::kInner, + .sigma = 8.0f, + .alpha = 0.5f, + .image_filter = ImageFilter::MakeBlur(Sigma{3}, + Sigma{3}, + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)}}, + // 9. Outer style, translucent. + {"OuterTranslucent", + {.style = FilterContents::BlurStyle::kOuter, + .sigma = 8.0f, + .alpha = 0.5f}}, + // 10. Outer style, opaque, image filtered. + {"OuterOpaqueWithBlurImageFilter", + {.style = FilterContents::BlurStyle::kOuter, + .sigma = 8.0f, + .image_filter = ImageFilter::MakeBlur(Sigma{3}, + Sigma{3}, + FilterContents::BlurStyle::kNormal, + Entity::TileMode::kClamp)}}, +}; + +#define MASK_BLUR_VARIANT_TEST(config) \ + TEST_P(AiksTest, MaskBlurVariantTest##config) { \ + ASSERT_TRUE(OpenPlaygroundHere( \ + MaskBlurVariantTest(*this, kPaintVariations.at(#config)))); \ + } + +MASK_BLUR_VARIANT_TEST(NormalTranslucentZeroSigma) +MASK_BLUR_VARIANT_TEST(NormalTranslucent) +MASK_BLUR_VARIANT_TEST(SolidTranslucent) +MASK_BLUR_VARIANT_TEST(SolidOpaque) +MASK_BLUR_VARIANT_TEST(SolidTranslucentWithFilters) +MASK_BLUR_VARIANT_TEST(SolidTranslucentExclusionBlend) +MASK_BLUR_VARIANT_TEST(InnerTranslucent) +MASK_BLUR_VARIANT_TEST(InnerTranslucentWithBlurImageFilter) +MASK_BLUR_VARIANT_TEST(OuterTranslucent) +MASK_BLUR_VARIANT_TEST(OuterOpaqueWithBlurImageFilter) + +#undef MASK_BLUR_VARIANT_TEST + TEST_P(AiksTest, GaussianBlurAtPeripheryVertical) { Canvas canvas; diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index 2fbfe2b2394d5..5a0a86faebbf2 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -308,41 +308,119 @@ void Canvas::DrawPaint(const Paint& paint) { } bool Canvas::AttemptDrawBlurredRRect(const Rect& rect, - Size corner_radius, + Size corner_radii, const Paint& paint) { if (paint.color_source.GetType() != ColorSource::Type::kColor || paint.style != Paint::Style::kFill) { return false; } - if (!paint.mask_blur_descriptor.has_value() || - paint.mask_blur_descriptor->style != FilterContents::BlurStyle::kNormal) { + if (!paint.mask_blur_descriptor.has_value()) { return false; } + // A blur sigma that is not positive enough should not result in a blur. if (paint.mask_blur_descriptor->sigma.sigma <= kEhCloseEnough) { return false; } - Paint new_paint = paint; - // For symmetrically mask blurred solid RRects, absorb the mask blur and use // a faster SDF approximation. - auto contents = std::make_shared(); - contents->SetColor(new_paint.color); - contents->SetSigma(new_paint.mask_blur_descriptor->sigma); - contents->SetRRect(rect, corner_radius); + Color rrect_color = + paint.HasColorFilter() + // Absorb the color filter, if any. + ? paint.GetColorFilter()->GetCPUColorFilterProc()(paint.color) + : paint.color; + + Paint rrect_paint = {.mask_blur_descriptor = paint.mask_blur_descriptor}; + + // In some cases, we need to render the mask blur to a separate layer. + // + // 1. If the blur style is normal, we'll be drawing using one draw call and + // no clips. And so we can just wrap the RRect contents with the + // ImageFilter, which will get applied to the result as per usual. + // + // 2. If the blur style is solid, we combine the non-blurred RRect with the + // blurred RRect via two separate draw calls, and so we need to defer any + // fancy blending, translucency, or image filtering until after these two + // draws have been combined in a separate layer. + // + // 3. If the blur style is outer or inner, we apply the blur style via a + // clip. The ImageFilter needs to be applied to the mask blurred result. + // And so if there's an ImageFilter, we need to defer applying it until + // after the clipped RRect blur has been drawn to a separate texture. + // However, since there's only one draw call that produces color, we + // don't need to worry about the blend mode or translucency (unlike with + // BlurStyle::kSolid). + // + if ((paint.mask_blur_descriptor->style != + FilterContents::BlurStyle::kNormal && + paint.image_filter) || + (paint.mask_blur_descriptor->style == FilterContents::BlurStyle::kSolid && + (!rrect_color.IsOpaque() || + paint.blend_mode != BlendMode::kSourceOver))) { + // Defer the alpha, blend mode, and image filter to a separate layer. + SaveLayer({.color = Color::White().WithAlpha(rrect_color.alpha), + .blend_mode = paint.blend_mode, + .image_filter = paint.image_filter}); + rrect_paint.color = rrect_color.WithAlpha(1); + } else { + rrect_paint.color = rrect_color; + rrect_paint.blend_mode = paint.blend_mode; + rrect_paint.image_filter = paint.image_filter; + Save(); + } - new_paint.mask_blur_descriptor = std::nullopt; + auto draw_blurred_rrect = [this, &rect, &corner_radii, &rrect_paint]() { + auto contents = std::make_shared(); - Entity entity; - entity.SetTransform(GetCurrentTransform()); - entity.SetClipDepth(GetClipDepth()); - entity.SetBlendMode(new_paint.blend_mode); - entity.SetContents(new_paint.WithFilters(std::move(contents))); + contents->SetColor(rrect_paint.color); + contents->SetSigma(rrect_paint.mask_blur_descriptor->sigma); + contents->SetRRect(rect, corner_radii); - AddEntityToCurrentPass(std::move(entity)); + Entity blurred_rrect_entity; + blurred_rrect_entity.SetTransform(GetCurrentTransform()); + blurred_rrect_entity.SetClipDepth(GetClipDepth()); + blurred_rrect_entity.SetBlendMode(rrect_paint.blend_mode); + + rrect_paint.mask_blur_descriptor = std::nullopt; + blurred_rrect_entity.SetContents( + rrect_paint.WithFilters(std::move(contents))); + AddEntityToCurrentPass(std::move(blurred_rrect_entity)); + }; + + switch (rrect_paint.mask_blur_descriptor->style) { + case FilterContents::BlurStyle::kNormal: { + draw_blurred_rrect(); + break; + } + case FilterContents::BlurStyle::kSolid: { + // First, draw the blurred RRect. + draw_blurred_rrect(); + // Then, draw the non-blurred RRect on top. + Entity entity; + entity.SetTransform(GetCurrentTransform()); + entity.SetClipDepth(GetClipDepth()); + entity.SetBlendMode(rrect_paint.blend_mode); + entity.SetContents(CreateContentsForGeometryWithFilters( + rrect_paint, Geometry::MakeRoundRect(rect, corner_radii))); + AddEntityToCurrentPass(std::move(entity)); + break; + } + case FilterContents::BlurStyle::kOuter: { + ClipRRect(rect, corner_radii, Entity::ClipOperation::kDifference); + draw_blurred_rrect(); + break; + } + case FilterContents::BlurStyle::kInner: { + ClipRRect(rect, corner_radii, Entity::ClipOperation::kIntersect); + draw_blurred_rrect(); + break; + } + } + + Restore(); return true; } diff --git a/impeller/aiks/canvas.h b/impeller/aiks/canvas.h index b4ba0838bec8d..867c5b8535069 100644 --- a/impeller/aiks/canvas.h +++ b/impeller/aiks/canvas.h @@ -207,7 +207,7 @@ class Canvas { void RestoreClip(); bool AttemptDrawBlurredRRect(const Rect& rect, - Size corner_radius, + Size corner_radii, const Paint& paint); Canvas(const Canvas&) = delete; diff --git a/testing/impeller_golden_tests_output.txt b/testing/impeller_golden_tests_output.txt index 26f73d04bad0a..0a6c28e5bb62d 100644 --- a/testing/impeller_golden_tests_output.txt +++ b/testing/impeller_golden_tests_output.txt @@ -516,6 +516,36 @@ impeller_Play_AiksTest_ImageFilteredUnboundedSaveLayerWithUnboundedContents_Vulk impeller_Play_AiksTest_LinearToSrgbFilterSubpassCollapseOptimization_Metal.png impeller_Play_AiksTest_LinearToSrgbFilterSubpassCollapseOptimization_OpenGLES.png impeller_Play_AiksTest_LinearToSrgbFilterSubpassCollapseOptimization_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucentWithBlurImageFilter_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucentWithBlurImageFilter_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucentWithBlurImageFilter_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucent_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucent_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestInnerTranslucent_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucentZeroSigma_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucentZeroSigma_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucentZeroSigma_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucent_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucent_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestNormalTranslucent_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterOpaqueWithBlurImageFilter_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterOpaqueWithBlurImageFilter_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterOpaqueWithBlurImageFilter_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterTranslucent_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterTranslucent_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestOuterTranslucent_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidOpaque_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidOpaque_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidOpaque_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentExclusionBlend_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentExclusionBlend_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentExclusionBlend_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentWithFilters_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentWithFilters_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucentWithFilters_Vulkan.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucent_Metal.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucent_OpenGLES.png +impeller_Play_AiksTest_MaskBlurVariantTestSolidTranslucent_Vulkan.png impeller_Play_AiksTest_MaskBlurWithZeroSigmaIsSkipped_Metal.png impeller_Play_AiksTest_MaskBlurWithZeroSigmaIsSkipped_OpenGLES.png impeller_Play_AiksTest_MaskBlurWithZeroSigmaIsSkipped_Vulkan.png