[Impeller] Reland 3: Implement draw order optimization. (flutter/engine#54673)
For each clip scope, draw opaque items in reverse order and translucent/backdrop-independent items in their original order afterwards. Clips are treated as translucent by the parent scope. Respects clips, subpass collapse, and the clear color optimization.
This commit is contained in:
@@ -168,6 +168,7 @@
|
||||
../../../flutter/impeller/entity/contents/host_buffer_unittests.cc
|
||||
../../../flutter/impeller/entity/contents/test
|
||||
../../../flutter/impeller/entity/contents/tiled_texture_contents_unittests.cc
|
||||
../../../flutter/impeller/entity/draw_order_resolver_unittests.cc
|
||||
../../../flutter/impeller/entity/entity_pass_target_unittests.cc
|
||||
../../../flutter/impeller/entity/entity_pass_unittests.cc
|
||||
../../../flutter/impeller/entity/entity_unittests.cc
|
||||
|
||||
@@ -42831,6 +42831,8 @@ ORIGIN: ../../../flutter/impeller/entity/contents/tiled_texture_contents.cc + ..
|
||||
ORIGIN: ../../../flutter/impeller/entity/contents/tiled_texture_contents.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/contents/vertices_contents.cc + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/contents/vertices_contents.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/draw_order_resolver.cc + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/draw_order_resolver.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/entity.cc + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/entity.h + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/impeller/entity/entity_pass.cc + ../../../flutter/LICENSE
|
||||
@@ -45714,6 +45716,8 @@ FILE: ../../../flutter/impeller/entity/contents/tiled_texture_contents.cc
|
||||
FILE: ../../../flutter/impeller/entity/contents/tiled_texture_contents.h
|
||||
FILE: ../../../flutter/impeller/entity/contents/vertices_contents.cc
|
||||
FILE: ../../../flutter/impeller/entity/contents/vertices_contents.h
|
||||
FILE: ../../../flutter/impeller/entity/draw_order_resolver.cc
|
||||
FILE: ../../../flutter/impeller/entity/draw_order_resolver.h
|
||||
FILE: ../../../flutter/impeller/entity/entity.cc
|
||||
FILE: ../../../flutter/impeller/entity/entity.h
|
||||
FILE: ../../../flutter/impeller/entity/entity_pass.cc
|
||||
|
||||
@@ -890,5 +890,39 @@ TEST_P(AiksTest, DispatcherDoesNotCullPerspectiveTransformedChildDisplayLists) {
|
||||
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
|
||||
}
|
||||
|
||||
// Results in a 100x100 green square. If any red is drawn, there is a bug.
|
||||
TEST_P(AiksTest, BackdropRestoreUsesCorrectCoverageForFirstRestoredClip) {
|
||||
DisplayListBuilder builder;
|
||||
|
||||
DlPaint paint;
|
||||
// Add a difference clip that cuts out the bottom right corner
|
||||
builder.ClipRect(SkRect::MakeLTRB(50, 50, 100, 100),
|
||||
DlCanvas::ClipOp::kDifference);
|
||||
|
||||
// Draw a red rectangle that's going to be completely covered by green later.
|
||||
paint.setColor(DlColor::kRed());
|
||||
builder.DrawRect(SkRect::MakeLTRB(0, 0, 100, 100), paint);
|
||||
|
||||
// Add a clip restricting the backdrop filter to the top right corner.
|
||||
auto count = builder.GetSaveCount();
|
||||
builder.Save();
|
||||
{
|
||||
builder.ClipRect(SkRect::MakeLTRB(0, 0, 100, 100));
|
||||
{
|
||||
// Create a save layer with a backdrop blur filter.
|
||||
auto backdrop_filter =
|
||||
DlBlurImageFilter::Make(10.0, 10.0, DlTileMode::kDecal);
|
||||
builder.SaveLayer(nullptr, nullptr, backdrop_filter.get());
|
||||
}
|
||||
}
|
||||
builder.RestoreToCount(count);
|
||||
|
||||
// Finally, overwrite all the previous stuff with green.
|
||||
paint.setColor(DlColor::kGreen());
|
||||
builder.DrawRect(SkRect::MakeLTRB(0, 0, 100, 100), paint);
|
||||
|
||||
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace impeller
|
||||
|
||||
@@ -166,6 +166,8 @@ impeller_component("entity") {
|
||||
"contents/tiled_texture_contents.h",
|
||||
"contents/vertices_contents.cc",
|
||||
"contents/vertices_contents.h",
|
||||
"draw_order_resolver.cc",
|
||||
"draw_order_resolver.h",
|
||||
"entity.cc",
|
||||
"entity.h",
|
||||
"entity_pass.cc",
|
||||
@@ -250,6 +252,7 @@ impeller_component("entity_unittests") {
|
||||
"contents/filters/matrix_filter_contents_unittests.cc",
|
||||
"contents/host_buffer_unittests.cc",
|
||||
"contents/tiled_texture_contents_unittests.cc",
|
||||
"draw_order_resolver_unittests.cc",
|
||||
"entity_pass_target_unittests.cc",
|
||||
"entity_pass_unittests.cc",
|
||||
"entity_playground.cc",
|
||||
|
||||
@@ -213,6 +213,11 @@ class ColorSourceContents : public Contents {
|
||||
pass.SetVertexBuffer(std::move(geometry_result.vertex_buffer));
|
||||
options.primitive_type = geometry_result.type;
|
||||
|
||||
// Enable depth writing for all opaque entities in order to allow
|
||||
// reordering. Opaque entities are coerced to source blending by
|
||||
// `EntityPass::AddEntity`.
|
||||
options.depth_write_enabled = options.blend_mode == BlendMode::kSource;
|
||||
|
||||
// Take the pre-populated vertex shader uniform struct and set managed
|
||||
// values.
|
||||
frame_info.mvp = geometry_result.transform;
|
||||
|
||||
@@ -575,16 +575,16 @@ Entity ApplyBlurStyle(FilterContents::BlurStyle blur_style,
|
||||
const ContentContext& renderer, const Entity& entity,
|
||||
RenderPass& pass) mutable {
|
||||
bool result = true;
|
||||
blur_entity.SetClipDepth(entity.GetClipDepth());
|
||||
blur_entity.SetTransform(entity.GetTransform() *
|
||||
blurred_transform);
|
||||
result = result && blur_entity.Render(renderer, pass);
|
||||
snapshot_entity.SetTransform(
|
||||
entity.GetTransform() *
|
||||
Matrix::MakeScale(1.f / source_space_scalar) *
|
||||
snapshot_transform);
|
||||
snapshot_entity.SetClipDepth(entity.GetClipDepth());
|
||||
result = result && snapshot_entity.Render(renderer, pass);
|
||||
blur_entity.SetClipDepth(entity.GetClipDepth());
|
||||
blur_entity.SetTransform(entity.GetTransform() *
|
||||
blurred_transform);
|
||||
result = result && blur_entity.Render(renderer, pass);
|
||||
return result;
|
||||
}),
|
||||
fml::MakeCopyable([blur_entity = blur_entity.Clone(),
|
||||
|
||||
@@ -153,6 +153,9 @@ bool TextureContents::Render(const ContentContext& renderer,
|
||||
}
|
||||
pipeline_options.primitive_type = PrimitiveType::kTriangleStrip;
|
||||
|
||||
pipeline_options.depth_write_enabled =
|
||||
stencil_enabled_ && pipeline_options.blend_mode == BlendMode::kSource;
|
||||
|
||||
pass.SetPipeline(strict_source_rect_enabled_
|
||||
? renderer.GetTextureStrictSrcPipeline(pipeline_options)
|
||||
: renderer.GetTexturePipeline(pipeline_options));
|
||||
|
||||
123
engine/src/flutter/impeller/entity/draw_order_resolver.cc
Normal file
123
engine/src/flutter/impeller/entity/draw_order_resolver.cc
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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/entity/draw_order_resolver.h"
|
||||
|
||||
#include "flutter/fml/logging.h"
|
||||
#include "impeller/base/validation.h"
|
||||
|
||||
namespace impeller {
|
||||
|
||||
DrawOrderResolver::DrawOrderResolver() : draw_order_layers_({{}}){};
|
||||
|
||||
void DrawOrderResolver::AddElement(size_t element_index, bool is_opaque) {
|
||||
DrawOrderLayer& layer = draw_order_layers_.back();
|
||||
if (is_opaque) {
|
||||
layer.opaque_elements.push_back(element_index);
|
||||
} else {
|
||||
layer.dependent_elements.push_back(element_index);
|
||||
}
|
||||
}
|
||||
void DrawOrderResolver::PushClip(size_t element_index) {
|
||||
draw_order_layers_.back().dependent_elements.push_back(element_index);
|
||||
draw_order_layers_.push_back({});
|
||||
};
|
||||
|
||||
void DrawOrderResolver::PopClip() {
|
||||
if (draw_order_layers_.size() == 1u) {
|
||||
// This is likely recoverable, so don't assert.
|
||||
VALIDATION_LOG << "Attemped to pop the first draw order clip layer. This "
|
||||
"may be a bug in `EntityPass`.";
|
||||
return;
|
||||
}
|
||||
|
||||
DrawOrderLayer& layer = draw_order_layers_.back();
|
||||
DrawOrderLayer& parent_layer =
|
||||
draw_order_layers_[draw_order_layers_.size() - 2];
|
||||
|
||||
layer.WriteCombinedDraws(parent_layer.dependent_elements, 0, 0);
|
||||
|
||||
draw_order_layers_.pop_back();
|
||||
}
|
||||
|
||||
void DrawOrderResolver::Flush() {
|
||||
FML_DCHECK(draw_order_layers_.size() >= 1u);
|
||||
|
||||
size_t layer_count = draw_order_layers_.size();
|
||||
|
||||
// Pop all clip layers.
|
||||
while (draw_order_layers_.size() > 1u) {
|
||||
PopClip();
|
||||
}
|
||||
|
||||
// Move the root layer items into the sorted list.
|
||||
DrawOrderLayer& layer = draw_order_layers_.back();
|
||||
if (!first_root_flush_.has_value()) {
|
||||
// Record the first flush.
|
||||
first_root_flush_ = std::move(layer);
|
||||
layer = {};
|
||||
} else {
|
||||
// Write subsequent flushes into the sorted root list.
|
||||
layer.WriteCombinedDraws(sorted_elements_, 0, 0);
|
||||
layer.opaque_elements.clear();
|
||||
layer.dependent_elements.clear();
|
||||
}
|
||||
|
||||
// Refill with empty layers.
|
||||
draw_order_layers_.resize(layer_count);
|
||||
}
|
||||
|
||||
DrawOrderResolver::ElementRefs DrawOrderResolver::GetSortedDraws(
|
||||
size_t opaque_skip_count,
|
||||
size_t translucent_skip_count) const {
|
||||
FML_DCHECK(draw_order_layers_.size() == 1u)
|
||||
<< "Attempted to get sorted draws before all clips were popped.";
|
||||
|
||||
ElementRefs sorted_elements;
|
||||
sorted_elements.reserve(
|
||||
(first_root_flush_.has_value()
|
||||
? first_root_flush_->opaque_elements.size() +
|
||||
first_root_flush_->dependent_elements.size()
|
||||
: 0u) +
|
||||
sorted_elements_.size() +
|
||||
draw_order_layers_.back().opaque_elements.size() +
|
||||
draw_order_layers_.back().dependent_elements.size());
|
||||
|
||||
// Write all flushed items.
|
||||
if (first_root_flush_.has_value()) {
|
||||
first_root_flush_->WriteCombinedDraws(sorted_elements, opaque_skip_count,
|
||||
translucent_skip_count);
|
||||
}
|
||||
sorted_elements.insert(sorted_elements.end(), sorted_elements_.begin(),
|
||||
sorted_elements_.end());
|
||||
|
||||
// Write any remaining non-flushed items.
|
||||
draw_order_layers_.back().WriteCombinedDraws(
|
||||
sorted_elements, first_root_flush_.has_value() ? 0 : opaque_skip_count,
|
||||
first_root_flush_.has_value() ? 0 : translucent_skip_count);
|
||||
|
||||
return sorted_elements;
|
||||
}
|
||||
|
||||
void DrawOrderResolver::DrawOrderLayer::WriteCombinedDraws(
|
||||
ElementRefs& destination,
|
||||
size_t opaque_skip_count,
|
||||
size_t translucent_skip_count) const {
|
||||
FML_DCHECK(opaque_skip_count <= opaque_elements.size());
|
||||
FML_DCHECK(translucent_skip_count <= dependent_elements.size());
|
||||
|
||||
destination.reserve(destination.size() + //
|
||||
opaque_elements.size() - opaque_skip_count + //
|
||||
dependent_elements.size() - translucent_skip_count);
|
||||
|
||||
// Draw backdrop-independent elements in reverse order first.
|
||||
destination.insert(destination.end(), opaque_elements.rbegin(),
|
||||
opaque_elements.rend() - opaque_skip_count);
|
||||
// Then, draw backdrop-dependent elements in their original order.
|
||||
destination.insert(destination.end(),
|
||||
dependent_elements.begin() + translucent_skip_count,
|
||||
dependent_elements.end());
|
||||
}
|
||||
|
||||
} // namespace impeller
|
||||
94
engine/src/flutter/impeller/entity/draw_order_resolver.h
Normal file
94
engine/src/flutter/impeller/entity/draw_order_resolver.h
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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.
|
||||
|
||||
#ifndef FLUTTER_IMPELLER_ENTITY_DRAW_ORDER_RESOLVER_H_
|
||||
#define FLUTTER_IMPELLER_ENTITY_DRAW_ORDER_RESOLVER_H_
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace impeller {
|
||||
|
||||
/// Helper that records draw indices in painter's order and sorts the draws into
|
||||
/// an optimized order based on translucency and clips.
|
||||
class DrawOrderResolver {
|
||||
public:
|
||||
using ElementRefs = std::vector<size_t>;
|
||||
|
||||
DrawOrderResolver();
|
||||
|
||||
void AddElement(size_t element_index, bool is_opaque);
|
||||
|
||||
void PushClip(size_t element_index);
|
||||
|
||||
void PopClip();
|
||||
|
||||
void Flush();
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
/// @brief Returns the sorted draws for the current draw order layer.
|
||||
/// This should only be called after all recording has finished.
|
||||
///
|
||||
/// @param[in] opaque_skip_count The number of opaque elements to skip
|
||||
/// when appending the combined elements.
|
||||
/// This is used for the "clear color"
|
||||
/// optimization.
|
||||
/// @param[in] translucent_skip_count The number of translucent elements to
|
||||
/// skip when appending the combined
|
||||
/// elements. This is used for the
|
||||
/// "clear color" optimization.
|
||||
///
|
||||
ElementRefs GetSortedDraws(size_t opaque_skip_count,
|
||||
size_t translucent_skip_count) const;
|
||||
|
||||
private:
|
||||
/// A data structure for collecting sorted draws for a given "draw order
|
||||
/// layer". Currently these layers just correspond to the local clip stack.
|
||||
struct DrawOrderLayer {
|
||||
/// The list of backdrop-independent elements (always just opaque). These
|
||||
/// are order independent, and so we render these elements in reverse
|
||||
/// painter's order so that they cull one another.
|
||||
ElementRefs opaque_elements;
|
||||
|
||||
/// The list of backdrop-dependent elements with respect to this draw
|
||||
/// order layer. These elements are drawn after all of the independent
|
||||
/// elements.
|
||||
ElementRefs dependent_elements;
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
/// @brief Appends the combined opaque and transparent elements into
|
||||
/// a final destination buffer.
|
||||
///
|
||||
/// @param[in] destination The buffer to append the combined
|
||||
/// elements to.
|
||||
/// @param[in] opaque_skip_count The number of opaque elements to
|
||||
/// skip when appending the combined
|
||||
/// elements. This is used for the
|
||||
/// "clear color" optimization.
|
||||
/// @param[in] translucent_skip_count The number of translucent elements
|
||||
/// to skip when appending the combined
|
||||
/// elements. This is used for the
|
||||
/// "clear color" optimization.
|
||||
///
|
||||
void WriteCombinedDraws(ElementRefs& destination,
|
||||
size_t opaque_skip_count,
|
||||
size_t translucent_skip_count) const;
|
||||
};
|
||||
std::vector<DrawOrderLayer> draw_order_layers_;
|
||||
|
||||
// The first time the root layer is flushed, the layer contents are stored
|
||||
// here. This is done to enable element skipping for the clear color
|
||||
// optimization.
|
||||
std::optional<DrawOrderLayer> first_root_flush_;
|
||||
// All subsequent root flushes are stored here.
|
||||
ElementRefs sorted_elements_;
|
||||
|
||||
DrawOrderResolver(const DrawOrderResolver&) = delete;
|
||||
|
||||
DrawOrderResolver& operator=(const DrawOrderResolver&) = delete;
|
||||
};
|
||||
|
||||
} // namespace impeller
|
||||
|
||||
#endif // FLUTTER_IMPELLER_ENTITY_DRAW_ORDER_RESOLVER_H_
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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 "impeller/entity/draw_order_resolver.h"
|
||||
#include "third_party/googletest/googletest/include/gtest/gtest.h"
|
||||
|
||||
namespace impeller {
|
||||
namespace testing {
|
||||
|
||||
TEST(DrawOrderResolverTest, GetSortedDrawsReturnsCorrectOrderWithNoClips) {
|
||||
DrawOrderResolver resolver;
|
||||
|
||||
// Opaque items.
|
||||
resolver.AddElement(0, true);
|
||||
resolver.AddElement(1, true);
|
||||
// Translucent items.
|
||||
resolver.AddElement(2, false);
|
||||
resolver.AddElement(3, false);
|
||||
|
||||
auto sorted_elements = resolver.GetSortedDraws(0, 0);
|
||||
|
||||
EXPECT_EQ(sorted_elements.size(), 4u);
|
||||
// First, the opaque items are drawn in reverse order.
|
||||
EXPECT_EQ(sorted_elements[0], 1u);
|
||||
EXPECT_EQ(sorted_elements[1], 0u);
|
||||
// Then the translucent items are drawn.
|
||||
EXPECT_EQ(sorted_elements[2], 2u);
|
||||
EXPECT_EQ(sorted_elements[3], 3u);
|
||||
}
|
||||
|
||||
TEST(DrawOrderResolverTest, GetSortedDrawsReturnsCorrectOrderWithClips) {
|
||||
DrawOrderResolver resolver;
|
||||
|
||||
// Items before clip.
|
||||
resolver.AddElement(0, false);
|
||||
resolver.AddElement(1, true);
|
||||
resolver.AddElement(2, false);
|
||||
resolver.AddElement(3, true);
|
||||
|
||||
// Clip.
|
||||
resolver.PushClip(4);
|
||||
{
|
||||
// Clipped items.
|
||||
resolver.AddElement(5, false);
|
||||
resolver.AddElement(6, false);
|
||||
// Clipped translucent items.
|
||||
resolver.AddElement(7, true);
|
||||
resolver.AddElement(8, true);
|
||||
}
|
||||
resolver.PopClip();
|
||||
|
||||
// Items after clip.
|
||||
resolver.AddElement(9, true);
|
||||
resolver.AddElement(10, false);
|
||||
resolver.AddElement(11, true);
|
||||
resolver.AddElement(12, false);
|
||||
|
||||
auto sorted_elements = resolver.GetSortedDraws(0, 0);
|
||||
|
||||
EXPECT_EQ(sorted_elements.size(), 13u);
|
||||
// First, all the non-clipped opaque items are drawn in reverse order.
|
||||
EXPECT_EQ(sorted_elements[0], 11u);
|
||||
EXPECT_EQ(sorted_elements[1], 9u);
|
||||
EXPECT_EQ(sorted_elements[2], 3u);
|
||||
EXPECT_EQ(sorted_elements[3], 1u);
|
||||
// Then, non-clipped translucent items that came before the clip are drawn in
|
||||
// their original order.
|
||||
EXPECT_EQ(sorted_elements[4], 0u);
|
||||
EXPECT_EQ(sorted_elements[5], 2u);
|
||||
|
||||
// Then, the clip and its sorted child items are drawn.
|
||||
EXPECT_EQ(sorted_elements[6], 4u);
|
||||
{
|
||||
// Opaque clipped items are drawn in reverse order.
|
||||
EXPECT_EQ(sorted_elements[7], 8u);
|
||||
EXPECT_EQ(sorted_elements[8], 7u);
|
||||
// Translucent clipped items are drawn.
|
||||
EXPECT_EQ(sorted_elements[9], 5u);
|
||||
EXPECT_EQ(sorted_elements[10], 6u);
|
||||
}
|
||||
// Finally, the non-clipped translucent items which came after the clip are
|
||||
// drawn in their original order.
|
||||
EXPECT_EQ(sorted_elements[11], 10u);
|
||||
EXPECT_EQ(sorted_elements[12], 12u);
|
||||
}
|
||||
|
||||
TEST(DrawOrderResolverTest, GetSortedDrawsRespectsSkipCounts) {
|
||||
DrawOrderResolver resolver;
|
||||
|
||||
// These items will be skipped.
|
||||
resolver.AddElement(0, false);
|
||||
resolver.AddElement(1, true);
|
||||
resolver.AddElement(2, false);
|
||||
// These ones will be included in the final draw list.
|
||||
resolver.AddElement(3, false);
|
||||
resolver.AddElement(4, true);
|
||||
resolver.AddElement(5, true);
|
||||
|
||||
// Form the draw list, skipping elements 0, 1, and 2.
|
||||
// This emulates what happens when entitypass applies the clear color
|
||||
// optimization.
|
||||
auto sorted_elements = resolver.GetSortedDraws(1, 2);
|
||||
|
||||
EXPECT_EQ(sorted_elements.size(), 3u);
|
||||
// First, opaque items are drawn in reverse order.
|
||||
EXPECT_EQ(sorted_elements[0], 5u);
|
||||
EXPECT_EQ(sorted_elements[1], 4u);
|
||||
// Then, translucent items are drawn.
|
||||
EXPECT_EQ(sorted_elements[2], 3u);
|
||||
}
|
||||
|
||||
TEST(DrawOrderResolverTest, GetSortedDrawsReturnsCorrectOrderWithFlush) {
|
||||
DrawOrderResolver resolver;
|
||||
|
||||
resolver.AddElement(0, false);
|
||||
resolver.AddElement(1, true);
|
||||
resolver.AddElement(2, false);
|
||||
resolver.AddElement(3, true);
|
||||
|
||||
resolver.Flush();
|
||||
|
||||
resolver.AddElement(4, false);
|
||||
resolver.AddElement(5, true);
|
||||
resolver.AddElement(6, false);
|
||||
resolver.AddElement(7, true);
|
||||
|
||||
resolver.Flush();
|
||||
|
||||
resolver.AddElement(8, false);
|
||||
resolver.AddElement(9, true);
|
||||
resolver.AddElement(10, false);
|
||||
resolver.AddElement(11, true);
|
||||
|
||||
auto sorted_elements = resolver.GetSortedDraws(1, 1);
|
||||
|
||||
EXPECT_EQ(sorted_elements.size(), 10u);
|
||||
|
||||
// Skipped draws apply to the first flush.
|
||||
EXPECT_EQ(sorted_elements[0], 3u);
|
||||
EXPECT_EQ(sorted_elements[1], 2u);
|
||||
|
||||
EXPECT_EQ(sorted_elements[2], 7u);
|
||||
EXPECT_EQ(sorted_elements[3], 5u);
|
||||
EXPECT_EQ(sorted_elements[4], 4u);
|
||||
EXPECT_EQ(sorted_elements[5], 6u);
|
||||
|
||||
EXPECT_EQ(sorted_elements[6], 11u);
|
||||
EXPECT_EQ(sorted_elements[7], 9u);
|
||||
EXPECT_EQ(sorted_elements[8], 8u);
|
||||
EXPECT_EQ(sorted_elements[9], 10u);
|
||||
}
|
||||
|
||||
} // namespace testing
|
||||
} // namespace impeller
|
||||
@@ -43,6 +43,8 @@ Entity::Entity(Entity&&) = default;
|
||||
|
||||
Entity::Entity(const Entity&) = default;
|
||||
|
||||
Entity& Entity::operator=(Entity&&) = default;
|
||||
|
||||
const Matrix& Entity::GetTransform() const {
|
||||
return transform_;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ class Entity {
|
||||
|
||||
Entity(Entity&&);
|
||||
|
||||
Entity& operator=(Entity&&);
|
||||
|
||||
/// @brief Get the global transform matrix for this Entity.
|
||||
const Matrix& GetTransform() const;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "impeller/entity/contents/filters/inputs/filter_input.h"
|
||||
#include "impeller/entity/contents/framebuffer_blend_contents.h"
|
||||
#include "impeller/entity/contents/texture_contents.h"
|
||||
#include "impeller/entity/draw_order_resolver.h"
|
||||
#include "impeller/entity/entity.h"
|
||||
#include "impeller/entity/entity_pass_clip_stack.h"
|
||||
#include "impeller/entity/inline_pass_context.h"
|
||||
@@ -44,6 +45,10 @@ std::tuple<std::optional<Color>, BlendMode> ElementAsBackgroundColor(
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool EntityPass::IsSubpass(const Element& element) {
|
||||
return std::holds_alternative<std::unique_ptr<EntityPass>>(element);
|
||||
}
|
||||
|
||||
EntityPass::EntityPass() = default;
|
||||
|
||||
EntityPass::~EntityPass() = default;
|
||||
@@ -106,11 +111,14 @@ void EntityPass::AddEntity(Entity entity) {
|
||||
if (entity.GetBlendMode() > Entity::kLastPipelineBlendMode) {
|
||||
advanced_blend_reads_from_pass_texture_ = true;
|
||||
}
|
||||
draw_order_resolver_.AddElement(elements_.size(),
|
||||
entity.GetBlendMode() == BlendMode::kSource);
|
||||
elements_.emplace_back(std::move(entity));
|
||||
}
|
||||
|
||||
void EntityPass::PushClip(Entity entity) {
|
||||
elements_.emplace_back(std::move(entity));
|
||||
draw_order_resolver_.PushClip(elements_.size() - 1);
|
||||
active_clips_.emplace_back(elements_.size() - 1);
|
||||
}
|
||||
|
||||
@@ -129,6 +137,7 @@ void EntityPass::PopClips(size_t num_clips, uint64_t depth) {
|
||||
FML_DCHECK(element);
|
||||
element->SetClipDepth(depth);
|
||||
active_clips_.pop_back();
|
||||
draw_order_resolver_.PopClip();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +280,14 @@ EntityPass* EntityPass::AddSubpass(std::unique_ptr<EntityPass> pass) {
|
||||
FML_DCHECK(pass->superpass_ == nullptr);
|
||||
pass->superpass_ = this;
|
||||
|
||||
if (pass->backdrop_filter_proc_) {
|
||||
bool has_backdrop_filter = pass->backdrop_filter_proc_ != nullptr;
|
||||
if (has_backdrop_filter) {
|
||||
backdrop_filter_reads_from_pass_texture_ = true;
|
||||
|
||||
// Since backdrop filters trigger the RenderPass to end and lose all depth
|
||||
// information for opaque draws, this is a hard barrier for the draw order
|
||||
// optimization. Flush all sorted draws accumulated up to this point.
|
||||
draw_order_resolver_.Flush();
|
||||
}
|
||||
if (pass->blend_mode_ > Entity::kLastPipelineBlendMode) {
|
||||
advanced_blend_reads_from_pass_texture_ = true;
|
||||
@@ -280,6 +295,12 @@ EntityPass* EntityPass::AddSubpass(std::unique_ptr<EntityPass> pass) {
|
||||
|
||||
auto subpass_pointer = pass.get();
|
||||
elements_.emplace_back(std::move(pass));
|
||||
|
||||
draw_order_resolver_.AddElement(elements_.size() - 1, false);
|
||||
if (has_backdrop_filter) {
|
||||
draw_order_resolver_.Flush();
|
||||
}
|
||||
|
||||
return subpass_pointer;
|
||||
}
|
||||
|
||||
@@ -744,6 +765,55 @@ bool EntityPass::RenderElement(Entity& element_entity,
|
||||
ContentContext& renderer,
|
||||
EntityPassClipStack& clip_coverage_stack,
|
||||
Point global_pass_position) const {
|
||||
// Setup advanced blends.
|
||||
if (element_entity.GetBlendMode() > Entity::kLastPipelineBlendMode) {
|
||||
if (renderer.GetDeviceCapabilities().SupportsFramebufferFetch()) {
|
||||
auto src_contents = element_entity.GetContents();
|
||||
auto contents = std::make_shared<FramebufferBlendContents>();
|
||||
contents->SetChildContents(src_contents);
|
||||
contents->SetBlendMode(element_entity.GetBlendMode());
|
||||
element_entity.SetContents(std::move(contents));
|
||||
element_entity.SetBlendMode(BlendMode::kSource);
|
||||
} else {
|
||||
// End the active pass and flush the buffer before rendering
|
||||
// "advanced" blends. Advanced blends work by binding the current
|
||||
// render target texture as an input ("destination"), blending with a
|
||||
// second texture input ("source"), writing the result to an
|
||||
// intermediate texture, and finally copying the data from the
|
||||
// intermediate texture back to the render target texture. And so all
|
||||
// of the commands that have written to the render target texture so
|
||||
// far need to execute before it's bound for blending (otherwise the
|
||||
// blend pass will end up executing before all the previous commands
|
||||
// in the active pass).
|
||||
|
||||
if (!pass_context.EndPass()) {
|
||||
VALIDATION_LOG
|
||||
<< "Failed to end the current render pass in order to read "
|
||||
"from "
|
||||
"the backdrop texture and apply an advanced blend.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Amend an advanced blend filter to the contents, attaching the pass
|
||||
// texture.
|
||||
auto texture = pass_context.GetTexture();
|
||||
if (!texture) {
|
||||
VALIDATION_LOG << "Failed to fetch the color texture in order to "
|
||||
"apply an advanced blend.";
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterInput::Vector inputs = {
|
||||
FilterInput::Make(texture, element_entity.GetTransform().Invert()),
|
||||
FilterInput::Make(element_entity.GetContents())};
|
||||
auto contents =
|
||||
ColorFilterContents::MakeBlend(element_entity.GetBlendMode(), inputs);
|
||||
contents->SetCoverageHint(element_entity.GetCoverage());
|
||||
element_entity.SetContents(std::move(contents));
|
||||
element_entity.SetBlendMode(BlendMode::kSource);
|
||||
}
|
||||
}
|
||||
|
||||
auto result = pass_context.GetRenderPass(pass_depth);
|
||||
if (!result.pass) {
|
||||
// Failure to produce a render pass should be explained by specific errors
|
||||
@@ -776,15 +846,19 @@ bool EntityPass::RenderElement(Entity& element_entity,
|
||||
}
|
||||
|
||||
if (result.just_created) {
|
||||
// Restore any clips that were recorded before the backdrop filter was
|
||||
// applied.
|
||||
auto& replay_entities = clip_coverage_stack.GetReplayEntities();
|
||||
for (const auto& replay : replay_entities) {
|
||||
SetClipScissor(clip_coverage_stack.CurrentClipCoverage(), *result.pass,
|
||||
global_pass_position);
|
||||
if (!replay.entity.Render(renderer, *result.pass)) {
|
||||
VALIDATION_LOG << "Failed to render entity for clip restore.";
|
||||
}
|
||||
clip_coverage_stack.ActivateClipReplay();
|
||||
}
|
||||
|
||||
// If there are any pending clips to replay, render any that may affect
|
||||
// the entity we're about to render.
|
||||
while (const EntityPassClipStack::ReplayResult* next_replay_clip =
|
||||
clip_coverage_stack.GetNextReplayResult(element_entity)) {
|
||||
auto& replay_entity = next_replay_clip->entity;
|
||||
SetClipScissor(next_replay_clip->clip_coverage, *result.pass,
|
||||
global_pass_position);
|
||||
if (!replay_entity.Render(renderer, *result.pass)) {
|
||||
VALIDATION_LOG << "Failed to render entity for clip replay.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,21 +965,87 @@ bool EntityPass::OnRender(
|
||||
renderer, clip_coverage_stack, global_pass_position);
|
||||
}
|
||||
|
||||
bool is_collapsing_clear_colors = !collapsed_parent_pass &&
|
||||
// Backdrop filters act as a entity before
|
||||
// everything and disrupt the optimization.
|
||||
!backdrop_filter_proc_;
|
||||
for (const auto& element : elements_) {
|
||||
// Skip elements that are incorporated into the clear color.
|
||||
if (is_collapsing_clear_colors) {
|
||||
auto [entity_color, _] =
|
||||
ElementAsBackgroundColor(element, clear_color_size);
|
||||
if (entity_color.has_value()) {
|
||||
continue;
|
||||
}
|
||||
is_collapsing_clear_colors = false;
|
||||
}
|
||||
bool should_collapse_clear_colors =
|
||||
!collapsed_parent_pass &&
|
||||
// Backdrop filters act as a entity before
|
||||
// everything and disrupt the optimization.
|
||||
!backdrop_filter_proc_;
|
||||
|
||||
// Count the number of elements eaten by the clear color optimization. Break
|
||||
// it down in terms of opaque and translucent elements so that we can skip
|
||||
// over these entities when applying the clear color optimization.
|
||||
size_t opaque_clear_entity_count = 0;
|
||||
size_t translucent_clear_entity_count = 0;
|
||||
if (should_collapse_clear_colors) {
|
||||
for (const auto& element : elements_) {
|
||||
if (const Entity* entity = std::get_if<Entity>(&element)) {
|
||||
std::optional<Color> entity_color =
|
||||
entity->AsBackgroundColor(clear_color_size);
|
||||
if (entity_color.has_value()) {
|
||||
if (entity->GetBlendMode() == BlendMode::kSource) {
|
||||
opaque_clear_entity_count++;
|
||||
} else {
|
||||
translucent_clear_entity_count++;
|
||||
}
|
||||
// We've found an entity that replaces the whole background color of
|
||||
// this layer, so continue counting.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We came across an element that doesn't replace the background color of
|
||||
// this layer, so stop counting.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
using ElementCallback = std::function<bool(const Element&)>;
|
||||
using ElementIterator = std::function<bool(const ElementCallback&)>;
|
||||
|
||||
ElementIterator element_iterator;
|
||||
|
||||
if (renderer.GetDeviceCapabilities().SupportsFramebufferFetch()) {
|
||||
element_iterator =
|
||||
[this, &opaque_clear_entity_count,
|
||||
&translucent_clear_entity_count](const ElementCallback& callback) {
|
||||
const auto& sorted_elements = draw_order_resolver_.GetSortedDraws(
|
||||
opaque_clear_entity_count, translucent_clear_entity_count);
|
||||
|
||||
for (const auto& element_ref : sorted_elements) {
|
||||
const Element& element = elements_[element_ref];
|
||||
if (!callback(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
} else {
|
||||
// If framebuffer fetch isn't supported, just disable the draw order
|
||||
// optimization. We could technically make it work by flushing each time
|
||||
// we encounter an advanced blend at recording time down the road.
|
||||
element_iterator = [this, &opaque_clear_entity_count,
|
||||
&translucent_clear_entity_count](
|
||||
const ElementCallback& callback) {
|
||||
size_t skips = opaque_clear_entity_count + translucent_clear_entity_count;
|
||||
for (const auto& element : elements_) {
|
||||
if (skips > 0) {
|
||||
skips--;
|
||||
continue;
|
||||
}
|
||||
if (!callback(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const auto& render_element = [&, this](Entity& entity) {
|
||||
return RenderElement(entity, clip_height_floor, pass_context, pass_depth,
|
||||
renderer, clip_coverage_stack, global_pass_position);
|
||||
};
|
||||
|
||||
std::optional<Entity> deferred_entity;
|
||||
bool result = element_iterator([&](const Element& element) {
|
||||
EntityResult result =
|
||||
GetEntityForElement(element, // element
|
||||
renderer, // renderer
|
||||
@@ -924,70 +1064,36 @@ bool EntityPass::OnRender(
|
||||
// in `GetEntityForElement()`.
|
||||
return false;
|
||||
case EntityResult::kSkip:
|
||||
continue;
|
||||
return true;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/// Setup advanced blends.
|
||||
///
|
||||
|
||||
if (result.entity.GetBlendMode() > Entity::kLastPipelineBlendMode) {
|
||||
if (renderer.GetDeviceCapabilities().SupportsFramebufferFetch()) {
|
||||
auto src_contents = result.entity.GetContents();
|
||||
auto contents = std::make_shared<FramebufferBlendContents>();
|
||||
contents->SetChildContents(src_contents);
|
||||
contents->SetBlendMode(result.entity.GetBlendMode());
|
||||
result.entity.SetContents(std::move(contents));
|
||||
result.entity.SetBlendMode(BlendMode::kSource);
|
||||
} else {
|
||||
// End the active pass and flush the buffer before rendering "advanced"
|
||||
// blends. Advanced blends work by binding the current render target
|
||||
// texture as an input ("destination"), blending with a second texture
|
||||
// input ("source"), writing the result to an intermediate texture, and
|
||||
// finally copying the data from the intermediate texture back to the
|
||||
// render target texture. And so all of the commands that have written
|
||||
// to the render target texture so far need to execute before it's bound
|
||||
// for blending (otherwise the blend pass will end up executing before
|
||||
// all the previous commands in the active pass).
|
||||
|
||||
if (!pass_context.EndPass()) {
|
||||
VALIDATION_LOG
|
||||
<< "Failed to end the current render pass in order to read from "
|
||||
"the backdrop texture and apply an advanced blend.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Amend an advanced blend filter to the contents, attaching the pass
|
||||
// texture.
|
||||
auto texture = pass_context.GetTexture();
|
||||
if (!texture) {
|
||||
VALIDATION_LOG << "Failed to fetch the color texture in order to "
|
||||
"apply an advanced blend.";
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterInput::Vector inputs = {
|
||||
FilterInput::Make(texture, result.entity.GetTransform().Invert()),
|
||||
FilterInput::Make(result.entity.GetContents())};
|
||||
auto contents = ColorFilterContents::MakeBlend(
|
||||
result.entity.GetBlendMode(), inputs);
|
||||
contents->SetCoverageHint(result.entity.GetCoverage());
|
||||
result.entity.SetContents(std::move(contents));
|
||||
result.entity.SetBlendMode(BlendMode::kSource);
|
||||
if (deferred_entity.has_value() &&
|
||||
result.entity.GetBlendMode() != BlendMode::kSource) {
|
||||
if (!render_element(*deferred_entity)) {
|
||||
return false;
|
||||
}
|
||||
deferred_entity.reset();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
/// Render the Element.
|
||||
///
|
||||
if (!RenderElement(result.entity, clip_height_floor, pass_context,
|
||||
pass_depth, renderer, clip_coverage_stack,
|
||||
global_pass_position)) {
|
||||
// Specific validation logs are handled in `render_element()`.
|
||||
return false;
|
||||
if (IsSubpass(element)) {
|
||||
if (deferred_entity.has_value()) {
|
||||
if (!render_element(*deferred_entity)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
deferred_entity = std::move(result.entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
return render_element(result.entity);
|
||||
});
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deferred_entity.has_value() && !render_element(*deferred_entity)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "impeller/entity/contents/contents.h"
|
||||
#include "impeller/entity/contents/filters/filter_contents.h"
|
||||
#include "impeller/entity/draw_order_resolver.h"
|
||||
#include "impeller/entity/entity.h"
|
||||
#include "impeller/entity/entity_pass_clip_stack.h"
|
||||
#include "impeller/entity/entity_pass_delegate.h"
|
||||
@@ -53,6 +54,8 @@ class EntityPass {
|
||||
/// `GetEntityForElement()`.
|
||||
using Element = std::variant<Entity, std::unique_ptr<EntityPass>>;
|
||||
|
||||
static bool IsSubpass(const Element& element);
|
||||
|
||||
using BackdropFilterProc = std::function<std::shared_ptr<FilterContents>(
|
||||
FilterInput::Ref,
|
||||
const Matrix& effect_transform,
|
||||
@@ -214,8 +217,7 @@ class EntityPass {
|
||||
kSkip,
|
||||
};
|
||||
|
||||
/// @brief The resulting entity that should be rendered. If `std::nullopt`,
|
||||
/// there is nothing to render.
|
||||
/// @brief The resulting entity that should be rendered.
|
||||
Entity entity;
|
||||
/// @brief This is set to `false` if there was an unexpected rendering
|
||||
/// error while resolving the Entity.
|
||||
@@ -316,10 +318,12 @@ class EntityPass {
|
||||
/// evaluated and recorded to an `EntityPassTarget` by the `OnRender` method.
|
||||
std::vector<Element> elements_;
|
||||
|
||||
DrawOrderResolver draw_order_resolver_;
|
||||
|
||||
/// The stack of currently active clips (during Aiks recording time). Each
|
||||
/// entry is an index into the `elements_` list. The depth value of a clip is
|
||||
/// the max of all the entities it affects, so assignment of the depth value
|
||||
/// is deferred until clip restore or end of the EntityPass.
|
||||
/// entry is an index into the `elements_` list. The depth value of a clip
|
||||
/// is the max of all the entities it affects, so assignment of the depth
|
||||
/// value is deferred until clip restore or end of the EntityPass.
|
||||
std::vector<size_t> active_clips_;
|
||||
|
||||
EntityPass* superpass_ = nullptr;
|
||||
@@ -338,8 +342,8 @@ class EntityPass {
|
||||
/// 1. An entity with an "advanced blend" is added to the pass.
|
||||
/// 2. A subpass with a backdrop filter is added to the pass.
|
||||
/// These are tracked as separate values because we may ignore
|
||||
/// `blend_reads_from_pass_texture_` if the device supports framebuffer based
|
||||
/// advanced blends.
|
||||
/// `blend_reads_from_pass_texture_` if the device supports framebuffer
|
||||
/// based advanced blends.
|
||||
bool advanced_blend_reads_from_pass_texture_ = false;
|
||||
bool backdrop_filter_reads_from_pass_texture_ = false;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "impeller/entity/entity_pass_clip_stack.h"
|
||||
|
||||
#include "flutter/fml/logging.h"
|
||||
#include "impeller/entity/contents/clip_contents.h"
|
||||
#include "impeller/entity/entity.h"
|
||||
|
||||
@@ -37,10 +39,12 @@ void EntityPassClipStack::PushSubpass(std::optional<Rect> subpass_coverage,
|
||||
.clip_height = clip_height},
|
||||
},
|
||||
});
|
||||
next_replay_index_ = 0;
|
||||
}
|
||||
|
||||
void EntityPassClipStack::PopSubpass() {
|
||||
subpass_state_.pop_back();
|
||||
next_replay_index_ = subpass_state_.back().rendered_clip_entities.size();
|
||||
}
|
||||
|
||||
const std::vector<ClipCoverageLayer>
|
||||
@@ -138,12 +142,22 @@ void EntityPassClipStack::RecordEntity(const Entity& entity,
|
||||
case Contents::ClipCoverage::Type::kNoChange:
|
||||
return;
|
||||
case Contents::ClipCoverage::Type::kAppend:
|
||||
FML_DCHECK(next_replay_index_ ==
|
||||
subpass_state.rendered_clip_entities.size())
|
||||
<< "Not all clips have been replayed before appending new clip.";
|
||||
subpass_state.rendered_clip_entities.push_back(
|
||||
{.entity = entity.Clone(), .clip_coverage = clip_coverage});
|
||||
next_replay_index_++;
|
||||
break;
|
||||
case Contents::ClipCoverage::Type::kRestore:
|
||||
FML_DCHECK(next_replay_index_ <=
|
||||
subpass_state.rendered_clip_entities.size());
|
||||
if (!subpass_state.rendered_clip_entities.empty()) {
|
||||
subpass_state.rendered_clip_entities.pop_back();
|
||||
|
||||
if (next_replay_index_ > subpass_state.rendered_clip_entities.size()) {
|
||||
next_replay_index_ = subpass_state.rendered_clip_entities.size();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -159,4 +173,27 @@ EntityPassClipStack::GetReplayEntities() const {
|
||||
return subpass_state_.back().rendered_clip_entities;
|
||||
}
|
||||
|
||||
void EntityPassClipStack::ActivateClipReplay() {
|
||||
next_replay_index_ = 0;
|
||||
}
|
||||
|
||||
const EntityPassClipStack::ReplayResult*
|
||||
EntityPassClipStack::GetNextReplayResult(const Entity& entity) {
|
||||
if (next_replay_index_ >=
|
||||
subpass_state_.back().rendered_clip_entities.size()) {
|
||||
// No clips need to be replayed.
|
||||
return nullptr;
|
||||
}
|
||||
ReplayResult* next_replay =
|
||||
&subpass_state_.back().rendered_clip_entities[next_replay_index_];
|
||||
if (next_replay->entity.GetClipDepth() < entity.GetClipDepth()) {
|
||||
// The next replay clip doesn't affect the current entity, so don't replay
|
||||
// it yet.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
next_replay_index_++;
|
||||
return next_replay;
|
||||
}
|
||||
|
||||
} // namespace impeller
|
||||
|
||||
@@ -62,9 +62,14 @@ class EntityPassClipStack {
|
||||
Contents::ClipCoverage::Type type,
|
||||
std::optional<Rect> clip_coverage);
|
||||
|
||||
// Visible for testing.
|
||||
const std::vector<ReplayResult>& GetReplayEntities() const;
|
||||
|
||||
void ActivateClipReplay();
|
||||
|
||||
/// @brief Returns the next Entity that should be replayed. If there are no
|
||||
/// enities to replay, then nullptr is returned.
|
||||
const ReplayResult* GetNextReplayResult(const Entity& entity);
|
||||
|
||||
// Visible for testing.
|
||||
const std::vector<ClipCoverageLayer> GetClipCoverageLayers() const;
|
||||
|
||||
@@ -77,6 +82,7 @@ class EntityPassClipStack {
|
||||
SubpassState& GetCurrentSubpassState();
|
||||
|
||||
std::vector<SubpassState> subpass_state_;
|
||||
size_t next_replay_index_ = 0;
|
||||
};
|
||||
|
||||
} // namespace impeller
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
digest.json
|
||||
impeller_GoldenTests_ConicalGradient.png
|
||||
impeller_Play_AiksTest_BackdropRestoreUsesCorrectCoverageForFirstRestoredClip_Metal.png
|
||||
impeller_Play_AiksTest_BackdropRestoreUsesCorrectCoverageForFirstRestoredClip_OpenGLES.png
|
||||
impeller_Play_AiksTest_BackdropRestoreUsesCorrectCoverageForFirstRestoredClip_Vulkan.png
|
||||
impeller_Play_AiksTest_BlendModeClear_Metal.png
|
||||
impeller_Play_AiksTest_BlendModeClear_OpenGLES.png
|
||||
impeller_Play_AiksTest_BlendModeClear_Vulkan.png
|
||||
|
||||
Reference in New Issue
Block a user