Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
268 changes: 217 additions & 51 deletions impeller/aiks/experimental_canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,23 @@ 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_) {
entity_pass_targets_.push_back(CreateRenderTarget(
renderer_, color0.texture->GetSize(), /*mip_count=*/1,
/*clear_color=*/Color::BlackTransparent()));
auto entity_pass_target =
CreateRenderTarget(renderer_, //
color0.texture->GetSize(), //
/*mip_count=*/1, //
/*clear_color=*/Color::BlackTransparent() //
);
render_passes_.push_back(
LazyRenderingConfig(renderer_, std::move(entity_pass_target)));
} else {
entity_pass_targets_.push_back(std::make_unique<EntityPassTarget>(
render_target_,
renderer_.GetDeviceCapabilities().SupportsReadFromResolve(),
renderer_.GetDeviceCapabilities().SupportsImplicitResolvingMSAA()));
auto entity_pass_target = std::make_unique<EntityPassTarget>(
render_target_, //
renderer_.GetDeviceCapabilities().SupportsReadFromResolve(), //
renderer_.GetDeviceCapabilities().SupportsImplicitResolvingMSAA() //
);
render_passes_.push_back(
LazyRenderingConfig(renderer_, std::move(entity_pass_target)));
}

auto inline_pass = std::make_unique<InlinePassContext>(
renderer_, *entity_pass_targets_.back(), 0);
inline_pass_contexts_.emplace_back(std::move(inline_pass));
auto result = inline_pass_contexts_.back()->GetRenderPass(0u);
render_passes_.push_back(result.pass);
}

void ExperimentalCanvas::Save(uint32_t total_content_depth) {
Expand All @@ -210,30 +212,148 @@ void ExperimentalCanvas::SaveLayer(
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());
}

// SaveLayer is a no-op, depending on the bounds promise. Should DL elide
// this?
if (bounds->IsEmpty() ||
(backdrop_filter &&
clip_coverage_stack_.CurrentClipCoverage().has_value() &&
clip_coverage_stack_.CurrentClipCoverage()->IsEmpty())) {
return;
}

if (can_distribute_opacity && !backdrop_filter &&
Paint::CanApplyOpacityPeephole(paint)) {
Save(total_content_depth);
transform_stack_.back().distributed_opacity *= paint.color.alpha;
return;
}
// 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());

int required_mip_count = 1;
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();
}
EntityPass::BackdropFilterProc backdrop_filter_proc =
[backdrop_filter = backdrop_filter->Clone()](
const FilterInput::Ref& input, const Matrix& effect_transform,
Entity::RenderingMode rendering_mode) {
auto filter = backdrop_filter->WrapInput(input);
filter->SetEffectTransform(effect_transform);
filter->SetRenderingMode(rendering_mode);
return filter;
};
required_mip_count = backdrop_filter->GetRequiredMipCount();

auto rendering_config = std::move(render_passes_.back());
render_passes_.pop_back();

// If the very first thing we render in this EntityPass is a subpass that
// happens to have a backdrop filter, than that backdrop filter will end
// may wind up sampling from the raw, uncleared texture that came straight
// out of the texture cache. By calling `pass_context.GetRenderPass` here,
// we force the texture to pass through at least one RenderPass with the
// correct clear configuration before any sampling occurs.
rendering_config.inline_pass_context->GetRenderPass(0);

ISize restore_size =
rendering_config.inline_pass_context->GetTexture()->GetSize();
backdrop_filter_contents = backdrop_filter_proc(
FilterInput::Make(rendering_config.inline_pass_context->GetTexture()),
transform_stack_.back().transform.Basis(),
// When the subpass has a translation that means the math with
// the snapshot has to be different.
transform_stack_.back().transform.HasTranslation()
? Entity::RenderingMode::kSubpassPrependSnapshotTransform
: Entity::RenderingMode::kSubpassAppendSnapshotTransform);

// The subpass will need to read from the current pass texture when
// rendering the backdrop, so if there's an active pass, end it prior to
// rendering the subpass.
rendering_config.inline_pass_context->EndPass();

// Create a new render pass that the backdrop filter contents will be
// restored to in order to continue rendering.
render_passes_.push_back(
LazyRenderingConfig(renderer_, //
CreateRenderTarget(renderer_, //
restore_size, //
1,
Color::BlackTransparent() //
)));

// Eagerly restore the BDF contents.

// If the pass context returns a backdrop texture, we need to draw it to the
// current pass. We do this because it's faster and takes significantly less
// memory than storing/loading large MSAA textures. Also, it's not possible
// to blit the non-MSAA resolve texture of the previous pass to MSAA
// textures (let alone a transient one).
Rect size_rect = Rect::MakeSize(restore_size);
auto msaa_backdrop_contents = TextureContents::MakeRect(size_rect);
msaa_backdrop_contents->SetStencilEnabled(false);
msaa_backdrop_contents->SetLabel("MSAA backdrop");
msaa_backdrop_contents->SetSourceRect(size_rect);
msaa_backdrop_contents->SetTexture(
rendering_config.inline_pass_context->GetTexture());

Entity msaa_backdrop_entity;
msaa_backdrop_entity.SetContents(std::move(msaa_backdrop_contents));
msaa_backdrop_entity.SetBlendMode(BlendMode::kSource);
msaa_backdrop_entity.SetClipDepth(std::numeric_limits<uint32_t>::max());
if (!msaa_backdrop_entity.Render(renderer_,
*render_passes_.back()
.inline_pass_context->GetRenderPass(0)
.pass)) {
VALIDATION_LOG << "Failed to render MSAA backdrop filter entity.";
return;
}

// 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(),
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass,
GetGlobalPassPosition());
if (!replay.entity.Render(renderer_,
*render_passes_.back()
.inline_pass_context->GetRenderPass(0)
.pass)) {
VALIDATION_LOG << "Failed to render entity for clip restore.";
}
}
}

// When applying a save layer, absorb any pending distributed opacity.
Paint paint_copy = paint;
paint_copy.color.alpha *= transform_stack_.back().distributed_opacity;
transform_stack_.back().distributed_opacity = 1.0;

// 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());
auto target =
CreateRenderTarget(renderer_,
ISize::MakeWH(subpass_coverage.GetSize().width,
subpass_coverage.GetSize().height),
1u, Color::BlackTransparent());
entity_pass_targets_.push_back(std::move(target));
if (backdrop_filter_contents) {
subpass_coverage = clip_coverage_stack_.CurrentClipCoverage().value_or(
save_layer_state_.back().coverage);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IIUC from earlier, the plan is to do a Save() instead in cases where this ends up being zero size in order to avoid making a zero size texture/RenderPass?
Then, stuff appended within the "skipped" SaveLayer may still append draws, but they shouldn't be able to draw anything into the parent texture given the zero size scissor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is done now. Also, on partial repaint examples we have fewer composited layers compared to the current entity pass. I'm not sure why the current entity pass skip logic doesn't seem to work at eliding the entire layer when there are backdrops.

Experimental Canvas

image

Current

image

}

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

CanvasStackEntry entry;
Expand All @@ -247,19 +367,25 @@ void ExperimentalCanvas::SaveLayer(
entry.rendering_mode = Entity::RenderingMode::kSubpassAppendSnapshotTransform;
transform_stack_.emplace_back(entry);

auto inline_pass = std::make_unique<InlinePassContext>(
renderer_, *entity_pass_targets_.back(), 0);
inline_pass_contexts_.emplace_back(std::move(inline_pass));

auto result = inline_pass_contexts_.back()->GetRenderPass(0u);
render_passes_.push_back(result.pass);

// Start non-collapsed subpasses with a fresh clip coverage stack limited by
// the subpass coverage. This is important because image filters applied to
// save layers may transform the subpass texture after it's rendered,
// causing parent clip coverage to get misaligned with the actual area that
// the subpass will affect in the parent pass.
clip_coverage_stack_.PushSubpass(subpass_coverage, GetClipHeight());

if (backdrop_filter_contents) {
// Render the backdrop entity.
Entity backdrop_entity;
backdrop_entity.SetContents(std::move(backdrop_filter_contents));
backdrop_entity.SetTransform(
Matrix::MakeTranslation(Vector3(-local_position)));
backdrop_entity.SetClipDepth(std::numeric_limits<uint32_t>::max());

backdrop_entity.Render(
renderer_,
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass);
}
}

bool ExperimentalCanvas::Restore() {
Expand Down Expand Up @@ -289,26 +415,44 @@ bool ExperimentalCanvas::Restore() {
Entity::RenderingMode::kSubpassAppendSnapshotTransform ||
transform_stack_.back().rendering_mode ==
Entity::RenderingMode::kSubpassPrependSnapshotTransform) {
auto inline_pass = std::move(inline_pass_contexts_.back());
auto lazy_render_pass = std::move(render_passes_.back());
render_passes_.pop_back();
// Force the render pass to be constructed if it never was.
lazy_render_pass.inline_pass_context->GetRenderPass(0);

SaveLayerState save_layer_state = save_layer_state_.back();
save_layer_state_.pop_back();

std::shared_ptr<Contents> contents =
PaintPassDelegate(save_layer_state.paint)
.CreateContentsForSubpassTarget(inline_pass->GetTexture(),
transform_stack_.back().transform);

inline_pass->EndPass();
render_passes_.pop_back();
inline_pass_contexts_.pop_back();
.CreateContentsForSubpassTarget(
lazy_render_pass.inline_pass_context->GetTexture(),
transform_stack_.back().transform);

lazy_render_pass.inline_pass_context->EndPass();

// Round the subpass texture position for pixel alignment with the parent
// pass render target. By default, we draw subpass textures with nearest
// 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();

Entity element_entity;
element_entity.SetClipDepth(++current_depth_);
element_entity.SetContents(std::move(contents));
element_entity.SetBlendMode(save_layer_state.paint.blend_mode);
element_entity.SetTransform(Matrix::MakeTranslation(
Vector3(save_layer_state.coverage.GetOrigin())));
element_entity.SetTransform(
Matrix::MakeTranslation(Vector3(subpass_texture_position)));

if (element_entity.GetBlendMode() > Entity::kLastPipelineBlendMode) {
if (renderer_.GetDeviceCapabilities().SupportsFramebufferFetch()) {
Expand All @@ -319,7 +463,10 @@ bool ExperimentalCanvas::Restore() {
}
}

element_entity.Render(renderer_, *render_passes_.back());
element_entity.Render(
renderer_, //
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass //
);
clip_coverage_stack_.PopSubpass();
transform_stack_.pop_back();

Expand Down Expand Up @@ -364,15 +511,20 @@ bool ExperimentalCanvas::Restore() {

if (clip_state_result.clip_did_change) {
// We only need to update the pass scissor if the clip state has changed.
SetClipScissor(clip_coverage_stack_.CurrentClipCoverage(),
*render_passes_.back(), GetGlobalPassPosition());
SetClipScissor(
clip_coverage_stack_.CurrentClipCoverage(), //
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass, //
GetGlobalPassPosition() //
);
}

if (!clip_state_result.should_render) {
return true;
}

entity.Render(renderer_, *render_passes_.back());
entity.Render(
renderer_,
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass);
}

return true;
Expand Down Expand Up @@ -441,7 +593,16 @@ void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity,
}
}

entity.Render(renderer_, *render_passes_.back());
InlinePassContext::RenderPassResult result =
render_passes_.back().inline_pass_context->GetRenderPass(0);
if (!result.pass) {
// Failure to produce a render pass should be explained by specific errors
// in `InlinePassContext::GetRenderPass()`, so avoid log spam and don't
// append a validation log here.
return;
}

entity.Render(renderer_, *result.pass);
}

void ExperimentalCanvas::AddClipEntityToCurrentPass(Entity entity) {
Expand Down Expand Up @@ -482,21 +643,27 @@ void ExperimentalCanvas::AddClipEntityToCurrentPass(Entity entity) {

if (clip_state_result.clip_did_change) {
// We only need to update the pass scissor if the clip state has changed.
SetClipScissor(clip_coverage_stack_.CurrentClipCoverage(),
*render_passes_.back(), GetGlobalPassPosition());
SetClipScissor(
clip_coverage_stack_.CurrentClipCoverage(),
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass,
GetGlobalPassPosition());
}

if (!clip_state_result.should_render) {
return;
}

entity.Render(renderer_, *render_passes_.back());
entity.Render(
renderer_,
*render_passes_.back().inline_pass_context->GetRenderPass(0).pass);
}

bool ExperimentalCanvas::BlitToOnscreen() {
auto command_buffer = renderer_.GetContext()->CreateCommandBuffer();
command_buffer->SetLabel("EntityPass Root Command Buffer");
auto offscreen_target = entity_pass_targets_.back()->GetRenderTarget();
auto offscreen_target = render_passes_.back()
.inline_pass_context->GetPassTarget()
.GetRenderTarget();

if (renderer_.GetContext()
->GetCapabilities()
Expand Down Expand Up @@ -551,8 +718,8 @@ bool ExperimentalCanvas::BlitToOnscreen() {
}

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

// If requires_readback_ was true, then we rendered to an offscreen texture
// instead of to the onscreen provided in the render target. Now we need to
Expand All @@ -562,7 +729,6 @@ void ExperimentalCanvas::EndReplay() {
}

render_passes_.clear();
inline_pass_contexts_.clear();
renderer_.GetRenderTargetCache()->End();

Reset();
Expand Down
Loading