-
Notifications
You must be signed in to change notification settings - Fork 6k
[Impeller] opt into exp canvas. #54913
Changes from all commits
97160d4
dd06f92
5181dd1
9468843
e2f5268
d198704
0292df8
bf7a396
08f6361
c85e2b4
df4e5e5
b894059
f50102e
be9be26
06cbda0
1c0c46c
c168b96
69884f9
eb2bd85
4248520
3938253
90e2eb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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()); | ||
|
@@ -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."; | ||
} | ||
} | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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(); | ||
|
||
|
@@ -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()), | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fixes flutter/flutter#152366 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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}; | ||
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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 == | ||
|
@@ -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(); | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @flar to double check my math here... There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_); | ||
|
@@ -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; | ||
} | ||
|
@@ -670,6 +716,10 @@ void ExperimentalCanvas::DrawTextFrame( | |
|
||
void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity, | ||
bool reuse_depth) { | ||
if (IsSkipping()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
|
@@ -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_); | ||
|
||
|
@@ -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; | ||
} | ||
|
@@ -763,6 +812,10 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity, | |
} | ||
|
||
void ExperimentalCanvas::AddClipEntityToCurrentPass(Entity entity) { | ||
if (IsSkipping()) { | ||
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
@@ -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); | ||
|
||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)