Skip to content

Commit

Permalink
[Impeller] distinguish between no clear color and transparent black c…
Browse files Browse the repository at this point in the history
…lear color. (flutter#49038)

If we clear to transparent black, we're not forcing the pass to be constructed. Change the entity pass API so that we can tell the difference between clearing transparent black and not having a clear color.

In flutter/flutter#139571 , the app is creating a layer that is clearing to a transparent color, which is getting skipped. That invalid texture is fed into a blend which produces either black or magenta error texture.

Fixes flutter/flutter#139571
  • Loading branch information
jonahwilliams authored and zanderso committed Dec 15, 2023
1 parent bc2e856 commit 97df97a
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 17 deletions.
174 changes: 168 additions & 6 deletions impeller/aiks/aiks_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ TEST_P(AiksTest, CanRenderLinearGradientWithDitheringDisabled) {

TEST_P(AiksTest, CanRenderLinearGradientWithDitheringEnabled) {
CanRenderLinearGradientWithDithering(this, true);
} // namespace
}

static void CanRenderRadialGradientWithDithering(AiksTest* aiks_test,
bool use_dithering) {
Expand Down Expand Up @@ -2416,19 +2416,18 @@ TEST_P(AiksTest, ClearColorOptimizationDoesNotApplyForBackdropFilters) {
Picture picture = canvas.EndRecordingAsPicture();

std::optional<Color> actual_color;
bool found_subpass = false;
picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool {
if (auto subpass = std::get_if<std::unique_ptr<EntityPass>>(&element)) {
actual_color = subpass->get()->GetClearColor();
found_subpass = true;
}
// Fail if the first element isn't a subpass.
return true;
});

ASSERT_TRUE(actual_color.has_value());
if (!actual_color) {
return;
}
ASSERT_EQ(actual_color.value(), Color::BlackTransparent());
EXPECT_TRUE(found_subpass);
EXPECT_FALSE(actual_color.has_value());
}

TEST_P(AiksTest, CollapsedDrawPaintInSubpass) {
Expand Down Expand Up @@ -3645,5 +3644,168 @@ TEST_P(AiksTest, MaskBlurWithZeroSigmaIsSkipped) {
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, AdvancedBlendWithClearColorOptimization) {
Canvas canvas;
canvas.DrawPaint(
{.color = {1.0, 1.0, 0.0, 1.0}, .blend_mode = BlendMode::kSource});

canvas.DrawRect(
Rect::MakeXYWH(0, 0, 200, 300),
{.color = {1.0, 0.0, 1.0, 1.0}, .blend_mode = BlendMode::kMultiply});
}

TEST_P(AiksTest, GaussianBlurAtPeripheryVertical) {
Canvas canvas;

canvas.Scale(GetContentScale());
canvas.DrawRRect(Rect::MakeLTRB(0, 0, GetWindowSize().width, 100),
Size(10, 10), Paint{.color = Color::LimeGreen()});
canvas.DrawRRect(Rect::MakeLTRB(0, 110, GetWindowSize().width, 210),
Size(10, 10), Paint{.color = Color::Magenta()});
canvas.ClipRect(Rect::MakeLTRB(100, 0, 200, GetWindowSize().height));
canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt,
ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kClamp));
canvas.Restore();

ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, GaussianBlurAtPeripheryHorizontal) {
Canvas canvas;

canvas.Scale(GetContentScale());
std::shared_ptr<Texture> boston = CreateTextureForFixture("boston.jpg");
canvas.DrawImageRect(
std::make_shared<Image>(boston),
Rect::MakeXYWH(0, 0, boston->GetSize().width, boston->GetSize().height),
Rect::MakeLTRB(0, 0, GetWindowSize().width, 100), Paint{});
canvas.DrawRRect(Rect::MakeLTRB(0, 110, GetWindowSize().width, 210),
Size(10, 10), Paint{.color = Color::Magenta()});
canvas.ClipRect(Rect::MakeLTRB(0, 50, GetWindowSize().width, 150));
canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt,
ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kClamp));
canvas.Restore();
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

#define FLT_FORWARD(mock, real, method) \
EXPECT_CALL(*mock, method()) \
.WillRepeatedly(::testing::Return(real->method()));

TEST_P(AiksTest, GaussianBlurWithoutDecalSupport) {
if (GetParam() != PlaygroundBackend::kMetal) {
GTEST_SKIP_(
"This backend doesn't yet support setting device capabilities.");
}
if (!WillRenderSomething()) {
// Sometimes these tests are run without playgrounds enabled which is
// pointless for this test since we are asserting that
// `SupportsDecalSamplerAddressMode` is called.
GTEST_SKIP_("This test requires playgrounds.");
}

std::shared_ptr<const Capabilities> old_capabilities =
GetContext()->GetCapabilities();
auto mock_capabilities = std::make_shared<MockCapabilities>();
EXPECT_CALL(*mock_capabilities, SupportsDecalSamplerAddressMode())
.Times(::testing::AtLeast(1))
.WillRepeatedly(::testing::Return(false));
FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultColorFormat);
FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultStencilFormat);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsOffscreenMSAA);
FLT_FORWARD(mock_capabilities, old_capabilities,
SupportsImplicitResolvingMSAA);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsReadFromResolve);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsFramebufferFetch);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsSSBO);
FLT_FORWARD(mock_capabilities, old_capabilities, SupportsCompute);
FLT_FORWARD(mock_capabilities, old_capabilities,
SupportsTextureToTextureBlits);
ASSERT_TRUE(SetCapabilities(mock_capabilities).ok());

auto texture = std::make_shared<Image>(CreateTextureForFixture("boston.jpg"));
Canvas canvas;
canvas.Scale(GetContentScale() * 0.5);
canvas.DrawPaint({.color = Color::Black()});
canvas.DrawImage(
texture, Point(200, 200),
{
.image_filter = ImageFilter::MakeBlur(
Sigma(20.0), Sigma(20.0), FilterContents::BlurStyle::kNormal,
Entity::TileMode::kDecal),
});
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, GaussianBlurOneDimension) {
Canvas canvas;

canvas.Scale(GetContentScale());
canvas.Scale({0.5, 0.5, 1.0});
std::shared_ptr<Texture> boston = CreateTextureForFixture("boston.jpg");
canvas.DrawImage(std::make_shared<Image>(boston), Point(100, 100), Paint{});
canvas.SaveLayer({.blend_mode = BlendMode::kSource}, std::nullopt,
ImageFilter::MakeBlur(Sigma(50.0), Sigma(0.0),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kClamp));
canvas.Restore();
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

// Smoketest to catch issues with the coverage hint.
// Draws a rotated blurred image within a rectangle clip. The center of the clip
// rectangle is the center of the rotated image. The entire area of the clip
// rectangle should be filled with opaque colors output by the blur.
TEST_P(AiksTest, GaussianBlurRotatedAndClipped) {
Canvas canvas;
std::shared_ptr<Texture> boston = CreateTextureForFixture("boston.jpg");
Rect bounds =
Rect::MakeXYWH(0, 0, boston->GetSize().width, boston->GetSize().height);
Vector2 image_center = Vector2(bounds.GetSize() / 2);
Paint paint = {.image_filter =
ImageFilter::MakeBlur(Sigma(20.0), Sigma(20.0),
FilterContents::BlurStyle::kNormal,
Entity::TileMode::kDecal)};
Vector2 clip_size = {150, 75};
Vector2 center = Vector2(1024, 768) / 2;
canvas.Scale(GetContentScale());
canvas.ClipRect(
Rect::MakeLTRB(center.x, center.y, center.x, center.y).Expand(clip_size));
canvas.Translate({center.x, center.y, 0});
canvas.Scale({0.6, 0.6, 1});
canvas.Rotate(Degrees(25));

canvas.DrawImageRect(std::make_shared<Image>(boston), /*source=*/bounds,
/*dest=*/bounds.Shift(-image_center), paint);

ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

TEST_P(AiksTest, SubpassWithClearColorOptimization) {
Canvas canvas;

// Use a non-srcOver blend mode to ensure that we don't detect this as an
// opacity peephole optimization.
canvas.SaveLayer(
{.color = Color::Blue().WithAlpha(0.5), .blend_mode = BlendMode::kSource},
Rect::MakeLTRB(0, 0, 200, 200));
canvas.DrawPaint(
{.color = Color::BlackTransparent(), .blend_mode = BlendMode::kSource});
canvas.Restore();

canvas.SaveLayer(
{.color = Color::Blue(), .blend_mode = BlendMode::kDestinationOver});
canvas.Restore();

// This playground should appear blank on CI since we are only drawing
// transparent black. If the clear color optimization is broken, the texture
// will be filled with NaNs and may produce a magenta texture on macOS or iOS.
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}

} // namespace testing
} // namespace impeller
1 change: 0 additions & 1 deletion impeller/aiks/paint_pass_delegate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include "impeller/entity/contents/texture_contents.h"
#include "impeller/entity/entity_pass.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/path_builder.h"

namespace impeller {

Expand Down
46 changes: 37 additions & 9 deletions impeller/entity/entity_pass.cc
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ bool EntityPass::Render(ContentContext& renderer,
if (!supports_onscreen_backdrop_reads && reads_from_onscreen_backdrop) {
auto offscreen_target = CreateRenderTarget(
renderer, root_render_target.GetRenderTargetSize(), true,
GetClearColor(render_target.GetRenderTargetSize()));
GetClearColorOrDefault(render_target.GetRenderTargetSize()));

if (!OnRender(renderer, // renderer
capture, // capture
Expand Down Expand Up @@ -475,7 +475,8 @@ bool EntityPass::Render(ContentContext& renderer,
}

// Set up the clear color of the root pass.
color0.clear_color = GetClearColor(render_target.GetRenderTargetSize());
color0.clear_color =
GetClearColorOrDefault(render_target.GetRenderTargetSize());
root_render_target.SetColorAttachment(color0, 0);

EntityPassTarget pass_target(
Expand Down Expand Up @@ -628,10 +629,16 @@ EntityPass::EntityResult EntityPass::GetEntityForElement(
}

auto subpass_target = CreateRenderTarget(
<<<<<<< HEAD
renderer, // renderer
subpass_size, // size
subpass->GetTotalPassReads(renderer) > 0, // readable
subpass->GetClearColor(subpass_size)); // clear_color
=======
renderer, // renderer
subpass_size, // size
subpass->GetClearColorOrDefault(subpass_size)); // clear_color
>>>>>>> 0b0fab8215 ([Impeller] distinguish between no clear color and transparent black clear color. (#49038))

if (!subpass_target.IsValid()) {
VALIDATION_LOG << "Subpass render target is invalid.";
Expand Down Expand Up @@ -722,8 +729,7 @@ bool EntityPass::OnRender(
}
auto clear_color_size = pass_target.GetRenderTarget().GetRenderTargetSize();

if (!collapsed_parent_pass &&
!GetClearColor(clear_color_size).IsTransparent()) {
if (!collapsed_parent_pass && GetClearColor(clear_color_size).has_value()) {
// Force the pass context to create at least one new pass if the clear color
// is present.
pass_context.GetRenderPass(pass_depth);
Expand Down Expand Up @@ -965,6 +971,20 @@ bool EntityPass::OnRender(
FilterInput::Make(texture,
result.entity.GetTransformation().Invert()),
FilterInput::Make(result.entity.GetContents())};
<<<<<<< HEAD
=======
std::optional<Color> foreground_color;
std::optional<Rect> coverage;
if (was_collapsing_clear_colors) {
// If all previous elements were skipped due to clear color
// optimization, then provide the clear color as the foreground of the
// advanced blend.
foreground_color = GetClearColorOrDefault(clear_color_size);
coverage = Rect::MakeSize(clear_color_size);
} else {
coverage = result.entity.GetCoverage();
}
>>>>>>> 0b0fab8215 ([Impeller] distinguish between no clear color and transparent black clear color. (#49038))
auto contents = ColorFilterContents::MakeBlend(
result.entity.GetBlendMode(), inputs);
contents->SetCoverageHint(result.entity.GetCoverage());
Expand Down Expand Up @@ -1140,21 +1160,29 @@ void EntityPass::SetBlendMode(BlendMode blend_mode) {
flood_clip_ = Entity::IsBlendModeDestructive(blend_mode);
}

Color EntityPass::GetClearColor(ISize target_size) const {
Color result = Color::BlackTransparent();
Color EntityPass::GetClearColorOrDefault(ISize size) const {
return GetClearColor(size).value_or(Color::BlackTransparent());
}

std::optional<Color> EntityPass::GetClearColor(ISize target_size) const {
if (backdrop_filter_proc_) {
return result;
return std::nullopt;
}

std::optional<Color> result = std::nullopt;
for (const Element& element : elements_) {
auto [entity_color, blend_mode] =
ElementAsBackgroundColor(element, target_size);
if (!entity_color.has_value()) {
break;
}
result = result.Blend(entity_color.value(), blend_mode);
result = result.value_or(Color::BlackTransparent())
.Blend(entity_color.value(), blend_mode);
}
if (result.has_value()) {
return result->Premultiply();
}
return result.Premultiply();
return result;
}

void EntityPass::SetBackdropFilter(BackdropFilterProc proc) {
Expand Down
8 changes: 7 additions & 1 deletion impeller/entity/entity_pass.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ class EntityPass {

void SetBlendMode(BlendMode blend_mode);

Color GetClearColor(ISize size = ISize::Infinite()) const;
/// @brief Return the premultiplied clear color of the pass entities, if any.
std::optional<Color> GetClearColor(ISize size = ISize::Infinite()) const;

/// @brief Return the premultiplied clear color of the pass entities.
///
/// If the entity pass has no clear color, this will return transparent black.
Color GetClearColorOrDefault(ISize size = ISize::Infinite()) const;

void SetBackdropFilter(BackdropFilterProc proc);

Expand Down

0 comments on commit 97df97a

Please sign in to comment.