Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
../../../flutter/impeller/entity/contents/clip_contents_unittests.cc
../../../flutter/impeller/entity/contents/filters/gaussian_blur_filter_contents_unittests.cc
../../../flutter/impeller/entity/contents/filters/inputs/filter_input_unittests.cc
../../../flutter/impeller/entity/contents/filters/matrix_filter_contents_unittests.cc
../../../flutter/impeller/entity/contents/host_buffer_unittests.cc
../../../flutter/impeller/entity/contents/test
../../../flutter/impeller/entity/contents/tiled_texture_contents_unittests.cc
Expand Down
52 changes: 52 additions & 0 deletions impeller/display_list/dl_golden_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace testing {

using impeller::PlaygroundBackend;
using impeller::PlaygroundTest;
using impeller::Point;

INSTANTIATE_PLAYGROUND_SUITE(DlGoldenTest);

Expand Down Expand Up @@ -48,5 +49,56 @@ TEST_P(DlGoldenTest, CanRenderImage) {
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}

// Asserts that subpass rendering of MatrixImageFilters works.
// https://github.com/flutter/flutter/issues/147807
TEST_P(DlGoldenTest, Bug147807) {
Point content_scale = GetContentScale();
auto draw = [content_scale](DlCanvas* canvas,
const std::vector<sk_sp<DlImage>>& images) {
canvas->Transform2DAffine(content_scale.x, 0, 0, 0, content_scale.y, 0);
DlPaint paint;
paint.setColor(DlColor(0xfffef7ff));
canvas->DrawRect(SkRect::MakeLTRB(0, 0, 375, 667), paint);
paint.setColor(DlColor(0xffff9800));
canvas->DrawRect(SkRect::MakeLTRB(0, 0, 187.5, 333.5), paint);
paint.setColor(DlColor(0xff9c27b0));
canvas->DrawRect(SkRect::MakeLTRB(187.5, 0, 375, 333.5), paint);
paint.setColor(DlColor(0xff4caf50));
canvas->DrawRect(SkRect::MakeLTRB(0, 333.5, 187.5, 667), paint);
paint.setColor(DlColor(0xfff44336));
canvas->DrawRect(SkRect::MakeLTRB(187.5, 333.5, 375, 667), paint);

canvas->Save();
{
canvas->ClipRRect(
SkRRect::MakeOval(SkRect::MakeLTRB(201.25, 10, 361.25, 170)),
DlCanvas::ClipOp::kIntersect, true);
SkRect save_layer_bounds = SkRect::MakeLTRB(201.25, 10, 361.25, 170);
DlMatrixImageFilter backdrop(SkMatrix::MakeAll(3, 0, -280, //
0, 3, -920, //
0, 0, 1),
DlImageSampling::kLinear);
canvas->SaveLayer(&save_layer_bounds, /*paint=*/nullptr, &backdrop);
{
canvas->Translate(201.25, 10);
auto paint = DlPaint()
.setAntiAlias(true)
.setColor(DlColor(0xff2196f3))
.setStrokeWidth(5)
.setDrawStyle(DlDrawStyle::kStroke);
canvas->DrawCircle(SkPoint::Make(80, 80), 80, paint);
}
canvas->Restore();
}
canvas->Restore();
};

DisplayListBuilder builder;
std::vector<sk_sp<DlImage>> images;
draw(&builder, images);

ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}

} // namespace testing
} // namespace flutter
1 change: 1 addition & 0 deletions impeller/entity/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ impeller_component("entity_unittests") {
"contents/clip_contents_unittests.cc",
"contents/filters/gaussian_blur_filter_contents_unittests.cc",
"contents/filters/inputs/filter_input_unittests.cc",
"contents/filters/matrix_filter_contents_unittests.cc",
"contents/host_buffer_unittests.cc",
"contents/tiled_texture_contents_unittests.cc",
"entity_pass_target_unittests.cc",
Expand Down
92 changes: 61 additions & 31 deletions impeller/entity/contents/filters/matrix_filter_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ void MatrixFilterContents::SetSamplerDescriptor(SamplerDescriptor desc) {
sampler_descriptor_ = std::move(desc);
}

namespace {
Matrix CalculateSubpassTransform(const Matrix& entity_transform,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdero this is what I was talking about the other day. We should be sharing the math between rendering and coverage.

const Matrix& effect_transform,
const Matrix& matrix) {
return entity_transform * //
effect_transform * //
matrix * //
effect_transform.Invert();
}
} // namespace

std::optional<Entity> MatrixFilterContents::RenderFilter(
const FilterInput::Vector& inputs,
const ContentContext& renderer,
Expand All @@ -40,29 +51,42 @@ std::optional<Entity> MatrixFilterContents::RenderFilter(
return std::nullopt;
}

// The filter's matrix needs to be applied within the space defined by the
// scene's current transform matrix (CTM). For example: If the CTM is
// scaled up, then translations applied by the matrix should be magnified
// accordingly.
//
// To accomplish this, we sandwich the filter's matrix within the CTM in both
// cases. But notice that for the subpass backdrop filter case, we use the
// "effect transform" instead of the Entity's transform!
//
// That's because in the subpass backdrop filter case, the Entity's transform
// isn't actually the captured CTM of the scene like it usually is; instead,
// it's just a screen space translation that offsets the backdrop texture (as
// mentioned above). And so we sneak the subpass's captured CTM in through the
// effect transform.

auto transform = rendering_mode_ == Entity::RenderingMode::kSubpass
? effect_transform
: entity.GetTransform();
snapshot->transform = transform * //
matrix_ * //
transform.Invert() * //
snapshot->transform;

if (rendering_mode_ == Entity::RenderingMode::kSubpass) {
// There are two special quirks with how Matrix filters behave when used as
// subpass backdrop filters:
//
// 1. For subpass backdrop filters, the snapshot transform is always just a
// translation that positions the parent pass texture correctly relative
// to the subpass texture. However, this translation always needs to be
// applied in screen space.
//
// Since we know the snapshot transform will always have an identity
// basis in this case, we safely reverse the order and apply the filter's
// matrix within the snapshot transform space.
//
// 2. The filter's matrix needs to be applied within the space defined by
// the scene's current transformation matrix (CTM). For example: If the
// CTM is scaled up, then translations applied by the matrix should be
// magnified accordingly.
//
// To accomplish this, we sandwitch the filter's matrix within the CTM in
// both cases. But notice that for the subpass backdrop filter case, we
// use the "effect transform" instead of the Entity's transform!
//
// That's because in the subpass backdrop filter case, the Entity's
// transform isn't actually the captured CTM of the scene like it usually
// is; instead, it's just a screen space translation that offsets the
// backdrop texture (as mentioned above). And so we sneak the subpass's
// captured CTM in through the effect transform.
//
snapshot->transform = CalculateSubpassTransform(snapshot->transform,
effect_transform, matrix_);
} else {
snapshot->transform = entity.GetTransform() * //
matrix_ * //
entity.GetTransform().Invert() * //
snapshot->transform;
}
snapshot->sampler_descriptor = sampler_descriptor_;
if (!snapshot.has_value()) {
return std::nullopt;
Expand Down Expand Up @@ -91,17 +115,23 @@ std::optional<Rect> MatrixFilterContents::GetFilterCoverage(
return std::nullopt;
}

auto coverage = inputs[0]->GetCoverage(entity);
std::optional<Rect> coverage = inputs[0]->GetCoverage(entity);
if (!coverage.has_value()) {
return std::nullopt;
}
auto& m = rendering_mode_ == Entity::RenderingMode::kSubpass
? effect_transform
: inputs[0]->GetTransform(entity);
auto transform = m * //
matrix_ * //
m.Invert(); //
return coverage->TransformBounds(transform);

Matrix input_transform = inputs[0]->GetTransform(entity);
if (rendering_mode_ == Entity::RenderingMode::kSubpass) {
Rect coverage_bounds = coverage->TransformBounds(input_transform.Invert());
Matrix transform =
CalculateSubpassTransform(input_transform, effect_transform, matrix_);
return coverage_bounds.TransformBounds(transform);
} else {
Matrix transform = input_transform * //
matrix_ * //
input_transform.Invert(); //
return coverage->TransformBounds(transform);
}
}

} // namespace impeller
198 changes: 198 additions & 0 deletions impeller/entity/contents/filters/matrix_filter_contents_unittests.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/testing.h"
#include "gmock/gmock.h"
#include "impeller/entity/contents/filters/matrix_filter_contents.h"
#include "impeller/entity/entity_playground.h"
#include "impeller/geometry/geometry_asserts.h"

namespace impeller {
namespace testing {

class MatrixFilterContentsTest : public EntityPlayground {
public:
/// Create a texture that has been cleared to transparent black.
std::shared_ptr<Texture> MakeTexture(ISize size) {
std::shared_ptr<CommandBuffer> command_buffer =
GetContentContext()->GetContext()->CreateCommandBuffer();
if (!command_buffer) {
return nullptr;
}

auto render_target = GetContentContext()->MakeSubpass(
"Clear Subpass", size, command_buffer,
[](const ContentContext&, RenderPass&) { return true; });

if (!GetContentContext()
->GetContext()
->GetCommandQueue()
->Submit(/*buffers=*/{command_buffer})
.ok()) {
return nullptr;
}

if (render_target.ok()) {
return render_target.value().GetRenderTargetTexture();
}
return nullptr;
}
};

INSTANTIATE_PLAYGROUND_SUITE(MatrixFilterContentsTest);

TEST(MatrixFilterContentsTest, Create) {
MatrixFilterContents contents;
EXPECT_TRUE(contents.IsTranslationOnly());
}

TEST(MatrixFilterContentsTest, CoverageEmpty) {
MatrixFilterContents contents;
FilterInput::Vector inputs = {};
Entity entity;
std::optional<Rect> coverage =
contents.GetFilterCoverage(inputs, entity, /*effect_transform=*/Matrix());
ASSERT_FALSE(coverage.has_value());
}

TEST(MatrixFilterContentsTest, CoverageSimple) {
MatrixFilterContents contents;
FilterInput::Vector inputs = {
FilterInput::Make(Rect::MakeLTRB(10, 10, 110, 110))};
Entity entity;
std::optional<Rect> coverage =
contents.GetFilterCoverage(inputs, entity, /*effect_transform=*/Matrix());

ASSERT_EQ(coverage, Rect::MakeLTRB(10, 10, 110, 110));
}

TEST(MatrixFilterContentsTest, Coverage2x) {
MatrixFilterContents contents;
contents.SetMatrix(Matrix::MakeScale({2.0, 2.0, 1.0}));
FilterInput::Vector inputs = {
FilterInput::Make(Rect::MakeXYWH(10, 10, 100, 100))};
Entity entity;
std::optional<Rect> coverage =
contents.GetFilterCoverage(inputs, entity, /*effect_transform=*/Matrix());

ASSERT_EQ(coverage, Rect::MakeXYWH(20, 20, 200, 200));
}

TEST(MatrixFilterContentsTest, Coverage2xEffect) {
MatrixFilterContents contents;
FilterInput::Vector inputs = {
FilterInput::Make(Rect::MakeXYWH(10, 10, 100, 100))};
Entity entity;
std::optional<Rect> coverage = contents.GetFilterCoverage(
inputs, entity, /*effect_transform=*/Matrix::MakeScale({2.0, 2.0, 1.0}));

ASSERT_EQ(coverage, Rect::MakeXYWH(10, 10, 100, 100));
}

namespace {
void expectRenderCoverageEqual(const std::optional<Entity>& result,
const std::optional<Rect> contents_coverage,
const Rect& expected) {
EXPECT_TRUE(result.has_value());
if (result.has_value()) {
EXPECT_EQ(result.value().GetBlendMode(), BlendMode::kSourceOver);
std::optional<Rect> result_coverage = result.value().GetCoverage();
EXPECT_TRUE(result_coverage.has_value());
EXPECT_TRUE(contents_coverage.has_value());
if (result_coverage.has_value() && contents_coverage.has_value()) {
EXPECT_TRUE(RectNear(contents_coverage.value(), expected));
EXPECT_TRUE(RectNear(result_coverage.value(), expected));
}
}
}
} // namespace

TEST_P(MatrixFilterContentsTest, RenderCoverageMatchesGetCoverageIdentity) {
std::shared_ptr<Texture> texture = MakeTexture(ISize(100, 100));
MatrixFilterContents contents;
contents.SetInputs({FilterInput::Make(texture)});

Entity entity;
entity.SetTransform(Matrix::MakeTranslation({100, 200, 0}));

std::shared_ptr<ContentContext> renderer = GetContentContext();
std::optional<Entity> result =
contents.GetEntity(*renderer, entity, /*coverage_hint=*/{});
expectRenderCoverageEqual(result, contents.GetCoverage(entity),
Rect::MakeXYWH(100, 200, 100, 100));
}

TEST_P(MatrixFilterContentsTest, RenderCoverageMatchesGetCoverageTranslate) {
std::shared_ptr<Texture> texture = MakeTexture(ISize(100, 100));
MatrixFilterContents contents;
contents.SetInputs({FilterInput::Make(texture)});
contents.SetMatrix(Matrix::MakeTranslation({50, 100, 0}));
contents.SetEffectTransform(Matrix::MakeScale({2, 2, 1}));

Entity entity;
entity.SetTransform(Matrix::MakeTranslation({100, 200, 0}));

std::shared_ptr<ContentContext> renderer = GetContentContext();
std::optional<Entity> result =
contents.GetEntity(*renderer, entity, /*coverage_hint=*/{});
expectRenderCoverageEqual(result, contents.GetCoverage(entity),
Rect::MakeXYWH(150, 300, 100, 100));
}

TEST_P(MatrixFilterContentsTest,
RenderCoverageMatchesGetCoverageSubpassTranslate) {
std::shared_ptr<Texture> texture = MakeTexture(ISize(100, 100));
MatrixFilterContents contents;
contents.SetInputs({FilterInput::Make(texture)});
contents.SetMatrix(Matrix::MakeTranslation({50, 100, 0}));
contents.SetEffectTransform(Matrix::MakeScale({2, 2, 1}));
contents.SetRenderingMode(Entity::RenderingMode::kSubpass);

Entity entity;
entity.SetTransform(Matrix::MakeTranslation({100, 200, 0}));

std::shared_ptr<ContentContext> renderer = GetContentContext();
std::optional<Entity> result =
contents.GetEntity(*renderer, entity, /*coverage_hint=*/{});
expectRenderCoverageEqual(result, contents.GetCoverage(entity),
Rect::MakeXYWH(200, 400, 100, 100));
}

TEST_P(MatrixFilterContentsTest, RenderCoverageMatchesGetCoverageScale) {
std::shared_ptr<Texture> texture = MakeTexture(ISize(100, 100));
MatrixFilterContents contents;
contents.SetInputs({FilterInput::Make(texture)});
contents.SetMatrix(Matrix::MakeScale({3, 3, 1}));
contents.SetEffectTransform(Matrix::MakeScale({2, 2, 1}));

Entity entity;
entity.SetTransform(Matrix::MakeTranslation({100, 200, 0}));

std::shared_ptr<ContentContext> renderer = GetContentContext();
std::optional<Entity> result =
contents.GetEntity(*renderer, entity, /*coverage_hint=*/{});
expectRenderCoverageEqual(result, contents.GetCoverage(entity),
Rect::MakeXYWH(100, 200, 300, 300));
}

TEST_P(MatrixFilterContentsTest, RenderCoverageMatchesGetCoverageSubpassScale) {
std::shared_ptr<Texture> texture = MakeTexture(ISize(100, 100));
MatrixFilterContents contents;
contents.SetInputs({FilterInput::Make(texture)});
contents.SetMatrix(Matrix::MakeScale({3, 3, 1}));
contents.SetEffectTransform(Matrix::MakeScale({2, 2, 1}));
contents.SetRenderingMode(Entity::RenderingMode::kSubpass);

Entity entity;
entity.SetTransform(Matrix::MakeTranslation({100, 200, 0}));

std::shared_ptr<ContentContext> renderer = GetContentContext();
std::optional<Entity> result =
contents.GetEntity(*renderer, entity, /*coverage_hint=*/{});
expectRenderCoverageEqual(result, contents.GetCoverage(entity),
Rect::MakeXYWH(100, 200, 300, 300));
}

} // namespace testing
} // namespace impeller
Loading