[Impeller] Collapse DrawRects into clear colors optimization (flutter/engine#43168)

design doc: https://docs.google.com/document/d/1Lqf1BRn4uCcUfv9dZlDyAgdSMeQ3FTnPgPjKF9yQ3MI/edit

fixes: https://github.com/flutter/flutter/issues/129292

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
gaaclarke
2023-06-29 00:37:02 +00:00
committed by GitHub
parent 0098a0b154
commit 4f5dfc63c5
19 changed files with 442 additions and 29 deletions

View File

@@ -121,6 +121,7 @@
../../../flutter/impeller/README.md
../../../flutter/impeller/aiks/aiks_unittests.cc
../../../flutter/impeller/aiks/canvas_unittests.cc
../../../flutter/impeller/aiks/testing
../../../flutter/impeller/archivist/archivist_unittests.cc
../../../flutter/impeller/base/README.md
../../../flutter/impeller/base/base_unittests.cc

View File

@@ -50,6 +50,9 @@ impeller_component("aiks_unittests") {
sources = [
"aiks_unittests.cc",
"canvas_unittests.cc",
"testing/context_mock.h",
"testing/context_spy.cc",
"testing/context_spy.h",
]
deps = [
":aiks",
@@ -69,7 +72,12 @@ impeller_component("aiks_unittests_golden") {
"IMPELLER_ENABLE_VALIDATION=1",
]
sources = [ "aiks_unittests.cc" ]
sources = [
"aiks_unittests.cc",
"testing/context_mock.h",
"testing/context_spy.cc",
"testing/context_spy.h",
]
deps = [
":aiks",
":aiks_playground",

View File

@@ -16,6 +16,7 @@
#include "impeller/aiks/canvas.h"
#include "impeller/aiks/image.h"
#include "impeller/aiks/paint_pass_delegate.h"
#include "impeller/aiks/testing/context_spy.h"
#include "impeller/entity/contents/color_source_contents.h"
#include "impeller/entity/contents/filters/inputs/filter_input.h"
#include "impeller/entity/contents/scene_contents.h"
@@ -1999,9 +2000,105 @@ TEST_P(AiksTest, DrawPaintAbsorbsClears) {
{.color = Color::CornflowerBlue(), .blend_mode = BlendMode::kSource});
Picture picture = canvas.EndRecordingAsPicture();
ASSERT_EQ(picture.pass->GetElementCount(), 0u);
ASSERT_EQ(picture.pass->GetClearColor(), Color::CornflowerBlue());
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context);
std::shared_ptr<Image> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClears) {
Canvas canvas;
canvas.DrawRect({0, 0, 300, 300},
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRect({0, 0, 300, 300}, {.color = Color::CornflowerBlue(),
.blend_mode = BlendMode::kSource});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context);
std::shared_ptr<Image> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 0llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegativeRRect) {
Canvas canvas;
canvas.DrawRRect({0, 0, 300, 300}, 5.0,
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRRect(
{0, 0, 300, 300}, 5.0,
{.color = Color::CornflowerBlue(), .blend_mode = BlendMode::kSource});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context);
std::shared_ptr<Image> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 2llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegativeRotation) {
Canvas canvas;
canvas.Translate(Vector3(150.0, 150.0, 0.0));
canvas.Rotate(Degrees(45.0));
canvas.Translate(Vector3(-150.0, -150.0, 0.0));
canvas.DrawRect({0, 0, 300, 300},
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context);
std::shared_ptr<Image> image = picture.ToImage(renderer, {300, 300});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 1llu);
}
TEST_P(AiksTest, DrawRectAbsorbsClearsNegative) {
Canvas canvas;
canvas.DrawRect({0, 0, 300, 300},
{.color = Color::Red(), .blend_mode = BlendMode::kSource});
canvas.DrawRect({0, 0, 300, 300}, {.color = Color::CornflowerBlue(),
.blend_mode = BlendMode::kSource});
std::shared_ptr<ContextSpy> spy = ContextSpy::Make();
Picture picture = canvas.EndRecordingAsPicture();
std::shared_ptr<Context> real_context = GetContext();
std::shared_ptr<ContextMock> mock_context = spy->MakeContext(real_context);
AiksContext renderer(mock_context);
std::shared_ptr<Image> image = picture.ToImage(renderer, {301, 301});
ASSERT_EQ(spy->render_passes_.size(), 1llu);
std::shared_ptr<RenderPass> render_pass = spy->render_passes_[0];
ASSERT_EQ(render_pass->GetCommands().size(), 2llu);
}
TEST_P(AiksTest, CollapsedDrawPaintInSubpass) {
Canvas canvas;
canvas.DrawPaint(
{.color = Color::Yellow(), .blend_mode = BlendMode::kSource});
canvas.SaveLayer({.blend_mode = BlendMode::kMultiply});
canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource});
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));
}
TEST_P(AiksTest, ForegroundBlendSubpassCollapseOptimization) {

View File

@@ -172,16 +172,6 @@ void Canvas::DrawPath(const Path& path, const Paint& paint) {
}
void Canvas::DrawPaint(const Paint& paint) {
if (xformation_stack_.size() == 1 && // If we're recording the root pass,
GetCurrentPass().GetElementCount() == 0 && // and this is the first item,
(paint.blend_mode == BlendMode::kSourceOver ||
paint.blend_mode == BlendMode::kSource) &&
paint.color.alpha >= 1.0f) {
// Then we can absorb this drawPaint as the clear color of the pass.
GetCurrentPass().SetClearColor(paint.color);
return;
}
Entity entity;
entity.SetTransformation(GetCurrentTransformation());
entity.SetStencilDepth(GetStencilDepth());

View File

@@ -0,0 +1,86 @@
// 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.
#pragma once
#include <string>
#include <vector>
#include "gmock/gmock.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/context.h"
#include "impeller/renderer/render_target.h"
namespace impeller {
namespace testing {
class CommandBufferMock : public CommandBuffer {
public:
CommandBufferMock(std::weak_ptr<const Context> context)
: CommandBuffer(context) {}
MOCK_CONST_METHOD0(IsValid, bool());
MOCK_CONST_METHOD1(SetLabel, void(const std::string& label));
MOCK_METHOD1(SubmitCommandsAsync,
bool(std::shared_ptr<RenderPass> render_pass));
MOCK_METHOD1(OnCreateRenderPass,
std::shared_ptr<RenderPass>(RenderTarget render_target));
static std::shared_ptr<RenderPass> ForwardOnCreateRenderPass(
CommandBuffer* command_buffer,
RenderTarget render_target) {
return command_buffer->OnCreateRenderPass(render_target);
}
MOCK_CONST_METHOD0(OnCreateBlitPass, std::shared_ptr<BlitPass>());
static std::shared_ptr<BlitPass> ForwardOnCreateBlitPass(
CommandBuffer* command_buffer) {
return command_buffer->OnCreateBlitPass();
}
MOCK_METHOD1(OnSubmitCommands, bool(CompletionCallback callback));
static bool ForwardOnSubmitCommands(CommandBuffer* command_buffer,
CompletionCallback callback) {
return command_buffer->OnSubmitCommands(callback);
}
MOCK_METHOD0(OnWaitUntilScheduled, void());
static void ForwardOnWaitUntilScheduled(CommandBuffer* command_buffer) {
return command_buffer->OnWaitUntilScheduled();
}
MOCK_CONST_METHOD0(OnCreateComputePass, std::shared_ptr<ComputePass>());
static std::shared_ptr<ComputePass> ForwardOnCreateComputePass(
CommandBuffer* command_buffer) {
return command_buffer->OnCreateComputePass();
}
};
class ContextMock : public Context {
public:
MOCK_CONST_METHOD0(DescribeGpuModel, std::string());
MOCK_CONST_METHOD0(IsValid, bool());
MOCK_CONST_METHOD0(GetCapabilities,
const std::shared_ptr<const Capabilities>&());
MOCK_METHOD1(UpdateOffscreenLayerPixelFormat, bool(PixelFormat format));
MOCK_CONST_METHOD0(GetResourceAllocator, std::shared_ptr<Allocator>());
MOCK_CONST_METHOD0(GetShaderLibrary, std::shared_ptr<ShaderLibrary>());
MOCK_CONST_METHOD0(GetSamplerLibrary, std::shared_ptr<SamplerLibrary>());
MOCK_CONST_METHOD0(GetPipelineLibrary, std::shared_ptr<PipelineLibrary>());
MOCK_CONST_METHOD0(CreateCommandBuffer, std::shared_ptr<CommandBuffer>());
};
} // namespace testing
} // namespace impeller

View File

@@ -0,0 +1,111 @@
// 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 "impeller/aiks/testing/context_spy.h"
namespace impeller {
namespace testing {
std::shared_ptr<ContextSpy> ContextSpy::Make() {
return std::shared_ptr<ContextSpy>(new ContextSpy());
}
std::shared_ptr<ContextMock> ContextSpy::MakeContext(
const std::shared_ptr<Context>& real_context) {
std::shared_ptr<ContextMock> mock_context =
std::make_shared<::testing::NiceMock<ContextMock>>();
std::shared_ptr<ContextSpy> shared_this = shared_from_this();
ON_CALL(*mock_context, IsValid).WillByDefault([real_context]() {
return real_context->IsValid();
});
ON_CALL(*mock_context, GetCapabilities)
.WillByDefault(
[real_context]() -> const std::shared_ptr<const Capabilities>& {
return real_context->GetCapabilities();
});
ON_CALL(*mock_context, UpdateOffscreenLayerPixelFormat)
.WillByDefault([real_context](PixelFormat format) {
return real_context->UpdateOffscreenLayerPixelFormat(format);
});
ON_CALL(*mock_context, GetResourceAllocator).WillByDefault([real_context]() {
return real_context->GetResourceAllocator();
});
ON_CALL(*mock_context, GetShaderLibrary).WillByDefault([real_context]() {
return real_context->GetShaderLibrary();
});
ON_CALL(*mock_context, GetSamplerLibrary).WillByDefault([real_context]() {
return real_context->GetSamplerLibrary();
});
ON_CALL(*mock_context, GetPipelineLibrary).WillByDefault([real_context]() {
return real_context->GetPipelineLibrary();
});
ON_CALL(*mock_context, CreateCommandBuffer)
.WillByDefault([real_context, shared_this]() {
auto real_buffer = real_context->CreateCommandBuffer();
auto spy = std::make_shared<::testing::NiceMock<CommandBufferMock>>(
real_context);
ON_CALL(*spy, IsValid).WillByDefault([real_buffer]() {
return real_buffer->IsValid();
});
ON_CALL(*spy, SetLabel)
.WillByDefault([real_buffer](const std::string& label) {
return real_buffer->SetLabel(label);
});
ON_CALL(*spy, SubmitCommandsAsync)
.WillByDefault([real_buffer](
std::shared_ptr<RenderPass> render_pass) {
return real_buffer->SubmitCommandsAsync(std::move(render_pass));
});
ON_CALL(*spy, OnCreateRenderPass)
.WillByDefault(
[real_buffer, shared_this](const RenderTarget& render_target) {
std::shared_ptr<RenderPass> result =
CommandBufferMock::ForwardOnCreateRenderPass(
real_buffer.get(), render_target);
shared_this->render_passes_.push_back(result);
return result;
});
ON_CALL(*spy, OnCreateBlitPass).WillByDefault([real_buffer]() {
return CommandBufferMock::ForwardOnCreateBlitPass(real_buffer.get());
});
ON_CALL(*spy, OnSubmitCommands)
.WillByDefault(
[real_buffer](CommandBuffer::CompletionCallback callback) {
return CommandBufferMock::ForwardOnSubmitCommands(
real_buffer.get(), std::move(callback));
});
ON_CALL(*spy, OnWaitUntilScheduled).WillByDefault([real_buffer]() {
return CommandBufferMock::ForwardOnWaitUntilScheduled(
real_buffer.get());
});
ON_CALL(*spy, OnCreateComputePass).WillByDefault([real_buffer]() {
return CommandBufferMock::ForwardOnCreateComputePass(
real_buffer.get());
});
return spy;
});
return mock_context;
}
} // namespace testing
} // namespace impeller

View File

@@ -0,0 +1,30 @@
// 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.
#pragma once
#include <memory>
#include "impeller/aiks/testing/context_mock.h"
namespace impeller {
namespace testing {
/// Forwards calls to a real Context but can store information about how
/// the Context was used.
class ContextSpy : public std::enable_shared_from_this<ContextSpy> {
public:
static std::shared_ptr<ContextSpy> Make();
std::shared_ptr<ContextMock> MakeContext(
const std::shared_ptr<Context>& real_context);
std::vector<std::shared_ptr<RenderPass>> render_passes_;
private:
ContextSpy() = default;
};
} // namespace testing
} // namespace impeller

View File

@@ -117,6 +117,11 @@ class Contents {
/// Use of this method is invalid if CanAcceptOpacity returns false.
virtual void SetInheritedOpacity(Scalar opacity);
virtual std::optional<Color> AsBackgroundColor(const Entity& entity,
ISize target_size) const {
return {};
}
private:
std::optional<Rect> coverage_hint_;
std::optional<Size> color_source_size_;

View File

@@ -101,4 +101,19 @@ std::unique_ptr<SolidColorContents> SolidColorContents::Make(const Path& path,
return contents;
}
std::optional<Color> SolidColorContents::AsBackgroundColor(
const Entity& entity,
ISize target_size) const {
if (!(GetColor().IsOpaque() &&
(entity.GetBlendMode() == BlendMode::kSource ||
entity.GetBlendMode() == BlendMode::kSourceOver))) {
return {};
}
Rect target_rect = Rect::MakeSize(target_size);
return GetGeometry()->CoversArea(entity.GetTransformation(), target_rect)
? GetColor()
: std::optional<Color>();
}
} // namespace impeller

View File

@@ -49,6 +49,9 @@ class SolidColorContents final : public ColorSourceContents {
const Entity& entity,
RenderPass& pass) const override;
std::optional<Color> AsBackgroundColor(const Entity& entity,
ISize target_size) const override;
private:
Color color_;

View File

@@ -36,6 +36,20 @@
namespace impeller {
namespace {
std::optional<Color> AsBackgroundColor(const EntityPass::Element& element,
ISize target_size) {
if (const Entity* entity = std::get_if<Entity>(&element)) {
std::optional<Color> entity_color =
entity->GetContents()->AsBackgroundColor(*entity, target_size);
if (entity_color.has_value()) {
return entity_color.value();
}
}
return {};
}
} // namespace
EntityPass::EntityPass() = default;
EntityPass::~EntityPass() = default;
@@ -235,9 +249,9 @@ bool EntityPass::Render(ContentContext& renderer,
// and then blit the results onto the onscreen texture. If using this branch,
// there's no need to set up a stencil attachment on the root render target.
if (!supports_onscreen_backdrop_reads && reads_from_onscreen_backdrop) {
auto offscreen_target =
CreateRenderTarget(renderer, root_render_target.GetRenderTargetSize(),
true, clear_color_.Premultiply());
auto offscreen_target = CreateRenderTarget(
renderer, root_render_target.GetRenderTargetSize(), true,
GetClearColor(render_target.GetRenderTargetSize()).Premultiply());
if (!OnRender(renderer, // renderer
offscreen_target.GetRenderTarget()
@@ -342,7 +356,8 @@ bool EntityPass::Render(ContentContext& renderer,
}
// Set up the clear color of the root pass.
color0.clear_color = clear_color_.Premultiply();
color0.clear_color =
GetClearColor(render_target.GetRenderTargetSize()).Premultiply();
root_render_target.SetColorAttachment(color0, 0);
EntityPassTarget pass_target(
@@ -478,7 +493,7 @@ EntityPass::EntityResult EntityPass::GetEntityForElement(
renderer, // renderer
subpass_size, // size
subpass->GetTotalPassReads(renderer) > 0, // readable
Color::BlackTransparent()); // clear_color
subpass->GetClearColor(subpass_size)); // clear_color
if (!subpass_target.IsValid()) {
VALIDATION_LOG << "Subpass render target is invalid.";
@@ -559,7 +574,7 @@ bool EntityPass::OnRender(
return false;
}
if (!(clear_color_ == Color::BlackTransparent())) {
if (!(GetClearColor(root_pass_size) == Color::BlackTransparent())) {
// Force the pass context to create at least one new pass if the clear color
// is present. The `EndPass` first ensures that the clear color will get
// applied even if this EntityPass is getting collapsed into the parent
@@ -704,7 +719,19 @@ bool EntityPass::OnRender(
render_element(backdrop_entity);
}
bool is_collapsing_clear_colors = true;
for (const auto& element : elements_) {
// Skip elements that are incorporated into the clear color.
if (is_collapsing_clear_colors) {
std::optional<Color> entity_color =
AsBackgroundColor(element, root_pass_size);
if (entity_color.has_value()) {
continue;
} else {
is_collapsing_clear_colors = false;
}
}
EntityResult result =
GetEntityForElement(element, // element
renderer, // renderer
@@ -892,12 +919,17 @@ void EntityPass::SetBlendMode(BlendMode blend_mode) {
flood_clip_ = Entity::IsBlendModeDestructive(blend_mode);
}
void EntityPass::SetClearColor(Color clear_color) {
clear_color_ = clear_color;
}
Color EntityPass::GetClearColor() const {
return clear_color_;
Color EntityPass::GetClearColor(ISize target_size) const {
Color result = Color::BlackTransparent();
for (const Element& element : elements_) {
std::optional<Color> entity_color = AsBackgroundColor(element, target_size);
if (entity_color.has_value()) {
result = entity_color.value();
} else {
break;
}
}
return result;
}
void EntityPass::SetBackdropFilter(std::optional<BackdropFilterProc> proc) {

View File

@@ -86,9 +86,7 @@ class EntityPass {
void SetBlendMode(BlendMode blend_mode);
void SetClearColor(Color clear_color);
Color GetClearColor() const;
Color GetClearColor(ISize size = ISize::Infinite()) const;
void SetBackdropFilter(std::optional<BackdropFilterProc> proc);
@@ -210,7 +208,6 @@ class EntityPass {
size_t stencil_depth_ = 0u;
BlendMode blend_mode_ = BlendMode::kSourceOver;
bool flood_clip_ = false;
Color clear_color_ = Color::BlackTransparent();
bool enable_offscreen_debug_checkerboard_ = false;
/// These values are incremented whenever something is added to the pass that

View File

@@ -54,4 +54,9 @@ std::optional<Rect> CoverGeometry::GetCoverage(const Matrix& transform) const {
return Rect::MakeMaximum();
}
bool CoverGeometry::CoversArea(const Matrix& transform,
const Rect& rect) const {
return true;
}
} // namespace impeller

View File

@@ -16,6 +16,9 @@ class CoverGeometry : public Geometry {
~CoverGeometry();
// |Geometry|
bool CoversArea(const Matrix& transform, const Rect& rect) const override;
private:
// |Geometry|
GeometryResult GetPositionBuffer(const ContentContext& renderer,

View File

@@ -87,6 +87,12 @@ class Geometry {
virtual GeometryVertexType GetVertexType() const = 0;
virtual std::optional<Rect> GetCoverage(const Matrix& transform) const = 0;
/// @return `true` if this geometry will completely cover all fragments in
/// `rect` when the `transform` is applied to it.
virtual bool CoversArea(const Matrix& transform, const Rect& rect) const {
return false;
}
};
} // namespace impeller

View File

@@ -47,4 +47,12 @@ std::optional<Rect> RectGeometry::GetCoverage(const Matrix& transform) const {
return rect_.TransformBounds(transform);
}
bool RectGeometry::CoversArea(const Matrix& transform, const Rect& rect) const {
if (!transform.IsTranslationScaleOnly()) {
return false;
}
Rect coverage = rect_.TransformBounds(transform);
return coverage.Contains(rect);
}
} // namespace impeller

View File

@@ -14,6 +14,9 @@ class RectGeometry : public Geometry {
~RectGeometry();
// |Geometry|
bool CoversArea(const Matrix& transform, const Rect& rect) const override;
private:
// |Geometry|
GeometryResult GetPositionBuffer(const ContentContext& renderer,

View File

@@ -18,6 +18,10 @@ class Context;
class RenderPass;
class RenderTarget;
namespace testing {
class CommandBufferMock;
}
//------------------------------------------------------------------------------
/// @brief A collection of encoded commands to be submitted to the GPU for
/// execution. A command buffer is obtained from a graphics
@@ -38,6 +42,8 @@ class RenderTarget;
/// different from the encoding order.
///
class CommandBuffer {
friend class testing::CommandBufferMock;
public:
enum class Status {
kPending,

View File

@@ -57,6 +57,13 @@ class RenderPass {
///
bool EncodeCommands() const;
//----------------------------------------------------------------------------
/// @brief Accessor for the current Commands.
///
/// @details Visible for testing.
///
const std::vector<Command>& GetCommands() const { return commands_; }
protected:
const std::weak_ptr<const Context> context_;
const RenderTarget render_target_;