diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index cf4b729a7f9a2..8b3cf4bddbd93 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -138,6 +138,7 @@ ../../../flutter/impeller/compiler/shader_bundle_unittests.cc ../../../flutter/impeller/compiler/switches_unittests.cc ../../../flutter/impeller/core/allocator_unittests.cc +../../../flutter/impeller/display_list/aiks_dl_opacity_unittests.cc ../../../flutter/impeller/display_list/aiks_dl_path_unittests.cc ../../../flutter/impeller/display_list/dl_golden_unittests.cc ../../../flutter/impeller/display_list/dl_golden_unittests.h diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 01f5a296a235e..3d56c397a0325 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -493,30 +493,6 @@ TEST_P(AiksTest, CanEmptyPictureConvertToImage) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } -TEST_P(AiksTest, CanRenderGroupOpacity) { - Canvas canvas; - - Paint red; - red.color = Color::Red(); - Paint green; - green.color = Color::Green().WithAlpha(0.5); - Paint blue; - blue.color = Color::Blue(); - - Paint alpha; - alpha.color = Color::Red().WithAlpha(0.5); - - canvas.SaveLayer(alpha); - - canvas.DrawRect(Rect::MakeXYWH(000, 000, 100, 100), red); - canvas.DrawRect(Rect::MakeXYWH(020, 020, 100, 100), green); - canvas.DrawRect(Rect::MakeXYWH(040, 040, 100, 100), blue); - - canvas.Restore(); - - ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); -} - TEST_P(AiksTest, CoordinateConversionsAreCorrect) { Canvas canvas; @@ -1653,45 +1629,6 @@ TEST_P(AiksTest, PaintWithFilters) { ASSERT_FALSE(paint.HasColorFilter()); } -TEST_P(AiksTest, OpacityPeepHoleApplicationTest) { - auto entity_pass = std::make_shared(); - auto rect = Rect::MakeLTRB(0, 0, 100, 100); - Paint paint; - paint.color = Color::White().WithAlpha(0.5); - paint.color_filter = - ColorFilter::MakeBlend(BlendMode::kSourceOver, Color::Blue()); - - // Paint has color filter, can't elide. - auto delegate = std::make_shared(paint); - ASSERT_FALSE(delegate->CanCollapseIntoParentPass(entity_pass.get())); - - paint.color_filter = nullptr; - paint.image_filter = ImageFilter::MakeBlur(Sigma(1.0), Sigma(1.0), - FilterContents::BlurStyle::kNormal, - Entity::TileMode::kClamp); - - // Paint has image filter, can't elide. - delegate = std::make_shared(paint); - ASSERT_FALSE(delegate->CanCollapseIntoParentPass(entity_pass.get())); - - paint.image_filter = nullptr; - paint.color = Color::Red(); - - // Paint has no alpha, can't elide; - delegate = std::make_shared(paint); - ASSERT_FALSE(delegate->CanCollapseIntoParentPass(entity_pass.get())); - - // Positive test. - Entity entity; - entity.SetContents(SolidColorContents::Make( - PathBuilder{}.AddRect(rect).TakePath(), Color::Red())); - entity_pass->AddEntity(std::move(entity)); - paint.color = Color::Red().WithAlpha(0.5); - - delegate = std::make_shared(paint); - ASSERT_TRUE(delegate->CanCollapseIntoParentPass(entity_pass.get())); -} - TEST_P(AiksTest, DrawPaintAbsorbsClears) { Canvas canvas; canvas.DrawPaint({.color = Color::Red(), .blend_mode = BlendMode::kSource}); diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index 9f243ea8e0437..bc8d1af742c1e 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -224,6 +224,7 @@ void Canvas::Save(bool create_subpass, entry.transform = transform_stack_.back().transform; entry.cull_rect = transform_stack_.back().cull_rect; entry.clip_height = transform_stack_.back().clip_height; + entry.distributed_opacity = transform_stack_.back().distributed_opacity; if (create_subpass) { entry.rendering_mode = Entity::RenderingMode::kSubpassAppendSnapshotTransform; @@ -830,6 +831,7 @@ void Canvas::AddRenderEntityToCurrentPass(Entity entity, bool reuse_depth) { ++current_depth_; } entity.SetClipDepth(current_depth_); + entity.SetInheritedOpacity(transform_stack_.back().distributed_opacity); GetCurrentPass().AddEntity(std::move(entity)); } @@ -841,8 +843,16 @@ void Canvas::SaveLayer(const Paint& paint, std::optional bounds, const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, - uint32_t total_content_depth) { + uint32_t total_content_depth, + bool can_distribute_opacity) { + if (can_distribute_opacity && !backdrop_filter && + Paint::CanApplyOpacityPeephole(paint)) { + Save(false, total_content_depth, paint.blend_mode, backdrop_filter); + transform_stack_.back().distributed_opacity *= paint.color.alpha; + return; + } TRACE_EVENT0("flutter", "Canvas::saveLayer"); + Save(true, total_content_depth, paint.blend_mode, backdrop_filter); // The DisplayList bounds/rtree doesn't account for filters applied to parent @@ -863,14 +873,12 @@ void Canvas::SaveLayer(const Paint& paint, paint.image_filter->Visit(mip_count_visitor); new_layer_pass.SetRequiredMipCount(mip_count_visitor.GetRequiredMipCount()); } + // 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; - // Only apply opacity peephole on default blending. - if (paint.blend_mode == BlendMode::kSourceOver) { - new_layer_pass.SetDelegate( - std::make_shared(paint)); - } else { - new_layer_pass.SetDelegate(std::make_shared(paint)); - } + new_layer_pass.SetDelegate(std::make_shared(paint_copy)); } void Canvas::DrawTextFrame(const std::shared_ptr& text_frame, diff --git a/impeller/aiks/canvas.h b/impeller/aiks/canvas.h index c8a814a235a2d..e5bdc379b0622 100644 --- a/impeller/aiks/canvas.h +++ b/impeller/aiks/canvas.h @@ -36,6 +36,7 @@ struct CanvasStackEntry { size_t clip_height = 0u; // The number of clips tracked for this canvas stack entry. size_t num_clips = 0u; + Scalar distributed_opacity = 1.0f; Entity::RenderingMode rendering_mode = Entity::RenderingMode::kDirect; }; @@ -75,7 +76,8 @@ class Canvas { std::optional bounds = std::nullopt, const std::shared_ptr& backdrop_filter = nullptr, ContentBoundsPromise bounds_promise = ContentBoundsPromise::kUnknown, - uint32_t total_content_depth = kMaxDepth); + uint32_t total_content_depth = kMaxDepth, + bool can_distribute_opacity = false); virtual bool Restore(); diff --git a/impeller/aiks/experimental_canvas.cc b/impeller/aiks/experimental_canvas.cc index 5bc7909202f69..27e27c15b02f0 100644 --- a/impeller/aiks/experimental_canvas.cc +++ b/impeller/aiks/experimental_canvas.cc @@ -194,6 +194,7 @@ void ExperimentalCanvas::Save(uint32_t total_content_depth) { 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) << entry.clip_depth << " <=? " << transform_stack_.back().clip_depth << " after allocating " << total_content_depth; @@ -207,12 +208,25 @@ void ExperimentalCanvas::SaveLayer( std::optional bounds, const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, - uint32_t total_content_depth) { + uint32_t total_content_depth, + bool can_distribute_opacity) { + 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()); } + + // 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; + Rect subpass_coverage = bounds->TransformBounds(GetCurrentTransform()); auto target = CreateRenderTarget(renderer_, @@ -220,7 +234,7 @@ void ExperimentalCanvas::SaveLayer( subpass_coverage.GetSize().height), 1u, Color::BlackTransparent()); entity_pass_targets_.push_back(std::move(target)); - save_layer_state_.push_back(SaveLayerState{paint, subpass_coverage}); + save_layer_state_.push_back(SaveLayerState{paint_copy, subpass_coverage}); CanvasStackEntry entry; entry.transform = transform_stack_.back().transform; @@ -395,6 +409,8 @@ void ExperimentalCanvas::DrawTextFrame( void ExperimentalCanvas::AddRenderEntityToCurrentPass(Entity entity, bool reuse_depth) { + entity.SetInheritedOpacity(transform_stack_.back().distributed_opacity); + auto transform = entity.GetTransform(); entity.SetTransform( Matrix::MakeTranslation(Vector3(-GetGlobalPassPosition())) * transform); diff --git a/impeller/aiks/experimental_canvas.h b/impeller/aiks/experimental_canvas.h index 626cae9be497f..4595207dda723 100644 --- a/impeller/aiks/experimental_canvas.h +++ b/impeller/aiks/experimental_canvas.h @@ -49,7 +49,8 @@ class ExperimentalCanvas : public Canvas { std::optional bounds, const std::shared_ptr& backdrop_filter, ContentBoundsPromise bounds_promise, - uint32_t total_content_depth) override; + uint32_t total_content_depth, + bool can_distribute_opacity) override; bool Restore() override; diff --git a/impeller/aiks/paint.h b/impeller/aiks/paint.h index 10b46dc99c05a..66049e1c0acd9 100644 --- a/impeller/aiks/paint.h +++ b/impeller/aiks/paint.h @@ -31,6 +31,15 @@ struct Paint { const Matrix& effect_transform)>; using ColorSourceProc = std::function()>; + /// @brief Whether or not a save layer with the provided paint can perform the + /// opacity peephole optimization. + static bool CanApplyOpacityPeephole(const Paint& paint) { + return paint.blend_mode == BlendMode::kSourceOver && + paint.invert_colors == false && + !paint.mask_blur_descriptor.has_value() && + paint.image_filter == nullptr && paint.color_filter == nullptr; + } + enum class Style { kFill, kStroke, diff --git a/impeller/aiks/paint_pass_delegate.cc b/impeller/aiks/paint_pass_delegate.cc index 17dfe4648fdb8..941baffc93f6c 100644 --- a/impeller/aiks/paint_pass_delegate.cc +++ b/impeller/aiks/paint_pass_delegate.cc @@ -5,7 +5,6 @@ #include "impeller/aiks/paint_pass_delegate.h" #include "impeller/core/formats.h" -#include "impeller/core/sampler_descriptor.h" #include "impeller/entity/contents/contents.h" #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/entity_pass.h" @@ -55,107 +54,4 @@ std::shared_ptr PaintPassDelegate::WithImageFilter( Entity::RenderingMode::kSubpassPrependSnapshotTransform); } -/// OpacityPeepholePassDelegate -/// ---------------------------------------------- - -OpacityPeepholePassDelegate::OpacityPeepholePassDelegate(Paint paint) - : paint_(std::move(paint)) {} - -// |EntityPassDelgate| -OpacityPeepholePassDelegate::~OpacityPeepholePassDelegate() = default; - -// |EntityPassDelgate| -bool OpacityPeepholePassDelegate::CanElide() { - return paint_.blend_mode == BlendMode::kDestination; -} - -// |EntityPassDelgate| -bool OpacityPeepholePassDelegate::CanCollapseIntoParentPass( - EntityPass* entity_pass) { - // Passes with enforced bounds that clip the contents can not be safely - // collapsed. - if (entity_pass->GetBoundsLimitMightClipContent()) { - return false; - } - - // OpacityPeepholePassDelegate will only get used if the pass's blend mode is - // SourceOver, so no need to check here. - if (paint_.color.alpha <= 0.0 || paint_.color.alpha >= 1.0 || - paint_.image_filter || paint_.color_filter) { - return false; - } - - // Note: determing whether any coverage intersects has quadradic complexity in - // the number of rectangles, and depending on whether or not we cache at - // different levels of the entity tree may end up cubic. In the interest of - // proving whether or not this optimization is valuable, we only consider very - // simple peephole optimizations here - where there is a single drawing - // command wrapped in save layer. This would indicate something like an - // Opacity or FadeTransition wrapping a very simple widget, like in the - // CupertinoPicker. - if (entity_pass->GetElementCount() > 3) { - // Single paint command with a save layer would be: - // 1. clip - // 2. draw command - // 3. restore. - return false; - } - bool all_can_accept = true; - std::vector all_coverages; - auto had_subpass = entity_pass->IterateUntilSubpass( - [&all_coverages, &all_can_accept](Entity& entity) { - const auto& contents = entity.GetContents(); - if (!entity.CanInheritOpacity()) { - all_can_accept = false; - return false; - } - auto maybe_coverage = contents->GetCoverage(entity); - if (maybe_coverage.has_value()) { - auto coverage = maybe_coverage.value(); - for (const auto& cv : all_coverages) { - if (cv.IntersectsWithRect(coverage)) { - all_can_accept = false; - return false; - } - } - all_coverages.push_back(coverage); - } - return true; - }); - if (had_subpass || !all_can_accept) { - return false; - } - auto alpha = paint_.color.alpha; - entity_pass->IterateUntilSubpass([&alpha](Entity& entity) { - entity.SetInheritedOpacity(alpha); - return true; - }); - return true; -} - -// |EntityPassDelgate| -std::shared_ptr -OpacityPeepholePassDelegate::CreateContentsForSubpassTarget( - std::shared_ptr target, - const Matrix& effect_transform) { - auto contents = TextureContents::MakeRect(Rect::MakeSize(target->GetSize())); - contents->SetLabel("Subpass"); - contents->SetTexture(target); - contents->SetSourceRect(Rect::MakeSize(target->GetSize())); - contents->SetOpacity(paint_.color.alpha); - contents->SetDeferApplyingOpacity(true); - - return paint_.WithFiltersForSubpassTarget(std::move(contents), - effect_transform); -} - -// |EntityPassDelgate| -std::shared_ptr OpacityPeepholePassDelegate::WithImageFilter( - const FilterInput::Variant& input, - const Matrix& effect_transform) const { - return paint_.WithImageFilter( - input, effect_transform, - Entity::RenderingMode::kSubpassPrependSnapshotTransform); -} - } // namespace impeller diff --git a/impeller/aiks/paint_pass_delegate.h b/impeller/aiks/paint_pass_delegate.h index 8761aaa0e2322..72e71d61403d3 100644 --- a/impeller/aiks/paint_pass_delegate.h +++ b/impeller/aiks/paint_pass_delegate.h @@ -43,43 +43,6 @@ class PaintPassDelegate final : public EntityPassDelegate { PaintPassDelegate& operator=(const PaintPassDelegate&) = delete; }; -/// A delegate that attempts to forward opacity from a save layer to -/// child contents. -/// -/// Currently this has a hardcoded limit of 3 entities in a pass, and -/// cannot forward to child subpass delegates. -class OpacityPeepholePassDelegate final : public EntityPassDelegate { - public: - explicit OpacityPeepholePassDelegate(Paint paint); - - // |EntityPassDelgate| - ~OpacityPeepholePassDelegate() override; - - // |EntityPassDelgate| - bool CanElide() override; - - // |EntityPassDelgate| - bool CanCollapseIntoParentPass(EntityPass* entity_pass) override; - - // |EntityPassDelgate| - std::shared_ptr CreateContentsForSubpassTarget( - std::shared_ptr target, - const Matrix& effect_transform) override; - - // |EntityPassDelgate| - std::shared_ptr WithImageFilter( - const FilterInput::Variant& input, - const Matrix& effect_transform) const override; - - private: - const Paint paint_; - - OpacityPeepholePassDelegate(const OpacityPeepholePassDelegate&) = delete; - - OpacityPeepholePassDelegate& operator=(const OpacityPeepholePassDelegate&) = - delete; -}; - } // namespace impeller #endif // FLUTTER_IMPELLER_AIKS_PAINT_PASS_DELEGATE_H_ diff --git a/impeller/display_list/BUILD.gn b/impeller/display_list/BUILD.gn index 392f857833002..599259fc2f437 100644 --- a/impeller/display_list/BUILD.gn +++ b/impeller/display_list/BUILD.gn @@ -52,6 +52,7 @@ impeller_component("display_list") { template("display_list_unittests_component") { target_name = invoker.target_name predefined_sources = [ + "aiks_dl_opacity_unittests.cc", "aiks_dl_path_unittests.cc", "dl_golden_unittests.cc", "dl_playground.cc", diff --git a/impeller/display_list/aiks_dl_opacity_unittests.cc b/impeller/display_list/aiks_dl_opacity_unittests.cc new file mode 100644 index 0000000000000..b97c10946c91c --- /dev/null +++ b/impeller/display_list/aiks_dl_opacity_unittests.cc @@ -0,0 +1,80 @@ +// 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 "display_list/dl_blend_mode.h" +#include "flutter/impeller/aiks/aiks_unittests.h" + +#include "flutter/display_list/dl_builder.h" +#include "flutter/display_list/dl_color.h" +#include "flutter/display_list/dl_paint.h" +#include "flutter/testing/testing.h" +#include "include/core/SkRect.h" + +namespace impeller { +namespace testing { + +using namespace flutter; + +TEST_P(AiksTest, DrawOpacityPeephole) { + DisplayListBuilder builder; + + DlPaint green; + green.setColor(DlColor::kGreen().modulateOpacity(0.5)); + + DlPaint alpha; + alpha.setColor(DlColor::kRed().modulateOpacity(0.5)); + + builder.SaveLayer(nullptr, &alpha); + builder.DrawRect(SkRect::MakeXYWH(0, 0, 100, 100), green); + builder.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderGroupOpacity) { + DisplayListBuilder builder; + + DlPaint red; + red.setColor(DlColor::kRed()); + DlPaint green; + green.setColor(DlColor::kGreen().modulateOpacity(0.5)); + DlPaint blue; + blue.setColor(DlColor::kBlue()); + + DlPaint alpha; + alpha.setColor(DlColor::kRed().modulateOpacity(0.5)); + + builder.SaveLayer(nullptr, &alpha); + builder.DrawRect(SkRect::MakeXYWH(0, 0, 100, 100), red); + builder.DrawRect(SkRect::MakeXYWH(200, 200, 100, 100), green); + builder.DrawRect(SkRect::MakeXYWH(400, 400, 100, 100), blue); + builder.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +TEST_P(AiksTest, CanRenderGroupOpacityToSavelayer) { + DisplayListBuilder builder; + + DlPaint red; + red.setColor(DlColor::kRed()); + + DlPaint alpha; + alpha.setColor(DlColor::kRed().modulateOpacity(0.7)); + + // Create a saveLayer that will forward its opacity to another + // saveLayer, to verify that we correctly distribute opacity. + SkRect bounds = SkRect::MakeLTRB(0, 0, 500, 500); + builder.SaveLayer(&bounds, &alpha); + builder.SaveLayer(&bounds, &alpha); + builder.DrawRect(SkRect::MakeXYWH(0, 0, 400, 400), red); + builder.DrawRect(SkRect::MakeXYWH(0, 0, 450, 450), red); + builder.Restore(); + builder.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); +} + +} // namespace testing +} // namespace impeller diff --git a/impeller/display_list/dl_dispatcher.cc b/impeller/display_list/dl_dispatcher.cc index fa8782426c4ff..2f55c944a08e0 100644 --- a/impeller/display_list/dl_dispatcher.cc +++ b/impeller/display_list/dl_dispatcher.cc @@ -626,7 +626,8 @@ void DlDispatcherBase::saveLayer(const SkRect& bounds, ? ContentBoundsPromise::kMayClipContents : ContentBoundsPromise::kContainsContents; GetCanvas().SaveLayer(paint, skia_conversions::ToRect(bounds), - ToImageFilter(backdrop), promise, total_content_depth); + ToImageFilter(backdrop), promise, total_content_depth, + options.can_distribute_opacity()); } // |flutter::DlOpReceiver| @@ -1029,7 +1030,8 @@ void DlDispatcherBase::drawDisplayList( save_paint.color = Color(0, 0, 0, opacity); GetCanvas().SaveLayer( save_paint, skia_conversions::ToRect(display_list->bounds()), nullptr, - ContentBoundsPromise::kContainsContents, display_list->total_depth()); + ContentBoundsPromise::kContainsContents, display_list->total_depth(), + display_list->can_apply_group_opacity()); } else { // The display list may alter the clip, which must be restored to the // current clip at the end of playback. diff --git a/testing/impeller_golden_tests_output.txt b/testing/impeller_golden_tests_output.txt index cee7f44d467b0..fb2b34e52ff03 100644 --- a/testing/impeller_golden_tests_output.txt +++ b/testing/impeller_golden_tests_output.txt @@ -310,6 +310,9 @@ impeller_Play_AiksTest_CanRenderForegroundBlendWithMaskBlur_Vulkan.png impeller_Play_AiksTest_CanRenderGradientDecalWithBackground_Metal.png impeller_Play_AiksTest_CanRenderGradientDecalWithBackground_OpenGLES.png impeller_Play_AiksTest_CanRenderGradientDecalWithBackground_Vulkan.png +impeller_Play_AiksTest_CanRenderGroupOpacityToSavelayer_Metal.png +impeller_Play_AiksTest_CanRenderGroupOpacityToSavelayer_OpenGLES.png +impeller_Play_AiksTest_CanRenderGroupOpacityToSavelayer_Vulkan.png impeller_Play_AiksTest_CanRenderGroupOpacity_Metal.png impeller_Play_AiksTest_CanRenderGroupOpacity_OpenGLES.png impeller_Play_AiksTest_CanRenderGroupOpacity_Vulkan.png @@ -533,6 +536,9 @@ impeller_Play_AiksTest_DrawAtlasWithColorAdvancedAndTransform_Vulkan.png impeller_Play_AiksTest_DrawLinesRenderCorrectly_Metal.png impeller_Play_AiksTest_DrawLinesRenderCorrectly_OpenGLES.png impeller_Play_AiksTest_DrawLinesRenderCorrectly_Vulkan.png +impeller_Play_AiksTest_DrawOpacityPeephole_Metal.png +impeller_Play_AiksTest_DrawOpacityPeephole_OpenGLES.png +impeller_Play_AiksTest_DrawOpacityPeephole_Vulkan.png impeller_Play_AiksTest_DrawPaintTransformsBounds_Metal.png impeller_Play_AiksTest_DrawPaintTransformsBounds_OpenGLES.png impeller_Play_AiksTest_DrawPaintTransformsBounds_Vulkan.png @@ -791,4 +797,4 @@ impeller_Play_DlGoldenTest_CanDrawPaint_OpenGLES.png impeller_Play_DlGoldenTest_CanDrawPaint_Vulkan.png impeller_Play_DlGoldenTest_CanRenderImage_Metal.png impeller_Play_DlGoldenTest_CanRenderImage_OpenGLES.png -impeller_Play_DlGoldenTest_CanRenderImage_Vulkan.png \ No newline at end of file +impeller_Play_DlGoldenTest_CanRenderImage_Vulkan.png