Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[Impeller] opt into exp canvas. #54913

Merged
merged 22 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion common/config.gni
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ declare_args() {
slimpeller = false

# Opt into new DL dispatcher that skips AIKS layer
experimental_canvas = false
experimental_canvas = true
}

# feature_defines_list ---------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions impeller/aiks/canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ struct CanvasStackEntry {
size_t num_clips = 0u;
Scalar distributed_opacity = 1.0f;
Entity::RenderingMode rendering_mode = Entity::RenderingMode::kDirect;
// Whether all entities in the current save should be skipped.
bool skipping = false;
Copy link
Member Author

Choose a reason for hiding this comment

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

When we encounter a saveLayer that should be skipped, we need a way to ensure that no child entities of that saveLayer get rendered. This is a quickish fix to make this work correctly.

In the future, we could change the dispatcher to use indicies and have a more explicit skip step.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could also do a normal save; clip(Empty)

// Whether subpass coverage was rounded out to pixel coverage, or if false
// truncated.
bool did_round_out = false;
};

enum class PointStyle {
Expand Down
189 changes: 121 additions & 68 deletions impeller/aiks/experimental_canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
// found in the LICENSE file.

#include "impeller/aiks/experimental_canvas.h"

#include <limits>
#include <optional>

#include "fml/logging.h"
#include "fml/trace_event.h"
#include "impeller/aiks/canvas.h"
Expand Down Expand Up @@ -61,7 +64,6 @@ static void ApplyFramebufferBlend(Entity& entity) {
static std::shared_ptr<Texture> FlipBackdrop(
std::vector<LazyRenderingConfig>& render_passes,
Point global_pass_position,
size_t current_clip_depth,
EntityPassClipStack& clip_coverage_stack,
ContentContext& renderer) {
auto rendering_config = std::move(render_passes.back());
Expand Down Expand Up @@ -137,21 +139,16 @@ static std::shared_ptr<Texture> FlipBackdrop(

// Restore any clips that were recorded before the backdrop filter was
// applied.
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(current_clip_depth)) {
auto& replay_entity = next_replay_clip->entity;
auto& replay_entities = clip_coverage_stack.GetReplayEntities();
for (const auto& replay : replay_entities) {
SetClipScissor(
next_replay_clip->clip_coverage,
replay.clip_coverage,
*render_passes.back().inline_pass_context->GetRenderPass(0).pass,
global_pass_position);
if (!replay_entity.Render(
if (!replay.entity.Render(
renderer,
*render_passes.back().inline_pass_context->GetRenderPass(0).pass)) {
VALIDATION_LOG << "Failed to render entity for clip replay.";
VALIDATION_LOG << "Failed to render entity for clip restore.";
}
}

Expand Down Expand Up @@ -296,41 +293,43 @@ void ExperimentalCanvas::SetupRenderPass() {
}
}

void ExperimentalCanvas::SkipUntilMatchingRestore(size_t total_content_depth) {
auto entry = CanvasStackEntry{};
entry.skipping = true;
entry.clip_depth = current_depth_ + total_content_depth;
transform_stack_.push_back(entry);
}

void ExperimentalCanvas::Save(uint32_t total_content_depth) {
if (IsSkipping()) {
return SkipUntilMatchingRestore(total_content_depth);
}

auto entry = CanvasStackEntry{};
entry.transform = transform_stack_.back().transform;
entry.cull_rect = transform_stack_.back().cull_rect;
entry.clip_depth = current_depth_ + total_content_depth;
entry.distributed_opacity = transform_stack_.back().distributed_opacity;
FML_CHECK(entry.clip_depth <= transform_stack_.back().clip_depth)
FML_DCHECK(entry.clip_depth <= transform_stack_.back().clip_depth)
Copy link
Member Author

Choose a reason for hiding this comment

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

I changed these to dchecks

<< entry.clip_depth << " <=? " << transform_stack_.back().clip_depth
<< " after allocating " << total_content_depth;
entry.clip_height = transform_stack_.back().clip_height;
entry.rendering_mode = Entity::RenderingMode::kDirect;
transform_stack_.emplace_back(entry);
transform_stack_.push_back(entry);
}

void ExperimentalCanvas::SaveLayer(
const Paint& paint,
std::optional<Rect> bounds,
const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise,
uint32_t total_content_depth,
bool can_distribute_opacity) {
TRACE_EVENT0("flutter", "Canvas::saveLayer");

std::optional<Rect> ExperimentalCanvas::ComputeCoverageLimit() const {
if (!clip_coverage_stack_.HasCoverage()) {
// The current clip is empty. This means the pass texture won't be
// visible, so skip it.
Save(total_content_depth);
return;
return std::nullopt;
}

auto maybe_current_clip_coverage = clip_coverage_stack_.CurrentClipCoverage();
if (!maybe_current_clip_coverage.has_value()) {
Save(total_content_depth);
return;
return std::nullopt;
}

auto current_clip_coverage = maybe_current_clip_coverage.value();

// The maximum coverage of the subpass. Subpasses textures should never
Expand All @@ -343,8 +342,28 @@ void ExperimentalCanvas::SaveLayer(
.Intersection(current_clip_coverage);

if (!maybe_coverage_limit.has_value() || maybe_coverage_limit->IsEmpty()) {
Save(total_content_depth);
return;
return std::nullopt;
}

return maybe_coverage_limit->Intersection(
Rect::MakeSize(render_target_.GetRenderTargetSize()));
}

void ExperimentalCanvas::SaveLayer(
const Paint& paint,
std::optional<Rect> bounds,
const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise,
uint32_t total_content_depth,
bool can_distribute_opacity) {
TRACE_EVENT0("flutter", "Canvas::saveLayer");
if (IsSkipping()) {
return SkipUntilMatchingRestore(total_content_depth);
}

auto maybe_coverage_limit = ComputeCoverageLimit();
if (!maybe_coverage_limit.has_value()) {
return SkipUntilMatchingRestore(total_content_depth);
}
auto coverage_limit = maybe_coverage_limit.value();

Expand All @@ -356,10 +375,9 @@ void ExperimentalCanvas::SaveLayer(
return;
}

std::shared_ptr<FilterContents> filter_contents;
if (paint.image_filter) {
filter_contents = paint.image_filter->GetFilterContents();
}
std::shared_ptr<FilterContents> filter_contents = paint.WithImageFilter(
Rect(), transform_stack_.back().transform,
Entity::RenderingMode::kSubpassPrependSnapshotTransform);

std::optional<Rect> maybe_subpass_coverage = ComputeSaveLayerCoverage(
bounds.value_or(Rect::MakeMaximum()),
Expand All @@ -371,13 +389,32 @@ void ExperimentalCanvas::SaveLayer(
/*flood_input_coverage=*/!!backdrop_filter //
);

if (!maybe_subpass_coverage.has_value() ||
maybe_subpass_coverage->IsEmpty()) {
Save(total_content_depth);
return;
if (!maybe_subpass_coverage.has_value()) {
return SkipUntilMatchingRestore(total_content_depth);
}

auto subpass_coverage = maybe_subpass_coverage.value();

// When an image filter is present, clamp to avoid flicking due to nearest
// sampled image. For other cases, round out to ensure than any geometry is
// not cut off.
Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, you will be filtering all of the subpass pixels, not just its theoretical coverage.

//
// See also this bug: https://github.com/flutter/flutter/issues/144213
//
// TODO(jonahwilliams): this could still round out for filters that use decal
// sampling mode.
ISize subpass_size;
bool did_round_out = false;
if (paint.image_filter) {
subpass_size = ISize(subpass_coverage.GetSize());
} else {
did_round_out = true;
subpass_size = ISize(IRect::RoundOut(subpass_coverage).GetSize());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't the variations of how you compute the subpass_size have a ramification for the origin? The subpass_coverage was relative to the global position I think? In which case if you just took its size and made a drawable for that then the global position would be the origin of the coverage. But if you rounded it out, then the global position would be the rounded down value of the origin of the coverage?

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm, good point. Right now we're always rounding in https://github.com/flutter/engine/blob/main/impeller/aiks/experimental_canvas.cc#L524-L526

But I think instead we need to take into account whether we rounded in or out, so that we're not a pixel off.

if (subpass_size.IsEmpty()) {
return SkipUntilMatchingRestore(total_content_depth);
}

// Backdrop filter state, ignored if there is no BDF.
std::shared_ptr<FilterContents> backdrop_filter_contents;
Point local_position = {0, 0};
Expand All @@ -393,11 +430,10 @@ void ExperimentalCanvas::SaveLayer(
return filter;
};

auto input_texture = FlipBackdrop(render_passes_, //
GetGlobalPassPosition(), //
std::numeric_limits<uint32_t>::max(), //
clip_coverage_stack_, //
renderer_ //
auto input_texture = FlipBackdrop(render_passes_, //
GetGlobalPassPosition(), //
clip_coverage_stack_, //
renderer_ //
);
if (!input_texture) {
// Validation failures are logged in FlipBackdrop.
Expand All @@ -419,23 +455,24 @@ void ExperimentalCanvas::SaveLayer(
paint_copy.color.alpha *= transform_stack_.back().distributed_opacity;
transform_stack_.back().distributed_opacity = 1.0;

render_passes_.push_back(LazyRenderingConfig(
renderer_, //
CreateRenderTarget(renderer_, //
ISize(subpass_coverage.GetSize()), //
Color::BlackTransparent() //
)));
render_passes_.push_back(
LazyRenderingConfig(renderer_, //
CreateRenderTarget(renderer_, //
subpass_size, //
Color::BlackTransparent() //
)));
save_layer_state_.push_back(SaveLayerState{paint_copy, subpass_coverage});

CanvasStackEntry entry;
entry.transform = transform_stack_.back().transform;
entry.cull_rect = transform_stack_.back().cull_rect;
entry.clip_depth = current_depth_ + total_content_depth;
FML_CHECK(entry.clip_depth <= transform_stack_.back().clip_depth)
FML_DCHECK(entry.clip_depth <= transform_stack_.back().clip_depth)
<< entry.clip_depth << " <=? " << transform_stack_.back().clip_depth
<< " after allocating " << total_content_depth;
entry.clip_height = transform_stack_.back().clip_height;
entry.rendering_mode = Entity::RenderingMode::kSubpassAppendSnapshotTransform;
entry.did_round_out = did_round_out;
transform_stack_.emplace_back(entry);

// The current clip aiks clip culling can not handle image filters.
Expand Down Expand Up @@ -484,10 +521,15 @@ bool ExperimentalCanvas::Restore() {
// to be overly conservative, but we need to jump the depth to
// the clip depth so that the next rendering op will get a
// larger depth (it will pre-increment the current_depth_ value).
FML_CHECK(current_depth_ <= transform_stack_.back().clip_depth)
FML_DCHECK(current_depth_ <= transform_stack_.back().clip_depth)
<< current_depth_ << " <=? " << transform_stack_.back().clip_depth;
current_depth_ = transform_stack_.back().clip_depth;

if (IsSkipping()) {
transform_stack_.pop_back();
return true;
}

if (transform_stack_.back().rendering_mode ==
Entity::RenderingMode::kSubpassAppendSnapshotTransform ||
transform_stack_.back().rendering_mode ==
Expand All @@ -499,12 +541,13 @@ bool ExperimentalCanvas::Restore() {

SaveLayerState save_layer_state = save_layer_state_.back();
save_layer_state_.pop_back();
auto global_pass_position = GetGlobalPassPosition();

std::shared_ptr<Contents> contents =
PaintPassDelegate(save_layer_state.paint)
.CreateContentsForSubpassTarget(
lazy_render_pass.inline_pass_context->GetTexture(),
Matrix::MakeTranslation(Vector3{-GetGlobalPassPosition()}) *
Matrix::MakeTranslation(Vector3{-global_pass_position}) *
transform_stack_.back().transform);

lazy_render_pass.inline_pass_context->EndPass();
Expand All @@ -514,16 +557,19 @@ bool ExperimentalCanvas::Restore() {
// sampling, so aligning here is important for avoiding visual nearest
// sampling errors caused by limited floating point precision when
// straddling a half pixel boundary.
//
// We do this in lieu of expanding/rounding out the subpass coverage in
// order to keep the bounds wrapping consistently tight around subpass
// elements. Which is necessary to avoid intense flickering in cases
// where a subpass texture has a large blur filter with clamp sampling.
//
// See also this bug: https://github.com/flutter/flutter/issues/144213
Point subpass_texture_position =
(save_layer_state.coverage.GetOrigin() - GetGlobalPassPosition())
.Round();
Point subpass_texture_position;
if (transform_stack_.back().did_round_out) {
Copy link
Member Author

Choose a reason for hiding this comment

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

@flar to double check my math here...

Copy link
Contributor

Choose a reason for hiding this comment

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

I think rather than a flag it would be better to have the subpass record "I represent the pixels located at [rect]" in the long run, I think that's all the information you need and it doesn't require the back-half to know how the front-half did its math.

// Subpass coverage was rounded out, origin potentially moved "down" by
// as much as a pixel.
subpass_texture_position =
(save_layer_state.coverage.GetOrigin() - global_pass_position)
.Floor();
} else {
// Subpass coverage was truncated. Pick the closest phyiscal pixel.
subpass_texture_position =
(save_layer_state.coverage.GetOrigin() - global_pass_position)
.Round();
}

Entity element_entity;
element_entity.SetClipDepth(++current_depth_);
Expand All @@ -545,9 +591,9 @@ bool ExperimentalCanvas::Restore() {
// 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).
auto input_texture = FlipBackdrop(
render_passes_, GetGlobalPassPosition(),
element_entity.GetClipDepth(), clip_coverage_stack_, renderer_);
auto input_texture =
FlipBackdrop(render_passes_, GetGlobalPassPosition(),
clip_coverage_stack_, renderer_);
if (!input_texture) {
return false;
}
Expand Down Expand Up @@ -670,6 +716,10 @@ void ExperimentalCanvas::DrawTextFrame(

void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
bool reuse_depth) {
if (IsSkipping()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we test this earlier? Before we make the entity in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

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

Eventually yes, as we won't ever make entities. But that logic is spread throughout the old and new canvas, so moving it earlier isn't easy as the old canvas doesn't use this check yet.

return;
}

entity.SetTransform(
Matrix::MakeTranslation(Vector3(-GetGlobalPassPosition())) *
entity.GetTransform());
Expand Down Expand Up @@ -708,7 +758,7 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
// We can render at a depth up to and including the depth of the currently
// active clips and we will still be clipped out, but we cannot render at
// a depth that is greater than the current clips or we will not be clipped.
FML_CHECK(current_depth_ <= transform_stack_.back().clip_depth)
FML_DCHECK(current_depth_ <= transform_stack_.back().clip_depth)
<< current_depth_ << " <=? " << transform_stack_.back().clip_depth;
entity.SetClipDepth(current_depth_);

Expand All @@ -725,9 +775,8 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
// 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).
auto input_texture =
FlipBackdrop(render_passes_, GetGlobalPassPosition(),
entity.GetClipDepth(), clip_coverage_stack_, renderer_);
auto input_texture = FlipBackdrop(render_passes_, GetGlobalPassPosition(),
clip_coverage_stack_, renderer_);
if (!input_texture) {
return;
}
Expand Down Expand Up @@ -763,6 +812,10 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
}

void ExperimentalCanvas::AddClipEntityToCurrentPass(Entity entity) {
if (IsSkipping()) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, check this sooner?

}

auto transform = entity.GetTransform();
entity.SetTransform(
Matrix::MakeTranslation(Vector3(-GetGlobalPassPosition())) * transform);
Expand All @@ -777,7 +830,7 @@ void ExperimentalCanvas::AddClipEntityToCurrentPass(Entity entity) {
// to know if a clip will actually be used in advance of storing it in
// the DisplayList buffer.
// See https://github.com/flutter/flutter/issues/147021
FML_CHECK(current_depth_ <= transform_stack_.back().clip_depth)
FML_DCHECK(current_depth_ <= transform_stack_.back().clip_depth)
<< current_depth_ << " <=? " << transform_stack_.back().clip_depth;
entity.SetClipDepth(transform_stack_.back().clip_depth);

Expand Down
Loading