diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 40ce340469..1a4e7c5ad2 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -51252,6 +51252,8 @@ ORIGIN: ../../../flutter/impeller/entity/contents/sweep_gradient_contents.cc + . ORIGIN: ../../../flutter/impeller/entity/contents/sweep_gradient_contents.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/contents/text_contents.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/contents/text_contents.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/contents/text_shadow_cache.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/entity/contents/text_shadow_cache.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/contents/texture_contents.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/contents/texture_contents.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/entity/contents/tiled_texture_contents.cc + ../../../flutter/LICENSE @@ -54240,6 +54242,8 @@ FILE: ../../../flutter/impeller/entity/contents/sweep_gradient_contents.cc FILE: ../../../flutter/impeller/entity/contents/sweep_gradient_contents.h FILE: ../../../flutter/impeller/entity/contents/text_contents.cc FILE: ../../../flutter/impeller/entity/contents/text_contents.h +FILE: ../../../flutter/impeller/entity/contents/text_shadow_cache.cc +FILE: ../../../flutter/impeller/entity/contents/text_shadow_cache.h FILE: ../../../flutter/impeller/entity/contents/texture_contents.cc FILE: ../../../flutter/impeller/entity/contents/texture_contents.h FILE: ../../../flutter/impeller/entity/contents/tiled_texture_contents.cc diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc index 0d7cbaec7d..6d23191a0e 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_text_unittests.cc @@ -13,6 +13,9 @@ #include "flutter/fml/build_config.h" #include "flutter/impeller/display_list/aiks_unittests.h" #include "flutter/testing/testing.h" +#include "impeller/display_list/aiks_context.h" +#include "impeller/display_list/dl_dispatcher.h" +#include "impeller/entity/contents/content_context.h" #include "impeller/entity/contents/text_contents.h" #include "impeller/entity/entity.h" #include "impeller/geometry/matrix.h" @@ -41,7 +44,8 @@ bool RenderTextInCanvasSkia(const std::shared_ptr& context, DisplayListBuilder& canvas, const std::string& text, const std::string_view& font_fixture, - const TextRenderOptions& options = {}) { + const TextRenderOptions& options = {}, + const std::optional& font = std::nullopt) { // Draw the baseline. DlPaint paint; paint.setColor(DlColor::kAqua().withAlpha(255 * 0.25)); @@ -54,17 +58,23 @@ bool RenderTextInCanvasSkia(const std::shared_ptr& context, canvas.DrawCircle(options.position, 5.0, paint); // Construct the text blob. - auto c_font_fixture = std::string(font_fixture); - auto mapping = flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); - if (!mapping) { - return false; + SkFont selected_font; + if (!font.has_value()) { + auto c_font_fixture = std::string(font_fixture); + auto mapping = + flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); + if (!mapping) { + return false; + } + sk_sp font_mgr = txt::GetDefaultFontManager(); + selected_font = SkFont(font_mgr->makeFromData(mapping), options.font_size); + if (options.is_subpixel) { + selected_font.setSubpixel(true); + } + } else { + selected_font = font.value(); } - sk_sp font_mgr = txt::GetDefaultFontManager(); - SkFont sk_font(font_mgr->makeFromData(mapping), options.font_size); - if (options.is_subpixel) { - sk_font.setSubpixel(true); - } - auto blob = SkTextBlob::MakeFromString(text.c_str(), sk_font); + auto blob = SkTextBlob::MakeFromString(text.c_str(), selected_font); if (!blob) { return false; } @@ -731,5 +741,108 @@ TEST_P(AiksTest, TextContentsMismatchedTransformTest) { *render_pass)); } +TEST_P(AiksTest, TextWithShadowCache) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + DlPaint paint; + paint.setColor(DlColor::ARGB(1, 0.1, 0.1, 0.1)); + builder.DrawPaint(paint); + + AiksContext aiks_context(GetContext(), + std::make_shared()); + // Cache empty + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 0u); + + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), builder, "Hello World", kFontFixture, + TextRenderOptions{ + .color = DlColor::kBlue(), + .filter = DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 4)})); + + DisplayListToTexture(builder.Build(), {400, 400}, aiks_context); + + // Text should be cached. + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 1u); +} + +TEST_P(AiksTest, MultipleTextWithShadowCache) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + DlPaint paint; + paint.setColor(DlColor::ARGB(1, 0.1, 0.1, 0.1)); + builder.DrawPaint(paint); + + AiksContext aiks_context(GetContext(), + std::make_shared()); + // Cache empty + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 0u); + + for (auto i = 0; i < 5; i++) { + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), builder, "Hello World", kFontFixture, + TextRenderOptions{ + .color = DlColor::kBlue(), + .filter = DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 4)})); + } + + DisplayListToTexture(builder.Build(), {400, 400}, aiks_context); + + // Text should be cached. Each text gets its own entry as we don't analyze the + // strings. + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 5u); +} + +TEST_P(AiksTest, SingleIconShadowTest) { + DisplayListBuilder builder; + builder.Scale(GetContentScale().x, GetContentScale().y); + DlPaint paint; + paint.setColor(DlColor::ARGB(1, 0.1, 0.1, 0.1)); + builder.DrawPaint(paint); + + AiksContext aiks_context(GetContext(), + std::make_shared()); + // Cache empty + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 0u); + + // Create font instance outside loop so all draws use identical font instance. + auto c_font_fixture = std::string(kFontFixture); + auto mapping = flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); + ASSERT_TRUE(mapping); + sk_sp font_mgr = txt::GetDefaultFontManager(); + SkFont sk_font(font_mgr->makeFromData(mapping), 50); + + for (auto i = 0; i < 10; i++) { + ASSERT_TRUE(RenderTextInCanvasSkia( + GetContext(), builder, "A", kFontFixture, + TextRenderOptions{ + .color = DlColor::kBlue(), + .filter = DlBlurMaskFilter::Make(DlBlurStyle::kNormal, 4)}, + sk_font)); + } + + DisplayListToTexture(builder.Build(), {400, 400}, aiks_context); + + // Text should be cached. All 10 glyphs use the same cache entry. + EXPECT_EQ(aiks_context.GetContentContext() + .GetTextShadowCache() + .GetCacheSizeForTesting(), + 1u); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_unittests.cc index 0dae345069..98b4a1ce81 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_unittests.cc @@ -78,6 +78,7 @@ TEST_P(AiksTest, CollapsedDrawPaintInSubpassBackdropFilter) { TEST_P(AiksTest, ColorMatrixFilterSubpassCollapseOptimization) { DisplayListBuilder builder(DlRect::MakeSize(GetWindowSize())); + builder.DrawPaint(DlPaint().setColor(DlColor::kWhite())); const float matrix[20] = { -1.0, 0, 0, 1.0, 0, // diff --git a/engine/src/flutter/impeller/display_list/canvas.cc b/engine/src/flutter/impeller/display_list/canvas.cc index 442c54d635..435855dd61 100644 --- a/engine/src/flutter/impeller/display_list/canvas.cc +++ b/engine/src/flutter/impeller/display_list/canvas.cc @@ -20,7 +20,6 @@ #include "impeller/base/validation.h" #include "impeller/core/formats.h" #include "impeller/display_list/color_filter.h" -#include "impeller/display_list/dl_atlas_geometry.h" #include "impeller/display_list/image_filter.h" #include "impeller/display_list/skia_conversions.h" #include "impeller/entity/contents/atlas_contents.h" @@ -32,6 +31,7 @@ #include "impeller/entity/contents/line_contents.h" #include "impeller/entity/contents/solid_rrect_blur_contents.h" #include "impeller/entity/contents/text_contents.h" +#include "impeller/entity/contents/text_shadow_cache.h" #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/contents/vertices_contents.h" #include "impeller/entity/geometry/circle_geometry.h" @@ -1467,6 +1467,50 @@ bool Canvas::Restore() { return true; } +bool Canvas::AttemptBlurredTextOptimization( + const std::shared_ptr& text_frame, + const std::shared_ptr& text_contents, + Entity& entity, + const Paint& paint) { + if (!paint.mask_blur_descriptor.has_value() || // + paint.image_filter != nullptr || // + paint.color_filter != nullptr || // + paint.invert_colors) { + return false; + } + + // TODO(bdero): This mask blur application is a hack. It will always wind up + // doing a gaussian blur that affects the color source itself + // instead of just the mask. The color filter text support + // needs to be reworked in order to interact correctly with + // mask filters. + // https://github.com/flutter/flutter/issues/133297 + std::shared_ptr filter = + paint.mask_blur_descriptor->CreateMaskBlur( + FilterInput::Make(text_contents), + /*is_solid_color=*/true, GetCurrentTransform()); + + std::optional maybe_glyph = text_frame->AsSingleGlyph(); + int64_t identifier = maybe_glyph.has_value() + ? maybe_glyph.value().index + : reinterpret_cast(text_frame.get()); + TextShadowCache::TextShadowCacheKey cache_key( + /*p_max_basis=*/entity.GetTransform().GetMaxBasisLengthXY(), + /*p_identifier=*/identifier, + /*p_is_single_glyph=*/maybe_glyph.has_value(), + /*p_font=*/text_frame->GetFont(), + /*p_sigma=*/paint.mask_blur_descriptor->sigma); + + std::optional result = renderer_.GetTextShadowCache().Lookup( + renderer_, entity, filter, cache_key); + if (result.has_value()) { + AddRenderEntityToCurrentPass(result.value(), /*reuse_depth=*/false); + return true; + } else { + return false; + } +} + void Canvas::DrawTextFrame(const std::shared_ptr& text_frame, Point position, const Paint& paint) { @@ -1491,15 +1535,12 @@ void Canvas::DrawTextFrame(const std::shared_ptr& text_frame, entity.SetTransform(GetCurrentTransform() * Matrix::MakeTranslation(position)); - // TODO(bdero): This mask blur application is a hack. It will always wind up - // doing a gaussian blur that affects the color source itself - // instead of just the mask. The color filter text support - // needs to be reworked in order to interact correctly with - // mask filters. - // https://github.com/flutter/flutter/issues/133297 - entity.SetContents(paint.WithFilters(paint.WithMaskBlur( - std::move(text_contents), true, GetCurrentTransform()))); + if (AttemptBlurredTextOptimization(text_frame, text_contents, entity, + paint)) { + return; + } + entity.SetContents(paint.WithFilters(std::move(text_contents))); AddRenderEntityToCurrentPass(entity, false); } diff --git a/engine/src/flutter/impeller/display_list/canvas.h b/engine/src/flutter/impeller/display_list/canvas.h index 746167bd98..0f8d4d692a 100644 --- a/engine/src/flutter/impeller/display_list/canvas.h +++ b/engine/src/flutter/impeller/display_list/canvas.h @@ -17,6 +17,7 @@ #include "impeller/display_list/paint.h" #include "impeller/entity/contents/atlas_contents.h" #include "impeller/entity/contents/clip_contents.h" +#include "impeller/entity/contents/text_contents.h" #include "impeller/entity/entity.h" #include "impeller/entity/entity_pass_clip_stack.h" #include "impeller/entity/geometry/geometry.h" @@ -368,6 +369,12 @@ class Canvas { const SamplerDescriptor& sampler, SourceRectConstraint src_rect_constraint); + bool AttemptBlurredTextOptimization( + const std::shared_ptr& text_frame, + const std::shared_ptr& text_contents, + Entity& entity, + const Paint& paint); + RenderPass& GetCurrentRenderPass() const; Canvas(const Canvas&) = delete; diff --git a/engine/src/flutter/impeller/display_list/dl_dispatcher.cc b/engine/src/flutter/impeller/display_list/dl_dispatcher.cc index a444410ac0..6e893c0447 100644 --- a/engine/src/flutter/impeller/display_list/dl_dispatcher.cc +++ b/engine/src/flutter/impeller/display_list/dl_dispatcher.cc @@ -13,6 +13,7 @@ #include "display_list/dl_sampling_options.h" #include "display_list/effects/dl_image_filter.h" #include "flutter/fml/logging.h" +#include "fml/closure.h" #include "impeller/core/formats.h" #include "impeller/display_list/aiks_context.h" #include "impeller/display_list/canvas.h" @@ -1333,15 +1334,19 @@ std::shared_ptr DisplayListToTexture( ); const auto& [data, count] = collector.TakeBackdropData(); impeller_dispatcher.SetBackdropData(data, count); + context.GetContentContext().GetTextShadowCache().MarkFrameStart(); + fml::ScopedCleanupClosure cleanup([&] { + if (reset_host_buffer) { + context.GetContentContext().GetTransientsBuffer().Reset(); + } + context.GetContentContext().GetTextShadowCache().MarkFrameEnd(); + context.GetContentContext().GetLazyGlyphAtlas()->ResetTextFrames(); + context.GetContext()->DisposeThreadLocalCachedResources(); + }); + display_list->Dispatch(impeller_dispatcher, sk_cull_rect); impeller_dispatcher.FinishRecording(); - if (reset_host_buffer) { - context.GetContentContext().GetTransientsBuffer().Reset(); - } - context.GetContentContext().GetLazyGlyphAtlas()->ResetTextFrames(); - context.GetContext()->DisposeThreadLocalCachedResources(); - return target.GetRenderTargetTexture(); } @@ -1366,11 +1371,16 @@ bool RenderToTarget(ContentContext& context, ); const auto& [data, count] = collector.TakeBackdropData(); impeller_dispatcher.SetBackdropData(data, count); + context.GetTextShadowCache().MarkFrameStart(); + fml::ScopedCleanupClosure cleanup([&] { + if (reset_host_buffer) { + context.GetTransientsBuffer().Reset(); + } + context.GetTextShadowCache().MarkFrameEnd(); + }); + display_list->Dispatch(impeller_dispatcher, cull_rect); impeller_dispatcher.FinishRecording(); - if (reset_host_buffer) { - context.GetTransientsBuffer().Reset(); - } context.GetLazyGlyphAtlas()->ResetTextFrames(); return true; diff --git a/engine/src/flutter/impeller/entity/BUILD.gn b/engine/src/flutter/impeller/entity/BUILD.gn index 35c88c6dbc..17d9012107 100644 --- a/engine/src/flutter/impeller/entity/BUILD.gn +++ b/engine/src/flutter/impeller/entity/BUILD.gn @@ -179,6 +179,8 @@ impeller_component("entity") { "contents/sweep_gradient_contents.h", "contents/text_contents.cc", "contents/text_contents.h", + "contents/text_shadow_cache.cc", + "contents/text_shadow_cache.h", "contents/texture_contents.cc", "contents/texture_contents.h", "contents/tiled_texture_contents.cc", diff --git a/engine/src/flutter/impeller/entity/contents/content_context.cc b/engine/src/flutter/impeller/entity/contents/content_context.cc index e1207d735c..a38c271a5f 100644 --- a/engine/src/flutter/impeller/entity/contents/content_context.cc +++ b/engine/src/flutter/impeller/entity/contents/content_context.cc @@ -13,6 +13,7 @@ #include "impeller/core/texture_descriptor.h" #include "impeller/entity/contents/framebuffer_blend_contents.h" #include "impeller/entity/contents/pipelines.h" +#include "impeller/entity/contents/text_shadow_cache.h" #include "impeller/entity/entity.h" #include "impeller/entity/render_target_cache.h" #include "impeller/renderer/command_buffer.h" @@ -532,7 +533,8 @@ ContentContext::ContentContext( context_->GetResourceAllocator()) : std::move(render_target_allocator)), host_buffer_(HostBuffer::Create(context_->GetResourceAllocator(), - context_->GetIdleWaiter())) { + context_->GetIdleWaiter())), + text_shadow_cache_(std::make_unique()) { if (!context_ || !context_->IsValid()) { return; } diff --git a/engine/src/flutter/impeller/entity/contents/content_context.h b/engine/src/flutter/impeller/entity/contents/content_context.h index 90d411a844..da3db495b9 100644 --- a/engine/src/flutter/impeller/entity/contents/content_context.h +++ b/engine/src/flutter/impeller/entity/contents/content_context.h @@ -16,6 +16,7 @@ #include "impeller/base/validation.h" #include "impeller/core/formats.h" #include "impeller/core/host_buffer.h" +#include "impeller/entity/contents/text_shadow_cache.h" #include "impeller/geometry/color.h" #include "impeller/renderer/capabilities.h" #include "impeller/renderer/command_buffer.h" @@ -294,6 +295,8 @@ class ContentContext { /// allocate their own device buffers. HostBuffer& GetTransientsBuffer() const { return *host_buffer_; } + TextShadowCache& GetTextShadowCache() const { return *text_shadow_cache_; } + private: std::shared_ptr context_; std::shared_ptr lazy_glyph_atlas_; @@ -340,6 +343,7 @@ class ContentContext { std::shared_ptr render_target_cache_; std::shared_ptr host_buffer_; std::shared_ptr empty_texture_; + std::unique_ptr text_shadow_cache_; ContentContext(const ContentContext&) = delete; diff --git a/engine/src/flutter/impeller/entity/contents/text_shadow_cache.cc b/engine/src/flutter/impeller/entity/contents/text_shadow_cache.cc new file mode 100644 index 0000000000..6902d1239c --- /dev/null +++ b/engine/src/flutter/impeller/entity/contents/text_shadow_cache.cc @@ -0,0 +1,95 @@ +// 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 "impeller/entity/contents/text_shadow_cache.h" + +#include "fml/closure.h" +#include "impeller/entity/contents/content_context.h" +#include "impeller/entity/contents/contents.h" +#include "impeller/entity/contents/filters/filter_contents.h" +#include "impeller/geometry/sigma.h" + +namespace impeller { + +// Rounds sigma values for gaussian blur to nearest decimal. +static constexpr int32_t kMaxSigmaDenominator = 10; + +TextShadowCache::TextShadowCacheKey::TextShadowCacheKey(Scalar p_max_basis, + int64_t p_identifier, + bool p_is_single_glyph, + const Font& p_font, + Sigma p_sigma) + : max_basis(p_max_basis), + identifier(p_identifier), + is_single_glyph(p_is_single_glyph), + font(p_font), + rounded_sigma(Rational(std::round(p_sigma.sigma * kMaxSigmaDenominator), + kMaxSigmaDenominator)) {} + +void TextShadowCache::MarkFrameStart() { + for (auto& entry : entries_) { + entry.second.used_this_frame = false; + } +} + +void TextShadowCache::MarkFrameEnd() { + absl::erase_if(entries_, + [](const auto& pair) { return !pair.second.used_this_frame; }); +} + +std::optional TextShadowCache::Lookup( + const ContentContext& renderer, + const Entity& entity, + const std::shared_ptr& contents, + const TextShadowCacheKey& text_key) { + auto it = entries_.find(text_key); + + if (it != entries_.end()) { + it->second.used_this_frame = true; + Entity cache_entity = it->second.entity.Clone(); + cache_entity.SetClipDepth(entity.GetClipDepth()); + cache_entity.SetTransform(entity.GetTransform() * it->second.key_matrix); + return cache_entity; + } + + std::optional filter_coverage = contents->GetCoverage(entity); + if (!filter_coverage.has_value()) { + return std::nullopt; + } + + // Execute the filter to produce a snapshot that can be resued on subsequent + // frames. To prevent this texture from being re-used by the render target + // cache, we temporarily disable any RT caching. + renderer.GetRenderTargetCache()->DisableCache(); + fml::ScopedCleanupClosure closure( + [&] { renderer.GetRenderTargetCache()->EnableCache(); }); + std::optional maybe_entity = + contents->GetEntity(renderer, entity, contents->GetCoverageHint()); + if (!maybe_entity.has_value()) { + return std::nullopt; + } + + // The original entity has a transform matrix A. The snapshot entity has a + // transform matrix B. We need a function that converts A to B, so that if we + // render an entity with a slightly different transform matrix A', it appears + // in the correct position. + // A * K = B + // A-1 * A * K = A-1 * B + // K = A-1 * B + // + // The transform matrix K can be computed by inverse A times B. Multiplying + // any subsequent entity transforms by this matrix will correctly position + // them. + Matrix key_matrix = + entity.GetTransform().Invert() * maybe_entity->GetTransform(); + entries_[text_key] = + TextShadowCacheData{.entity = maybe_entity.value().Clone(), + .used_this_frame = true, + .key_matrix = key_matrix}; + + maybe_entity->SetClipDepth(entity.GetClipDepth()); + return maybe_entity; +} + +} // namespace impeller diff --git a/engine/src/flutter/impeller/entity/contents/text_shadow_cache.h b/engine/src/flutter/impeller/entity/contents/text_shadow_cache.h new file mode 100644 index 0000000000..3e9168b6cc --- /dev/null +++ b/engine/src/flutter/impeller/entity/contents/text_shadow_cache.h @@ -0,0 +1,109 @@ +// 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. + +#ifndef FLUTTER_IMPELLER_ENTITY_CONTENTS_TEXT_SHADOW_CACHE_H_ +#define FLUTTER_IMPELLER_ENTITY_CONTENTS_TEXT_SHADOW_CACHE_H_ + +#include +#include + +#include "flutter/third_party/abseil-cpp/absl/container/flat_hash_map.h" +#include "impeller/entity/entity.h" +#include "impeller/geometry/scalar.h" +#include "impeller/geometry/sigma.h" + +namespace impeller { + +/// @brief A cache for blurred text that re-uses these across frames. +/// +/// Text shadows are generally stable, but expensive to compute as we use a +/// full gaussian blur. This class caches these shadows by text blob identifier +/// and holds them for at least one frame. +/// +/// Additionally, there is an optimization for a single glyph (generally an +/// Icon) that uses the content itself as a key. +/// +/// If there was a cheaper method of text frame identity, or a per-glyph caching +/// system this could be more efficient. As it exists, this mostly ameliorate +/// severe performance degradation for glyph shadows but does not provide +/// substantially better performance than Skia. +class TextShadowCache { + public: + TextShadowCache() = default; + + ~TextShadowCache() = default; + + /// @brief A key to look up cached glyph textures. + struct TextShadowCacheKey { + Scalar max_basis; + int64_t identifier; + bool is_single_glyph; + Font font; + Rational rounded_sigma; + + TextShadowCacheKey(Scalar p_max_basis, + int64_t p_identifier, + bool p_is_single_glyph, + const Font& p_font, + Sigma p_sigma); + + struct Hash { + std::size_t operator()(const TextShadowCacheKey& key) const { + return fml::HashCombine(key.max_basis, key.identifier, + key.is_single_glyph, key.font.GetHash(), + key.rounded_sigma.GetHash()); + } + }; + + struct Equal { + constexpr bool operator()(const TextShadowCacheKey& lhs, + const TextShadowCacheKey& rhs) const { + return lhs.max_basis == rhs.max_basis && + lhs.identifier == rhs.identifier && + lhs.is_single_glyph == rhs.is_single_glyph && + lhs.font.IsEqual(rhs.font) && + lhs.rounded_sigma == rhs.rounded_sigma; + } + }; + }; + + /// @brief Mark all glyph textures as unused this frame. + void MarkFrameStart(); + + /// @brief Remove all glyph textures that were not referenced at least once. + void MarkFrameEnd(); + + /// @brief Lookup the entity in the cache with the given filter/text contents, + /// returning the new entity to render. + /// + /// If the entity is not present, render and place in the cache. + std::optional Lookup(const ContentContext& renderer, + const Entity& entity, + const std::shared_ptr& contents, + const TextShadowCacheKey&); + + // Visible for testing. + size_t GetCacheSizeForTesting() const { return entries_.size(); } + + private: + TextShadowCache(const TextShadowCache&) = delete; + + TextShadowCache& operator=(const TextShadowCache&) = delete; + + struct TextShadowCacheData { + Entity entity; + bool used_this_frame = true; + Matrix key_matrix; + }; + + absl::flat_hash_map + entries_; +}; + +} // namespace impeller + +#endif // FLUTTER_IMPELLER_ENTITY_CONTENTS_TEXT_SHADOW_CACHE_H_ diff --git a/engine/src/flutter/impeller/entity/render_target_cache.cc b/engine/src/flutter/impeller/entity/render_target_cache.cc index 3bc6e20d5d..5846843ed5 100644 --- a/engine/src/flutter/impeller/entity/render_target_cache.cc +++ b/engine/src/flutter/impeller/entity/render_target_cache.cc @@ -14,12 +14,14 @@ RenderTargetCache::RenderTargetCache(std::shared_ptr allocator, keep_alive_frame_count_(keep_alive_frame_count) {} void RenderTargetCache::Start() { + cache_disabled_count_ = 0; for (auto& td : render_target_data_) { td.used_this_frame = false; } } void RenderTargetCache::End() { + cache_disabled_count_ = 0; std::vector retain; for (RenderTargetData& td : render_target_data_) { @@ -33,6 +35,22 @@ void RenderTargetCache::End() { render_target_data_.swap(retain); } +void RenderTargetCache::DisableCache() { + cache_disabled_count_++; +} + +bool RenderTargetCache::CacheEnabled() const { + return cache_disabled_count_ == 0; +} + +void RenderTargetCache::EnableCache() { + FML_DCHECK(cache_disabled_count_ > 0); + if (cache_disabled_count_ == 0) { + return; + } + cache_disabled_count_--; +} + RenderTarget RenderTargetCache::CreateOffscreen( const Context& context, ISize size, @@ -54,19 +72,22 @@ RenderTarget RenderTargetCache::CreateOffscreen( .has_msaa = false, .has_depth_stencil = stencil_attachment_config.has_value(), }; - for (RenderTargetData& render_target_data : render_target_data_) { - const RenderTargetConfig other_config = render_target_data.config; - if (!render_target_data.used_this_frame && other_config == config) { - render_target_data.used_this_frame = true; - render_target_data.keep_alive_frame_count = keep_alive_frame_count_; - ColorAttachment color0 = - render_target_data.render_target.GetColorAttachment(0); - std::optional depth = - render_target_data.render_target.GetDepthAttachment(); - std::shared_ptr depth_tex = depth ? depth->texture : nullptr; - return RenderTargetAllocator::CreateOffscreen( - context, size, mip_count, label, color_attachment_config, - stencil_attachment_config, color0.texture, depth_tex); + + if (CacheEnabled()) { + for (RenderTargetData& render_target_data : render_target_data_) { + const RenderTargetConfig other_config = render_target_data.config; + if (!render_target_data.used_this_frame && other_config == config) { + render_target_data.used_this_frame = true; + render_target_data.keep_alive_frame_count = keep_alive_frame_count_; + ColorAttachment color0 = + render_target_data.render_target.GetColorAttachment(0); + std::optional depth = + render_target_data.render_target.GetDepthAttachment(); + std::shared_ptr depth_tex = depth ? depth->texture : nullptr; + return RenderTargetAllocator::CreateOffscreen( + context, size, mip_count, label, color_attachment_config, + stencil_attachment_config, color0.texture, depth_tex); + } } } RenderTarget created_target = RenderTargetAllocator::CreateOffscreen( @@ -107,20 +128,22 @@ RenderTarget RenderTargetCache::CreateOffscreenMSAA( .has_msaa = true, .has_depth_stencil = stencil_attachment_config.has_value(), }; - for (RenderTargetData& render_target_data : render_target_data_) { - const RenderTargetConfig other_config = render_target_data.config; - if (!render_target_data.used_this_frame && other_config == config) { - render_target_data.used_this_frame = true; - render_target_data.keep_alive_frame_count = keep_alive_frame_count_; - ColorAttachment color0 = - render_target_data.render_target.GetColorAttachment(0); - std::optional depth = - render_target_data.render_target.GetDepthAttachment(); - std::shared_ptr depth_tex = depth ? depth->texture : nullptr; - return RenderTargetAllocator::CreateOffscreenMSAA( - context, size, mip_count, label, color_attachment_config, - stencil_attachment_config, color0.texture, color0.resolve_texture, - depth_tex); + if (CacheEnabled()) { + for (RenderTargetData& render_target_data : render_target_data_) { + const RenderTargetConfig other_config = render_target_data.config; + if (!render_target_data.used_this_frame && other_config == config) { + render_target_data.used_this_frame = true; + render_target_data.keep_alive_frame_count = keep_alive_frame_count_; + ColorAttachment color0 = + render_target_data.render_target.GetColorAttachment(0); + std::optional depth = + render_target_data.render_target.GetDepthAttachment(); + std::shared_ptr depth_tex = depth ? depth->texture : nullptr; + return RenderTargetAllocator::CreateOffscreenMSAA( + context, size, mip_count, label, color_attachment_config, + stencil_attachment_config, color0.texture, color0.resolve_texture, + depth_tex); + } } } RenderTarget created_target = RenderTargetAllocator::CreateOffscreenMSAA( diff --git a/engine/src/flutter/impeller/entity/render_target_cache.h b/engine/src/flutter/impeller/entity/render_target_cache.h index c877dc422e..a90de5bc41 100644 --- a/engine/src/flutter/impeller/entity/render_target_cache.h +++ b/engine/src/flutter/impeller/entity/render_target_cache.h @@ -6,6 +6,7 @@ #define FLUTTER_IMPELLER_ENTITY_RENDER_TARGET_CACHE_H_ #include + #include "impeller/renderer/render_target.h" namespace impeller { @@ -27,6 +28,12 @@ class RenderTargetCache : public RenderTargetAllocator { // |RenderTargetAllocator| void End() override; + // |RenderTargetAllocator| + void DisableCache() override; + + // |RenderTargetAllocator| + void EnableCache() override; + RenderTarget CreateOffscreen( const Context& context, ISize size, @@ -65,8 +72,11 @@ class RenderTargetCache : public RenderTargetAllocator { RenderTarget render_target; }; + bool CacheEnabled() const; + std::vector render_target_data_; uint32_t keep_alive_frame_count_; + uint32_t cache_disabled_count_ = 0; RenderTargetCache(const RenderTargetCache&) = delete; diff --git a/engine/src/flutter/impeller/geometry/sigma.h b/engine/src/flutter/impeller/geometry/sigma.h index 9ea85498db..93c43ee135 100644 --- a/engine/src/flutter/impeller/geometry/sigma.h +++ b/engine/src/flutter/impeller/geometry/sigma.h @@ -21,7 +21,7 @@ namespace impeller { /// quality blurs (with exponentially diminishing returns for the same sigma /// input). Making this value any lower results in a noticable loss of /// quality in the blur. -constexpr static float kKernelRadiusPerSigma = 1.73205080757; +constexpr static float kKernelRadiusPerSigma = 1.73205080757f; struct Radius; diff --git a/engine/src/flutter/impeller/renderer/render_target.h b/engine/src/flutter/impeller/renderer/render_target.h index a7a1e0bd32..c1a16db684 100644 --- a/engine/src/flutter/impeller/renderer/render_target.h +++ b/engine/src/flutter/impeller/renderer/render_target.h @@ -177,6 +177,12 @@ class RenderTargetAllocator { const std::shared_ptr& existing_color_resolve_texture = nullptr, const std::shared_ptr& existing_depth_stencil_texture = nullptr); + /// @brief Disable any caching until the next call to `EnabledCache`. + virtual void DisableCache() {} + + /// @brief Re-enable any caching if disabled. + virtual void EnableCache() {} + /// @brief Mark the beginning of a frame workload. /// /// This may be used to reset any tracking state on whether or not a diff --git a/engine/src/flutter/impeller/typographer/text_frame.cc b/engine/src/flutter/impeller/typographer/text_frame.cc index cdfd48dca9..633a403610 100644 --- a/engine/src/flutter/impeller/typographer/text_frame.cc +++ b/engine/src/flutter/impeller/typographer/text_frame.cc @@ -146,6 +146,17 @@ bool TextFrame::IsFrameComplete() const { return bound_values_.size() == run_size; } +const Font& TextFrame::GetFont() const { + return runs_[0].GetFont(); +} + +std::optional TextFrame::AsSingleGlyph() const { + if (runs_.size() == 1 && runs_[0].GetGlyphCount() == 1) { + return runs_[0].GetGlyphPositions()[0].glyph; + } + return std::nullopt; +} + const FrameBounds& TextFrame::GetFrameBounds(size_t index) const { FML_DCHECK(index < bound_values_.size()); return bound_values_[index]; diff --git a/engine/src/flutter/impeller/typographer/text_frame.h b/engine/src/flutter/impeller/typographer/text_frame.h index 9c84ceb447..431edf0aa6 100644 --- a/engine/src/flutter/impeller/typographer/text_frame.h +++ b/engine/src/flutter/impeller/typographer/text_frame.h @@ -7,6 +7,7 @@ #include #include "impeller/geometry/rational.h" +#include "impeller/typographer/glyph.h" #include "impeller/typographer/glyph_atlas.h" #include "impeller/typographer/text_run.h" @@ -80,6 +81,13 @@ class TextFrame { /// This method is only valid if [IsFrameComplete] returns true. const FrameBounds& GetFrameBounds(size_t index) const; + /// @brief If this text frame contains a single glyph (such as for an Icon), + /// then return it, otherwise std::nullopt. + std::optional AsSingleGlyph() const; + + /// @brief Return the font of the first glyph run. + const Font& GetFont() const; + /// @brief Store text frame scale, offset, and properties for hashing in th /// glyph atlas. void SetPerFrameData(Rational scale,