Skip to content

Commit

Permalink
[Impeller] Switch from AIKS canvas to DL based canvas implementation. (
Browse files Browse the repository at this point in the history
…#53781)

The first part of switching Impeller/Aiks to using the display list instead of re-recording rendering operations. This should eventually let us cut CPU overhead of the raster thread for complex applications, though it should have no impact on GPU performance.

This does introduce a GLES only rendering bug that I haven't had luck tracking down, but is almost certainly due to switching to DL computed depth values. I'd like to handle this as a follow up when we prioritize GLES. flutter/flutter#153504

Part of flutter/flutter#142054
  • Loading branch information
jonahwilliams authored Aug 15, 2024
1 parent e783ec6 commit f2c7fcf
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 102 deletions.
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
21 changes: 17 additions & 4 deletions impeller/aiks/aiks_playground.cc
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ bool AiksPlayground::ImGuiBegin(const char* name,

bool AiksPlayground::OpenPlaygroundHere(
const sk_sp<flutter::DisplayList>& list) {
DlDispatcher dispatcher;
list->Dispatch(dispatcher);
Picture picture = dispatcher.EndRecordingAsPicture();
return OpenPlaygroundHere(std::move(picture));
return OpenPlaygroundHere([list]() { return list; });
}

bool AiksPlayground::OpenPlaygroundHere(
Expand All @@ -91,12 +88,28 @@ bool AiksPlayground::OpenPlaygroundHere(

return Playground::OpenPlaygroundHere(
[&renderer, &callback](RenderTarget& render_target) -> bool {
#if EXPERIMENTAL_CANVAS
auto display_list = callback();
TextFrameDispatcher collector(renderer.GetContentContext(), Matrix());
display_list->Dispatch(collector);

ExperimentalDlDispatcher impeller_dispatcher(
renderer.GetContentContext(), render_target,
display_list->root_has_backdrop_filter(),
display_list->max_root_blend_mode(), IRect::MakeMaximum());
display_list->Dispatch(impeller_dispatcher);
impeller_dispatcher.FinishRecording();
renderer.GetContentContext().GetTransientsBuffer().Reset();
renderer.GetContentContext().GetLazyGlyphAtlas()->ResetTextFrames();
return true;
#else
auto display_list = callback();
DlDispatcher dispatcher;
display_list->Dispatch(dispatcher);
Picture picture = dispatcher.EndRecordingAsPicture();

return renderer.Render(picture, render_target, true);
#endif // EXPERIMENTAL_CANVAS
});
}

Expand Down
3 changes: 2 additions & 1 deletion impeller/aiks/canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,8 @@ void Canvas::SaveLayer(const Paint& paint,
const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise,
uint32_t total_content_depth,
bool can_distribute_opacity) {
bool can_distribute_opacity,
bool bounds_from_caller) {
if (can_distribute_opacity && !backdrop_filter &&
Paint::CanApplyOpacityPeephole(paint) &&
bounds_promise != ContentBoundsPromise::kMayClipContents) {
Expand Down
3 changes: 2 additions & 1 deletion impeller/aiks/canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class Canvas {
const std::shared_ptr<ImageFilter>& backdrop_filter = nullptr,
ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown,
uint32_t total_content_depth = kMaxDepth,
bool can_distribute_opacity = false);
bool can_distribute_opacity = false,
bool bounds_from_caller = false);

virtual bool Restore();

Expand Down
180 changes: 106 additions & 74 deletions impeller/aiks/experimental_canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "impeller/aiks/experimental_canvas.h"
#include "fml/logging.h"
#include "fml/trace_event.h"
#include "impeller/aiks/canvas.h"
#include "impeller/aiks/paint_pass_delegate.h"
#include "impeller/base/validation.h"
Expand Down Expand Up @@ -82,26 +83,28 @@ static std::shared_ptr<Texture> FlipBackdrop(
// unbalanced save layers. Ideally, this method would return false and the
// renderer could handle that by terminating dispatch.
render_passes.push_back(LazyRenderingConfig(
renderer, std::move(rendering_config.entity_pass_target)));
renderer, std::move(rendering_config.entity_pass_target),
std::move(rendering_config.inline_pass_context)));
return nullptr;
}

std::shared_ptr<Texture> input_texture =
rendering_config.entity_pass_target->Flip(
*renderer.GetContext()->GetResourceAllocator());
rendering_config.inline_pass_context->GetTexture();

if (!input_texture) {
VALIDATION_LOG << "Failed to fetch the color texture in order to "
"apply an advanced blend or backdrop filter.";

// Note: see above.
render_passes.push_back(LazyRenderingConfig(
renderer, std::move(rendering_config.entity_pass_target)));
renderer, std::move(rendering_config.entity_pass_target),
std::move(rendering_config.inline_pass_context)));
return nullptr;
}

render_passes.push_back(LazyRenderingConfig(
renderer, std::move(rendering_config.entity_pass_target)));
renderer, std::move(rendering_config.entity_pass_target),
std::move(rendering_config.inline_pass_context)));
// Eagerly restore the BDF contents.

// If the pass context returns a backdrop texture, we need to draw it to the
Expand Down Expand Up @@ -157,7 +160,6 @@ static const constexpr RenderTarget::AttachmentConfig kDefaultStencilConfig =
static std::unique_ptr<EntityPassTarget> CreateRenderTarget(
ContentContext& renderer,
ISize size,
int mip_count,
const Color& clear_color) {
const std::shared_ptr<Context>& context = renderer.GetContext();

Expand All @@ -166,18 +168,12 @@ static std::unique_ptr<EntityPassTarget> CreateRenderTarget(
/// What's important is the `StorageMode` of the textures, which cannot be
/// changed for the lifetime of the textures.

if (context->GetBackendType() == Context::BackendType::kOpenGLES) {
// TODO(https://github.com/flutter/flutter/issues/141732): Implement mip map
// generation on opengles.
mip_count = 1;
}

RenderTarget target;
if (context->GetCapabilities()->SupportsOffscreenMSAA()) {
target = renderer.GetRenderTargetCache()->CreateOffscreenMSAA(
/*context=*/*context,
/*size=*/size,
/*mip_count=*/mip_count,
/*mip_count=*/1,
/*label=*/"EntityPass",
/*color_attachment_config=*/
RenderTarget::AttachmentConfigMSAA{
Expand All @@ -191,7 +187,7 @@ static std::unique_ptr<EntityPassTarget> CreateRenderTarget(
target = renderer.GetRenderTargetCache()->CreateOffscreen(
*context, // context
size, // size
/*mip_count=*/mip_count,
/*mip_count=*/1,
"EntityPass", // label
RenderTarget::AttachmentConfig{
.storage_mode = StorageMode::kDevicePrivate,
Expand Down Expand Up @@ -273,12 +269,10 @@ void ExperimentalCanvas::SetupRenderPass() {
// a second save layer with the same dimensions as the onscreen. When
// rendering is completed, we must blit this saveLayer to the onscreen.
if (requires_readback_) {
auto entity_pass_target = CreateRenderTarget(
renderer_, //
color0.texture->GetSize(), //
// Note: this is incorrect, we also need to know what kind of filter.
/*mip_count=*/4, //
/*clear_color=*/Color::BlackTransparent());
auto entity_pass_target =
CreateRenderTarget(renderer_, //
color0.texture->GetSize(), //
/*clear_color=*/Color::BlackTransparent());
render_passes_.push_back(
LazyRenderingConfig(renderer_, std::move(entity_pass_target)));
} else {
Expand Down Expand Up @@ -312,47 +306,54 @@ void ExperimentalCanvas::SaveLayer(
const std::shared_ptr<ImageFilter>& backdrop_filter,
ContentBoundsPromise bounds_promise,
uint32_t total_content_depth,
bool can_distribute_opacity) {
// Can we always guarantee that we get a bounds? Does a lack of bounds
// indicate something?
if (!bounds.has_value()) {
bounds = Rect::MakeSize(render_target_.GetRenderTargetSize());
bool can_distribute_opacity,
bool bounds_from_caller) {
TRACE_EVENT0("flutter", "Canvas::saveLayer");

if (bounds.has_value() && bounds->IsEmpty()) {
Save(total_content_depth);
return;
}

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;
}

// SaveLayer is a no-op, depending on the bounds promise. Should/Can DL elide
// this?
if (bounds->IsEmpty()) {
auto maybe_current_clip_coverage = clip_coverage_stack_.CurrentClipCoverage();
if (!maybe_current_clip_coverage.has_value()) {
Save(total_content_depth);
return;
}
auto current_clip_coverage = maybe_current_clip_coverage.value();

// The maximum coverage of the subpass. Subpasses textures should never
// extend outside the parent pass texture or the current clip coverage.
Rect coverage_limit = Rect::MakeOriginSize(
GetGlobalPassPosition(),
Size(render_passes_.back().inline_pass_context->GetTexture()->GetSize()));
std::optional<Rect> maybe_coverage_limit =
Rect::MakeOriginSize(GetGlobalPassPosition(),
Size(render_passes_.back()
.inline_pass_context->GetTexture()
->GetSize()))
.Intersection(current_clip_coverage);

if (!maybe_coverage_limit.has_value()) {
Save(total_content_depth);
return;
}
maybe_coverage_limit = maybe_coverage_limit->Intersection(
Rect::MakeSize(render_target_.GetRenderTargetSize()));

// BDF No-op. need to do some precomputation to ensure this is fully skipped.
if (backdrop_filter) {
if (!clip_coverage_stack_.HasCoverage()) {
Save(total_content_depth);
return;
}
auto maybe_clip_coverage = clip_coverage_stack_.CurrentClipCoverage();
if (!maybe_clip_coverage.has_value()) {
Save(total_content_depth);
return;
}
auto clip_coverage = maybe_clip_coverage.value();
if (clip_coverage.IsEmpty() ||
!coverage_limit.IntersectsWithRect(clip_coverage)) {
Save(total_content_depth);
return;
}
if (!maybe_coverage_limit.has_value() || maybe_coverage_limit->IsEmpty()) {
Save(total_content_depth);
return;
}
auto coverage_limit = maybe_coverage_limit.value();

if (can_distribute_opacity && !backdrop_filter &&
Paint::CanApplyOpacityPeephole(paint)) {
Paint::CanApplyOpacityPeephole(paint) &&
bounds_promise != ContentBoundsPromise::kMayClipContents) {
Save(total_content_depth);
transform_stack_.back().distributed_opacity *= paint.color.alpha;
return;
Expand All @@ -362,11 +363,8 @@ void ExperimentalCanvas::SaveLayer(
std::shared_ptr<FilterContents> backdrop_filter_contents;
Point local_position = {0, 0};
if (backdrop_filter) {
auto current_clip_coverage = clip_coverage_stack_.CurrentClipCoverage();
if (current_clip_coverage.has_value()) {
local_position =
current_clip_coverage->GetOrigin() - GetGlobalPassPosition();
}
local_position =
current_clip_coverage.GetOrigin() - GetGlobalPassPosition();
EntityPass::BackdropFilterProc backdrop_filter_proc =
[backdrop_filter = backdrop_filter->Clone()](
const FilterInput::Ref& input, const Matrix& effect_transform,
Expand Down Expand Up @@ -401,23 +399,25 @@ void ExperimentalCanvas::SaveLayer(

// Backdrop Filter must expand bounds to at least the clip stack, otherwise
// the coverage of the parent render pass.
Rect subpass_coverage = bounds->TransformBounds(GetCurrentTransform());
if (backdrop_filter_contents) {
FML_CHECK(clip_coverage_stack_.HasCoverage());
// We should never hit this case as we check the intersection above.
// NOLINTBEGIN(bugprone-unchecked-optional-access)
subpass_coverage =
coverage_limit
.Intersection(clip_coverage_stack_.CurrentClipCoverage().value())
.value();
// NOLINTEND(bugprone-unchecked-optional-access)
Rect subpass_coverage;
if (backdrop_filter_contents ||
Entity::IsBlendModeDestructive(paint.blend_mode) || !bounds.has_value()) {
subpass_coverage = coverage_limit;
// TODO(jonahwilliams): if we have tight bounds we should be able to reduce
// this size here. if (bounds.has_value() && bounds_from_caller) {
// subpass_coverage =
// coverage_limit.Intersection(bounds.value()).value_or(bounds.value());
// }
} else {
subpass_coverage = bounds->TransformBounds(GetCurrentTransform());
}

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

CanvasStackEntry entry;
Expand Down Expand Up @@ -491,7 +491,8 @@ bool ExperimentalCanvas::Restore() {
PaintPassDelegate(save_layer_state.paint)
.CreateContentsForSubpassTarget(
lazy_render_pass.inline_pass_context->GetTexture(),
transform_stack_.back().transform);
Matrix::MakeTranslation(Vector3{-GetGlobalPassPosition()}) *
transform_stack_.back().transform);

lazy_render_pass.inline_pass_context->EndPass();

Expand Down Expand Up @@ -669,20 +670,22 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
// conditionally update the backdrop color to its solid color value blended
// with the current backdrop.
if (render_passes_.back().IsApplyingClearColor()) {
std::optional<Color> maybe_color =
entity.AsBackgroundColor(render_passes_.back()
.entity_pass_target->GetRenderTarget()
.GetRenderTargetSize());
std::optional<Color> maybe_color = entity.AsBackgroundColor(
render_passes_.back().inline_pass_context->GetTexture()->GetSize());
if (maybe_color.has_value()) {
Color color = maybe_color.value();
RenderTarget& render_target =
render_passes_.back().entity_pass_target->GetRenderTarget();
RenderTarget& render_target = render_passes_.back()
.inline_pass_context->GetPassTarget()
.GetRenderTarget();
ColorAttachment attachment =
render_target.GetColorAttachments().find(0u)->second;
// Attachment.clear color needs to be premultiplied at all times, but the
// Color::Blend function requires unpremultiplied colors.
attachment.clear_color = attachment.clear_color.Unpremultiply()
.Blend(color, entity.GetBlendMode())
.Premultiply();
render_target.SetColorAttachment(attachment, 0u);
return;
}
}

Expand All @@ -700,8 +703,36 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
if (renderer_.GetDeviceCapabilities().SupportsFramebufferFetch()) {
ApplyFramebufferBlend(entity);
} else {
VALIDATION_LOG << "Emulated advanced blends are currently unsupported.";
return;
// 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).
auto input_texture = FlipBackdrop(render_passes_, GetGlobalPassPosition(),
clip_coverage_stack_, renderer_);
if (!input_texture) {
return;
}

// The coverage hint tells the rendered Contents which portion of the
// rendered output will actually be used, and so we set this to the
// current clip coverage (which is the max clip bounds). The contents may
// optionally use this hint to avoid unnecessary rendering work.
auto element_coverage_hint = entity.GetContents()->GetCoverageHint();
entity.GetContents()->SetCoverageHint(Rect::Intersection(
element_coverage_hint, clip_coverage_stack_.CurrentClipCoverage()));

FilterInput::Vector inputs = {
FilterInput::Make(input_texture, entity.GetTransform().Invert()),
FilterInput::Make(entity.GetContents())};
auto contents =
ColorFilterContents::MakeBlend(entity.GetBlendMode(), inputs);
entity.SetContents(std::move(contents));
entity.SetBlendMode(BlendMode::kSource);
}
}

Expand Down Expand Up @@ -831,6 +862,7 @@ bool ExperimentalCanvas::BlitToOnscreen() {

void ExperimentalCanvas::EndReplay() {
FML_DCHECK(render_passes_.size() == 1u);
render_passes_.back().inline_pass_context->GetRenderPass(0);
render_passes_.back().inline_pass_context->EndPass();

// If requires_readback_ was true, then we rendered to an offscreen texture
Expand Down
Loading

0 comments on commit f2c7fcf

Please sign in to comment.