From c4d33221d8d55961814bfea775e5cbef13ed2da3 Mon Sep 17 00:00:00 2001 From: Dan Field Date: Tue, 20 Sep 2022 18:19:07 -0700 Subject: [PATCH] [Impeller] SDF text rendering (flutter/engine#36171) --- .../ci/licenses_golden/licenses_flutter | 2 + .../shader_lib/impeller/transform.glsl | 20 ++ engine/src/flutter/impeller/entity/BUILD.gn | 2 + .../entity/contents/content_context.cc | 2 + .../entity/contents/content_context.h | 10 + .../impeller/entity/contents/text_contents.cc | 123 ++++++++----- .../impeller/entity/contents/text_contents.h | 14 +- .../impeller/entity/entity_unittests.cc | 30 +++ .../impeller/entity/shaders/glyph_atlas.vert | 19 +- .../entity/shaders/glyph_atlas_sdf.frag | 34 ++++ .../entity/shaders/glyph_atlas_sdf.vert | 26 +++ .../backends/skia/text_frame_skia.cc | 14 +- .../backends/skia/text_render_context_skia.cc | 174 +++++++++++++++--- .../backends/skia/text_render_context_skia.h | 1 + .../src/flutter/impeller/typographer/glyph.h | 12 +- .../impeller/typographer/glyph_atlas.cc | 25 +-- .../impeller/typographer/glyph_atlas.h | 55 +++--- .../impeller/typographer/lazy_glyph_atlas.cc | 16 +- .../impeller/typographer/lazy_glyph_atlas.h | 6 +- .../impeller/typographer/text_frame.cc | 5 + .../flutter/impeller/typographer/text_frame.h | 5 + .../typographer/text_render_context.cc | 3 +- .../typographer/text_render_context.h | 8 +- .../flutter/impeller/typographer/text_run.cc | 5 + .../flutter/impeller/typographer/text_run.h | 5 + .../typographer/typographer_unittests.cc | 4 +- 26 files changed, 470 insertions(+), 150 deletions(-) create mode 100644 engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.frag create mode 100644 engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.vert diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 69f21876d63..8be29b36297 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -656,6 +656,8 @@ FILE: ../../../flutter/impeller/entity/shaders/gaussian_blur.frag FILE: ../../../flutter/impeller/entity/shaders/gaussian_blur.vert FILE: ../../../flutter/impeller/entity/shaders/glyph_atlas.frag FILE: ../../../flutter/impeller/entity/shaders/glyph_atlas.vert +FILE: ../../../flutter/impeller/entity/shaders/glyph_atlas_sdf.frag +FILE: ../../../flutter/impeller/entity/shaders/glyph_atlas_sdf.vert FILE: ../../../flutter/impeller/entity/shaders/gradient_fill.vert FILE: ../../../flutter/impeller/entity/shaders/linear_gradient_fill.frag FILE: ../../../flutter/impeller/entity/shaders/linear_to_srgb_filter.frag diff --git a/engine/src/flutter/impeller/compiler/shader_lib/impeller/transform.glsl b/engine/src/flutter/impeller/compiler/shader_lib/impeller/transform.glsl index e57bdf0b141..f4ee335286d 100644 --- a/engine/src/flutter/impeller/compiler/shader_lib/impeller/transform.glsl +++ b/engine/src/flutter/impeller/compiler/shader_lib/impeller/transform.glsl @@ -11,4 +11,24 @@ vec2 IPVec2TransformPosition(mat4 matrix, vec2 point) { return transformed.xy / transformed.w; } +// Returns the transformed gl_Position for a given glyph position in a glyph +// atlas. +vec4 IPPositionForGlyphPosition(mat4 mvp, vec2 unit_vertex, vec2 glyph_position, vec2 glyph_size) { + vec4 translate = mvp[0] * glyph_position.x + + mvp[1] * glyph_position.y + + mvp[3]; + mat4 translated_mvp = mat4( + mvp[0], + mvp[1], + mvp[2], + vec4( + translate.xyz, + mvp[3].w + ) + ); + return translated_mvp * + vec4(unit_vertex.x * glyph_size.x, + unit_vertex.y * glyph_size.y, 0.0, 1.0); +} + #endif diff --git a/engine/src/flutter/impeller/entity/BUILD.gn b/engine/src/flutter/impeller/entity/BUILD.gn index 10bc6ce9a30..c16751e2854 100644 --- a/engine/src/flutter/impeller/entity/BUILD.gn +++ b/engine/src/flutter/impeller/entity/BUILD.gn @@ -36,6 +36,8 @@ impeller_shaders("entity_shaders") { "shaders/gaussian_blur.vert", "shaders/glyph_atlas.frag", "shaders/glyph_atlas.vert", + "shaders/glyph_atlas_sdf.frag", + "shaders/glyph_atlas_sdf.vert", "shaders/gradient_fill.vert", "shaders/linear_to_srgb_filter.frag", "shaders/linear_to_srgb_filter.vert", diff --git a/engine/src/flutter/impeller/entity/contents/content_context.cc b/engine/src/flutter/impeller/entity/contents/content_context.cc index 28475830e6e..80e970f5233 100644 --- a/engine/src/flutter/impeller/entity/contents/content_context.cc +++ b/engine/src/flutter/impeller/entity/contents/content_context.cc @@ -207,6 +207,8 @@ ContentContext::ContentContext(std::shared_ptr context) CreateDefaultPipeline(*context_); glyph_atlas_pipelines_[{}] = CreateDefaultPipeline(*context_); + glyph_atlas_sdf_pipelines_[{}] = + CreateDefaultPipeline(*context_); vertices_pipelines_[{}] = CreateDefaultPipeline(*context_); atlas_pipelines_[{}] = CreateDefaultPipeline(*context_); diff --git a/engine/src/flutter/impeller/entity/contents/content_context.h b/engine/src/flutter/impeller/entity/contents/content_context.h index 548ba4dd6d4..83089ad08aa 100644 --- a/engine/src/flutter/impeller/entity/contents/content_context.h +++ b/engine/src/flutter/impeller/entity/contents/content_context.h @@ -40,6 +40,8 @@ #include "impeller/entity/gaussian_blur.vert.h" #include "impeller/entity/glyph_atlas.frag.h" #include "impeller/entity/glyph_atlas.vert.h" +#include "impeller/entity/glyph_atlas_sdf.frag.h" +#include "impeller/entity/glyph_atlas_sdf.vert.h" #include "impeller/entity/gradient_fill.vert.h" #include "impeller/entity/linear_gradient_fill.frag.h" #include "impeller/entity/linear_to_srgb_filter.frag.h" @@ -144,6 +146,8 @@ using SolidStrokePipeline = RenderPipelineT; using GlyphAtlasPipeline = RenderPipelineT; +using GlyphAtlasSdfPipeline = + RenderPipelineT; using VerticesPipeline = RenderPipelineT; using AtlasPipeline = @@ -272,6 +276,11 @@ class ContentContext { return GetPipeline(glyph_atlas_pipelines_, opts); } + std::shared_ptr> GetGlyphAtlasSdfPipeline( + ContentContextOptions opts) const { + return GetPipeline(glyph_atlas_sdf_pipelines_, opts); + } + std::shared_ptr> GetVerticesPipeline( ContentContextOptions opts) const { return GetPipeline(vertices_pipelines_, opts); @@ -399,6 +408,7 @@ class ContentContext { mutable Variants solid_stroke_pipelines_; mutable Variants clip_pipelines_; mutable Variants glyph_atlas_pipelines_; + mutable Variants glyph_atlas_sdf_pipelines_; mutable Variants vertices_pipelines_; mutable Variants atlas_pipelines_; // Advanced blends. diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.cc b/engine/src/flutter/impeller/entity/contents/text_contents.cc index 6cc0c292219..43dc5980231 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/text_contents.cc @@ -4,8 +4,8 @@ #include "impeller/entity/contents/text_contents.h" -#include #include +#include #include "impeller/entity/contents/content_context.h" #include "impeller/entity/entity.h" @@ -28,22 +28,16 @@ void TextContents::SetTextFrame(TextFrame frame) { frame_ = std::move(frame); } -void TextContents::SetGlyphAtlas(std::shared_ptr atlas) { - atlas_ = std::move(atlas); -} - void TextContents::SetGlyphAtlas(std::shared_ptr atlas) { - atlas_ = std::move(atlas); + lazy_atlas_ = std::move(atlas); } std::shared_ptr TextContents::ResolveAtlas( + GlyphAtlas::Type type, std::shared_ptr context) const { - if (auto lazy_atlas = std::get_if>(&atlas_)) { - return lazy_atlas->get()->CreateOrGetGlyphAtlas(context); - } - - if (auto atlas = std::get_if>(&atlas_)) { - return *atlas; + FML_DCHECK(lazy_atlas_); + if (lazy_atlas_) { + return lazy_atlas_->CreateOrGetGlyphAtlas(type, context); } return nullptr; @@ -61,33 +55,19 @@ std::optional TextContents::GetCoverage(const Entity& entity) const { return bounds->TransformBounds(entity.GetTransformation()); } -bool TextContents::Render(const ContentContext& renderer, - const Entity& entity, - RenderPass& pass) const { - if (color_.IsTransparent()) { - return true; - } - - auto atlas = ResolveAtlas(renderer.GetContext()); - - if (!atlas || !atlas->IsValid()) { - VALIDATION_LOG << "Cannot render glyphs without prepared atlas."; - return false; - } - - using VS = GlyphAtlasPipeline::VertexShader; - using FS = GlyphAtlasPipeline::FragmentShader; - - // Information shared by all glyph draw calls. - Command cmd; - cmd.label = "TextFrame"; - cmd.primitive_type = PrimitiveType::kTriangle; - cmd.pipeline = - renderer.GetGlyphAtlasPipeline(OptionsFromPassAndEntity(pass, entity)); - cmd.stencil_reference = entity.GetStencilDepth(); +template +static bool CommonRender(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass, + const Color& color, + const TextFrame& frame, + std::shared_ptr atlas, + Command& cmd) { + using VS = typename TPipeline::VertexShader; + using FS = typename TPipeline::FragmentShader; // Common vertex uniforms for all glyphs. - VS::FrameInfo frame_info; + typename VS::FrameInfo frame_info; frame_info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * entity.GetTransformation(); VS::BindFrameInfo(cmd, pass.GetTransientsBuffer().EmplaceUniform(frame_info)); @@ -96,8 +76,8 @@ bool TextContents::Render(const ContentContext& renderer, sampler_desc.min_filter = MinMagFilter::kLinear; sampler_desc.mag_filter = MinMagFilter::kLinear; - FS::FragInfo frag_info; - frag_info.text_color = ToVector(color_.Premultiply()); + typename FS::FragInfo frag_info; + frag_info.text_color = ToVector(color.Premultiply()); frag_info.atlas_size = Point{static_cast(atlas->GetTexture()->GetSize().width), static_cast(atlas->GetTexture()->GetSize().height)}; @@ -122,16 +102,15 @@ bool TextContents::Render(const ContentContext& renderer, {0, 0}, {1, 0}, {0, 1}, {1, 0}, {0, 1}, {1, 1}, }; - VertexBufferBuilder vertex_builder; - for (const auto& run : frame_.GetRuns()) { + VertexBufferBuilder vertex_builder; + for (const auto& run : frame.GetRuns()) { auto font = run.GetFont(); auto glyph_size = ISize::Ceil(font.GetMetrics().GetBoundingBox().size); for (const auto& glyph_position : run.GetGlyphPositions()) { FontGlyphPair font_glyph_pair{font, glyph_position.glyph}; - auto color_glyph = - atlas->IsColorFontGlyphPair(font_glyph_pair) ? 1.0 : 0.0; + for (const auto& point : unit_vertex_points) { - VS::PerVertexData vtx; + typename VS::PerVertexData vtx; vtx.unit_vertex = point; auto atlas_glyph_pos = atlas->FindFontGlyphPosition(font_glyph_pair); @@ -149,7 +128,10 @@ bool TextContents::Render(const ContentContext& renderer, 1 / atlas_glyph_pos->size.height}; vtx.atlas_glyph_size = Point{atlas_glyph_pos->size.width, atlas_glyph_pos->size.height}; - vtx.color_glyph = color_glyph; + if constexpr (std::is_same_v) { + vtx.color_glyph = + glyph_position.glyph.type == Glyph::Type::kBitmap ? 1.0 : 0.0; + } vertex_builder.AppendVertex(std::move(vtx)); } } @@ -165,4 +147,55 @@ bool TextContents::Render(const ContentContext& renderer, return true; } +bool TextContents::RenderSdf(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + auto atlas = ResolveAtlas(GlyphAtlas::Type::kSignedDistanceField, + renderer.GetContext()); + + if (!atlas || !atlas->IsValid()) { + VALIDATION_LOG << "Cannot render glyphs without prepared atlas."; + return false; + } + + // Information shared by all glyph draw calls. + Command cmd; + cmd.label = "TextFrameSDF"; + cmd.primitive_type = PrimitiveType::kTriangle; + cmd.pipeline = + renderer.GetGlyphAtlasSdfPipeline(OptionsFromPassAndEntity(pass, entity)); + cmd.stencil_reference = entity.GetStencilDepth(); + + return CommonRender(renderer, entity, pass, color_, + frame_, atlas, cmd); +} + +bool TextContents::Render(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const { + if (color_.IsTransparent()) { + return true; + } + + auto atlas = ResolveAtlas(frame_.HasColor() ? GlyphAtlas::Type::kColorBitmap + : GlyphAtlas::Type::kAlphaBitmap, + renderer.GetContext()); + + if (!atlas || !atlas->IsValid()) { + VALIDATION_LOG << "Cannot render glyphs without prepared atlas."; + return false; + } + + // Information shared by all glyph draw calls. + Command cmd; + cmd.label = "TextFrame"; + cmd.primitive_type = PrimitiveType::kTriangle; + cmd.pipeline = + renderer.GetGlyphAtlasPipeline(OptionsFromPassAndEntity(pass, entity)); + cmd.stencil_reference = entity.GetStencilDepth(); + + return CommonRender(renderer, entity, pass, color_, + frame_, atlas, cmd); +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.h b/engine/src/flutter/impeller/entity/contents/text_contents.h index 1caa38b77ad..015502ac502 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.h +++ b/engine/src/flutter/impeller/entity/contents/text_contents.h @@ -12,11 +12,11 @@ #include "flutter/fml/macros.h" #include "impeller/entity/contents/contents.h" #include "impeller/geometry/color.h" +#include "impeller/typographer/glyph_atlas.h" #include "impeller/typographer/text_frame.h" namespace impeller { -class GlyphAtlas; class LazyGlyphAtlas; class Context; @@ -28,8 +28,6 @@ class TextContents final : public Contents { void SetTextFrame(TextFrame frame); - void SetGlyphAtlas(std::shared_ptr atlas); - void SetGlyphAtlas(std::shared_ptr atlas); void SetColor(Color color); @@ -42,14 +40,18 @@ class TextContents final : public Contents { const Entity& entity, RenderPass& pass) const override; + // TODO(dnfield): remove this https://github.com/flutter/flutter/issues/111640 + bool RenderSdf(const ContentContext& renderer, + const Entity& entity, + RenderPass& pass) const; + private: TextFrame frame_; Color color_; - mutable std::variant, - std::shared_ptr> - atlas_; + mutable std::shared_ptr lazy_atlas_; std::shared_ptr ResolveAtlas( + GlyphAtlas::Type type, std::shared_ptr context) const; FML_DISALLOW_COPY_AND_ASSIGN(TextContents); diff --git a/engine/src/flutter/impeller/entity/entity_unittests.cc b/engine/src/flutter/impeller/entity/entity_unittests.cc index ce7fdffa264..e5cc679d025 100644 --- a/engine/src/flutter/impeller/entity/entity_unittests.cc +++ b/engine/src/flutter/impeller/entity/entity_unittests.cc @@ -20,6 +20,7 @@ #include "impeller/entity/contents/rrect_shadow_contents.h" #include "impeller/entity/contents/solid_color_contents.h" #include "impeller/entity/contents/solid_stroke_contents.h" +#include "impeller/entity/contents/text_contents.h" #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/contents/vertices_contents.h" #include "impeller/entity/entity.h" @@ -35,8 +36,11 @@ #include "impeller/renderer/render_pass.h" #include "impeller/renderer/vertex_buffer_builder.h" #include "impeller/tessellator/tessellator.h" +#include "impeller/typographer/backends/skia/text_frame_skia.h" +#include "impeller/typographer/backends/skia/text_render_context_skia.h" #include "include/core/SkBlendMode.h" #include "third_party/imgui/imgui.h" +#include "third_party/skia/include/core/SkTextBlob.h" namespace impeller { namespace testing { @@ -1976,5 +1980,31 @@ TEST_P(EntityTest, TTTBlendColor) { } } +TEST_P(EntityTest, SdfText) { + auto callback = [&](ContentContext& context, RenderPass& pass) -> bool { + SkFont font; + font.setSize(30); + auto blob = SkTextBlob::MakeFromString( + "the quick brown fox jumped over the lazy dog (but with sdf).", font); + auto frame = TextFrameFromTextBlob(blob); + auto lazy_glyph_atlas = std::make_shared(); + lazy_glyph_atlas->AddTextFrame(frame); + + auto text_contents = std::make_shared(); + text_contents->SetTextFrame(std::move(frame)); + text_contents->SetGlyphAtlas(std::move(lazy_glyph_atlas)); + text_contents->SetColor(Color(1.0, 0.0, 0.0, 1.0)); + Entity entity; + entity.SetTransformation( + Matrix::MakeTranslation(Vector3{200.0, 200.0, 0.0}) * + Matrix::MakeScale(GetContentScale())); + entity.SetContents(text_contents); + + // Force SDF rendering. + return text_contents->RenderSdf(context, entity, pass); + }; + ASSERT_TRUE(OpenPlaygroundHere(callback)); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/entity/shaders/glyph_atlas.vert b/engine/src/flutter/impeller/entity/shaders/glyph_atlas.vert index 1ac172c0829..e332d67d3be 100644 --- a/engine/src/flutter/impeller/entity/shaders/glyph_atlas.vert +++ b/engine/src/flutter/impeller/entity/shaders/glyph_atlas.vert @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include + uniform FrameInfo { mat4 mvp; } frame_info; @@ -19,22 +21,7 @@ out vec2 v_atlas_glyph_size; out float v_color_glyph; void main() { - vec4 translate = frame_info.mvp[0] * glyph_position.x - + frame_info.mvp[1] * glyph_position.y - + frame_info.mvp[3]; - mat4 translated_mvp = mat4( - frame_info.mvp[0], - frame_info.mvp[1], - frame_info.mvp[2], - vec4( - translate.xyz, - frame_info.mvp[3].w - ) - ); - gl_Position = translated_mvp - * vec4(unit_vertex.x * glyph_size.x, - unit_vertex.y * glyph_size.y, 0.0, 1.0); - + gl_Position = IPPositionForGlyphPosition(frame_info.mvp, unit_vertex, glyph_position, glyph_size); v_unit_vertex = unit_vertex; v_atlas_position = atlas_position; v_atlas_glyph_size = atlas_glyph_size; diff --git a/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.frag b/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.frag new file mode 100644 index 00000000000..38518cee424 --- /dev/null +++ b/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.frag @@ -0,0 +1,34 @@ +// 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. + +uniform sampler2D glyph_atlas_sampler; + +uniform FragInfo { + vec2 atlas_size; + vec4 text_color; +} frag_info; + +in vec2 v_unit_vertex; +in vec2 v_atlas_position; +in vec2 v_atlas_glyph_size; + +out vec4 frag_color; + +void main() { + vec2 scale_perspective = v_atlas_glyph_size / frag_info.atlas_size; + vec2 offset = v_atlas_position / frag_info.atlas_size; + + // Inspired by Metal by Example's SDF text rendering shader: + // https://github.com/metal-by-example/sample-code/blob/master/objc/12-TextRendering/TextRendering/Shaders.metal + + // Outline of glyph is the isocontour with value 50% + float edge_distance = 0.5; + // Sample the signed-distance field to find distance from this fragment to the glyph outline + float sample_distance = texture(glyph_atlas_sampler, v_unit_vertex * scale_perspective + offset).a; + // Use local automatic gradients to find anti-aliased anisotropic edge width, cf. Gustavson 2012 + float edge_width = length(vec2(dFdx(sample_distance), dFdy(sample_distance))); + // Smooth the glyph edge by interpolating across the boundary in a band with the width determined above + float insideness = smoothstep(edge_distance - edge_width, edge_distance + edge_width, sample_distance); + frag_color = frag_info.text_color * insideness; +} diff --git a/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.vert b/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.vert new file mode 100644 index 00000000000..830566304a1 --- /dev/null +++ b/engine/src/flutter/impeller/entity/shaders/glyph_atlas_sdf.vert @@ -0,0 +1,26 @@ +// 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 + +uniform FrameInfo { + mat4 mvp; +} frame_info; + +in vec2 unit_vertex; +in vec2 glyph_position; +in vec2 glyph_size; +in vec2 atlas_position; +in vec2 atlas_glyph_size; + +out vec2 v_unit_vertex; +out vec2 v_atlas_position; +out vec2 v_atlas_glyph_size; + +void main() { + gl_Position = IPPositionForGlyphPosition(frame_info.mvp, unit_vertex, glyph_position, glyph_size); + v_unit_vertex = unit_vertex; + v_atlas_position = atlas_position; + v_atlas_glyph_size = atlas_glyph_size; +} diff --git a/engine/src/flutter/impeller/typographer/backends/skia/text_frame_skia.cc b/engine/src/flutter/impeller/typographer/backends/skia/text_frame_skia.cc index 287876de3a7..132d896b699 100644 --- a/engine/src/flutter/impeller/typographer/backends/skia/text_frame_skia.cc +++ b/engine/src/flutter/impeller/typographer/backends/skia/text_frame_skia.cc @@ -8,6 +8,7 @@ #include "impeller/typographer/backends/skia/typeface_skia.h" #include "third_party/skia/include/core/SkFont.h" #include "third_party/skia/include/core/SkFontMetrics.h" +#include "third_party/skia/src/core/SkStrikeSpec.h" // nogncheck #include "third_party/skia/src/core/SkTextBlobPriv.h" // nogncheck namespace impeller { @@ -38,6 +39,12 @@ TextFrame TextFrameFromTextBlob(sk_sp blob, Scalar scale) { for (SkTextBlobRunIterator run(blob.get()); !run.done(); run.next()) { TextRun text_run(ToFont(run.font(), scale)); + + // TODO(jonahwilliams): ask Skia for a public API to look this up. + // https://github.com/flutter/flutter/issues/112005 + SkStrikeSpec strikeSpec = SkStrikeSpec::MakeWithNoDevice(run.font()); + SkBulkGlyphMetricsAndPaths paths{strikeSpec}; + const auto glyph_count = run.glyphCount(); const auto* glyphs = run.glyphs(); switch (run.positioning()) { @@ -52,7 +59,12 @@ TextFrame TextFrameFromTextBlob(sk_sp blob, Scalar scale) { // kFull_Positioning has two scalars per glyph. const SkPoint* glyph_points = run.points(); const auto* point = glyph_points + i; - text_run.AddGlyph(glyphs[i], Point{point->x(), point->y()}); + Glyph::Type type = paths.glyph(glyphs[i])->isColor() + ? Glyph::Type::kBitmap + : Glyph::Type::kPath; + + text_run.AddGlyph(Glyph{glyphs[i], type}, + Point{point->x(), point->y()}); } break; case SkTextBlobRunIterator::kRSXform_Positioning: diff --git a/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.cc b/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.cc index d6e718c3a64..12111552b57 100644 --- a/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.cc +++ b/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.cc @@ -15,9 +15,8 @@ #include "third_party/skia/include/core/SkFontMetrics.h" #include "third_party/skia/include/core/SkRSXform.h" #include "third_party/skia/include/core/SkSurface.h" -#include "third_party/skia/src/core/SkIPoint16.h" //nogncheck -#include "third_party/skia/src/core/SkStrikeSpec.h" // nogncheck -#include "third_party/skia/src/gpu/GrRectanizer.h" //nogncheck +#include "third_party/skia/src/core/SkIPoint16.h" // nogncheck +#include "third_party/skia/src/gpu/GrRectanizer.h" // nogncheck namespace impeller { @@ -27,11 +26,15 @@ TextRenderContextSkia::TextRenderContextSkia(std::shared_ptr context) TextRenderContextSkia::~TextRenderContextSkia() = default; static FontGlyphPair::Set CollectUniqueFontGlyphPairsSet( + GlyphAtlas::Type type, TextRenderContext::FrameIterator frame_iterator) { FontGlyphPair::Set set; while (auto frame = frame_iterator()) { for (const auto& run : frame->GetRuns()) { auto font = run.GetFont(); + // TODO(dnfield): If we're doing SDF here, we should be using a consistent + // point size. + // https://github.com/flutter/flutter/issues/112016 for (const auto& glyph_position : run.GetGlyphPositions()) { set.insert({font, glyph_position.glyph}); } @@ -41,10 +44,11 @@ static FontGlyphPair::Set CollectUniqueFontGlyphPairsSet( } static FontGlyphPair::Vector CollectUniqueFontGlyphPairs( + GlyphAtlas::Type type, TextRenderContext::FrameIterator frame_iterator) { TRACE_EVENT0("impeller", __FUNCTION__); FontGlyphPair::Vector vector; - auto set = CollectUniqueFontGlyphPairsSet(frame_iterator); + auto set = CollectUniqueFontGlyphPairsSet(type, frame_iterator); vector.reserve(set.size()); for (const auto& item : set) { vector.emplace_back(std::move(item)); @@ -104,13 +108,137 @@ static size_t OptimumAtlasSizeForFontGlyphPairs( return 0u; } +/// Compute signed-distance field for an 8-bpp grayscale image (values greater +/// than 127 are considered "on") For details of this algorithm, see "The 'dead +/// reckoning' signed distance transform" [Grevera 2004] +static void ConvertBitmapToSignedDistanceField(uint8_t* pixels, + uint16_t width, + uint16_t height) { + if (!pixels || width == 0 || height == 0) { + return; + } + + using ShortPoint = TPoint; + + // distance to nearest boundary point map + std::vector distance_map(width * height); + // nearest boundary point map + std::vector boundary_point_map(width * height); + + // Some helpers for manipulating the above arrays +#define image(_x, _y) (pixels[(_y)*width + (_x)] > 0x7f) +#define distance(_x, _y) distance_map[(_y)*width + (_x)] +#define nearestpt(_x, _y) boundary_point_map[(_y)*width + (_x)] + + const Scalar maxDist = hypot(width, height); + const Scalar distUnit = 1; + const Scalar distDiag = sqrt(2); + + // Initialization phase: set all distances to "infinity"; zero out nearest + // boundary point map + for (uint16_t y = 0; y < height; ++y) { + for (uint16_t x = 0; x < width; ++x) { + distance(x, y) = maxDist; + nearestpt(x, y) = ShortPoint{0, 0}; + } + } + + // Immediate interior/exterior phase: mark all points along the boundary as + // such + for (uint16_t y = 1; y < height - 1; ++y) { + for (uint16_t x = 1; x < width - 1; ++x) { + bool inside = image(x, y); + if (image(x - 1, y) != inside || image(x + 1, y) != inside || + image(x, y - 1) != inside || image(x, y + 1) != inside) { + distance(x, y) = 0; + nearestpt(x, y) = ShortPoint{x, y}; + } + } + } + + // Forward dead-reckoning pass + for (uint16_t y = 1; y < height - 2; ++y) { + for (uint16_t x = 1; x < width - 2; ++x) { + if (distance_map[(y - 1) * width + (x - 1)] + distDiag < distance(x, y)) { + nearestpt(x, y) = nearestpt(x - 1, y - 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x, y - 1) + distUnit < distance(x, y)) { + nearestpt(x, y) = nearestpt(x, y - 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x + 1, y - 1) + distDiag < distance(x, y)) { + nearestpt(x, y) = nearestpt(x + 1, y - 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x - 1, y) + distUnit < distance(x, y)) { + nearestpt(x, y) = nearestpt(x - 1, y); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + } + } + + // Backward dead-reckoning pass + for (uint16_t y = height - 2; y >= 1; --y) { + for (uint16_t x = width - 2; x >= 1; --x) { + if (distance(x + 1, y) + distUnit < distance(x, y)) { + nearestpt(x, y) = nearestpt(x + 1, y); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x - 1, y + 1) + distDiag < distance(x, y)) { + nearestpt(x, y) = nearestpt(x - 1, y + 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x, y + 1) + distUnit < distance(x, y)) { + nearestpt(x, y) = nearestpt(x, y + 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + if (distance(x + 1, y + 1) + distDiag < distance(x, y)) { + nearestpt(x, y) = nearestpt(x + 1, y + 1); + distance(x, y) = hypot(x - nearestpt(x, y).x, y - nearestpt(x, y).y); + } + } + } + + // Interior distance negation pass; distances outside the figure are + // considered negative + // Also does final quantization. + for (uint16_t y = 0; y < height; ++y) { + for (uint16_t x = 0; x < width; ++x) { + if (!image(x, y)) { + distance(x, y) = -distance(x, y); + } + + float norm_factor = 13.5; + float dist = distance(x, y); + float clamped_dist = fmax(-norm_factor, fmin(dist, norm_factor)); + float scaled_dist = clamped_dist / norm_factor; + uint8_t quantized_value = ((scaled_dist + 1) / 2) * UINT8_MAX; + pixels[y * width + x] = quantized_value; + } + } + +#undef image +#undef distance +#undef nearestpt +} + static std::shared_ptr CreateAtlasBitmap(const GlyphAtlas& atlas, size_t atlas_size) { TRACE_EVENT0("impeller", __FUNCTION__); auto bitmap = std::make_shared(); - auto image_info = atlas.ContainsColorGlyph() - ? SkImageInfo::MakeN32Premul(atlas_size, atlas_size) - : SkImageInfo::MakeA8(atlas_size, atlas_size); + SkImageInfo image_info; + + switch (atlas.GetType()) { + case GlyphAtlas::Type::kSignedDistanceField: + case GlyphAtlas::Type::kAlphaBitmap: + image_info = SkImageInfo::MakeA8(atlas_size, atlas_size); + break; + case GlyphAtlas::Type::kColorBitmap: + image_info = SkImageInfo::MakeN32Premul(atlas_size, atlas_size); + break; + } + if (!bitmap->tryAllocPixels(image_info)) { return nullptr; } @@ -133,7 +261,6 @@ static std::shared_ptr CreateAtlasBitmap(const GlyphAtlas& atlas, SkFont sk_font( TypefaceSkia::Cast(*font_glyph.font.GetTypeface()).GetSkiaTypeface(), metrics.point_size); - auto glyph_color = SK_ColorWHITE; SkPaint glyph_paint; @@ -196,29 +323,20 @@ static std::shared_ptr UploadGlyphTextureAtlas( } std::shared_ptr TextRenderContextSkia::CreateGlyphAtlas( + GlyphAtlas::Type type, FrameIterator frame_iterator) const { TRACE_EVENT0("impeller", __FUNCTION__); if (!IsValid()) { return nullptr; } - auto glyph_atlas = std::make_shared(); - glyph_atlas->SetFontColorCallback([](const FontGlyphPair& font_glyph) { - // TODO(jonahwilliams): ask Skia for a public API to look this up. - SkFont sk_font( - TypefaceSkia::Cast(*font_glyph.font.GetTypeface()).GetSkiaTypeface(), - font_glyph.font.GetMetrics().point_size); - - SkStrikeSpec strikeSpec = SkStrikeSpec::MakeWithNoDevice(sk_font); - SkBulkGlyphMetricsAndPaths paths{strikeSpec}; - return paths.glyph(font_glyph.glyph.index)->isColor(); - }); + auto glyph_atlas = std::make_shared(type); // --------------------------------------------------------------------------- // Step 1: Collect unique font-glyph pairs in the frame. // --------------------------------------------------------------------------- - auto font_glyph_pairs = CollectUniqueFontGlyphPairs(frame_iterator); + auto font_glyph_pairs = CollectUniqueFontGlyphPairs(type, frame_iterator); if (font_glyph_pairs.empty()) { return glyph_atlas; } @@ -262,9 +380,19 @@ std::shared_ptr TextRenderContextSkia::CreateGlyphAtlas( // --------------------------------------------------------------------------- // Step 6: Upload the atlas as a texture. // --------------------------------------------------------------------------- - auto format = glyph_atlas->ContainsColorGlyph() - ? PixelFormat::kR8G8B8A8UNormInt - : PixelFormat::kA8UNormInt; + PixelFormat format; + switch (type) { + case GlyphAtlas::Type::kSignedDistanceField: + ConvertBitmapToSignedDistanceField( + reinterpret_cast(bitmap->getPixels()), atlas_size, + atlas_size); + case GlyphAtlas::Type::kAlphaBitmap: + format = PixelFormat::kA8UNormInt; + break; + case GlyphAtlas::Type::kColorBitmap: + format = PixelFormat::kR8G8B8A8UNormInt; + break; + } auto texture = UploadGlyphTextureAtlas(GetContext()->GetResourceAllocator(), bitmap, atlas_size, format); if (!texture) { diff --git a/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.h b/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.h index 26dfb19a807..1b11df9d79c 100644 --- a/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.h +++ b/engine/src/flutter/impeller/typographer/backends/skia/text_render_context_skia.h @@ -17,6 +17,7 @@ class TextRenderContextSkia : public TextRenderContext { // |TextRenderContext| std::shared_ptr CreateGlyphAtlas( + GlyphAtlas::Type type, FrameIterator iterator) const override; private: diff --git a/engine/src/flutter/impeller/typographer/glyph.h b/engine/src/flutter/impeller/typographer/glyph.h index 000346ba9d1..d377afac465 100644 --- a/engine/src/flutter/impeller/typographer/glyph.h +++ b/engine/src/flutter/impeller/typographer/glyph.h @@ -15,9 +15,19 @@ namespace impeller { /// @brief The glyph index in the typeface. /// struct Glyph { + enum class Type { + kPath, + kBitmap, + }; + uint16_t index = 0; - Glyph(uint16_t p_index) : index(p_index) {} + //------------------------------------------------------------------------------ + /// @brief Whether the glyph is a path or a bitmap. + /// + Type type = Type::kPath; + + Glyph(uint16_t p_index, Type p_type) : index(p_index), type(p_type) {} }; } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/glyph_atlas.cc b/engine/src/flutter/impeller/typographer/glyph_atlas.cc index 5521780d2be..76867746eeb 100644 --- a/engine/src/flutter/impeller/typographer/glyph_atlas.cc +++ b/engine/src/flutter/impeller/typographer/glyph_atlas.cc @@ -6,7 +6,7 @@ namespace impeller { -GlyphAtlas::GlyphAtlas() = default; +GlyphAtlas::GlyphAtlas(Type type) : type_(type) {} GlyphAtlas::~GlyphAtlas() = default; @@ -14,8 +14,8 @@ bool GlyphAtlas::IsValid() const { return !!texture_; } -bool GlyphAtlas::ContainsColorGlyph() const { - return has_color_glyph; +GlyphAtlas::Type GlyphAtlas::GetType() const { + return type_; } const std::shared_ptr& GlyphAtlas::GetTexture() const { @@ -27,28 +27,9 @@ void GlyphAtlas::SetTexture(std::shared_ptr texture) { } void GlyphAtlas::AddTypefaceGlyphPosition(FontGlyphPair pair, Rect rect) { - if (callback_.has_value()) { - auto has_color = callback_.value()(pair); - has_color_glyph |= has_color; - colors_[pair] = has_color; - } - positions_[pair] = rect; } -void GlyphAtlas::SetFontColorCallback( - std::function callback) { - callback_ = std::move(callback); -} - -bool GlyphAtlas::IsColorFontGlyphPair(const FontGlyphPair& pair) const { - auto found = colors_.find(pair); - if (found == colors_.end()) { - return false; - } - return found->second; -} - std::optional GlyphAtlas::FindFontGlyphPosition( const FontGlyphPair& pair) const { auto found = positions_.find(pair); diff --git a/engine/src/flutter/impeller/typographer/glyph_atlas.h b/engine/src/flutter/impeller/typographer/glyph_atlas.h index cec7222ae9e..676b934416b 100644 --- a/engine/src/flutter/impeller/typographer/glyph_atlas.h +++ b/engine/src/flutter/impeller/typographer/glyph_atlas.h @@ -11,6 +11,7 @@ #include "flutter/fml/macros.h" #include "impeller/geometry/rect.h" +#include "impeller/renderer/pipeline.h" #include "impeller/renderer/texture.h" #include "impeller/typographer/font_glyph_pair.h" @@ -23,19 +24,44 @@ namespace impeller { /// class GlyphAtlas { public: + //---------------------------------------------------------------------------- + /// @brief Describes how the glyphs are represented in the texture. + enum class Type { + //-------------------------------------------------------------------------- + /// The glyphs are represented at a fixed size in an 8-bit grayscale texture + /// where the value of each pixel represents a signed-distance field that + /// stores the glyph outlines. + /// + kSignedDistanceField, + + //-------------------------------------------------------------------------- + /// The glyphs are reprsented at their requested size using only an 8-bit + /// alpha channel. + /// + kAlphaBitmap, + + //-------------------------------------------------------------------------- + /// The glyphs are reprsented at their requested size using N32 premul + /// colors. + /// + kColorBitmap, + }; + //---------------------------------------------------------------------------- /// @brief Create an empty glyph atlas. /// - GlyphAtlas(); + /// @param[in] type How the glyphs are represented in the texture. + /// + explicit GlyphAtlas(Type type); ~GlyphAtlas(); bool IsValid() const; //---------------------------------------------------------------------------- - /// @brief Whether at least one font-glyph pair has colors. + /// @brief Describes how the glyphs are represented in the texture. /// - bool ContainsColorGlyph() const; + Type GetType() const; //---------------------------------------------------------------------------- /// @brief Set the texture for the glyph atlas. @@ -44,20 +70,6 @@ class GlyphAtlas { /// void SetTexture(std::shared_ptr texture); - //---------------------------------------------------------------------------- - /// @brief Set a callback that determines if a glyph-font pair - /// has color. - /// - /// @param[in] callback The callback - /// - void SetFontColorCallback( - std::function callback); - - //---------------------------------------------------------------------------- - /// @brief Whether the provided glyph-font pair contains color. - /// - bool IsColorFontGlyphPair(const FontGlyphPair& pair) const; - //---------------------------------------------------------------------------- /// @brief Get the texture for the glyph atlas. /// @@ -104,9 +116,8 @@ class GlyphAtlas { std::optional FindFontGlyphPosition(const FontGlyphPair& pair) const; private: + const Type type_; std::shared_ptr texture_; - std::optional> callback_; - bool has_color_glyph = false; std::unordered_map positions_; - std::unordered_map - colors_; - FML_DISALLOW_COPY_AND_ASSIGN(GlyphAtlas); }; diff --git a/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.cc b/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.cc index e2b2631b926..a3ce97401b1 100644 --- a/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.cc +++ b/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.cc @@ -14,14 +14,18 @@ LazyGlyphAtlas::LazyGlyphAtlas() = default; LazyGlyphAtlas::~LazyGlyphAtlas() = default; void LazyGlyphAtlas::AddTextFrame(TextFrame frame) { - FML_DCHECK(!atlas_); + FML_DCHECK(atlas_map_.empty()); frames_.emplace_back(std::move(frame)); } std::shared_ptr LazyGlyphAtlas::CreateOrGetGlyphAtlas( + GlyphAtlas::Type type, std::shared_ptr context) const { - if (atlas_) { - return atlas_; + { + auto atlas_it = atlas_map_.find(type); + if (atlas_it != atlas_map_.end()) { + return atlas_it->second; + } } auto text_context = TextRenderContext::Create(std::move(context)); @@ -37,13 +41,13 @@ std::shared_ptr LazyGlyphAtlas::CreateOrGetGlyphAtlas( i++; return &result; }; - auto atlas = text_context->CreateGlyphAtlas(iterator); + auto atlas = text_context->CreateGlyphAtlas(type, iterator); if (!atlas || !atlas->IsValid()) { VALIDATION_LOG << "Could not create valid atlas."; return nullptr; } - atlas_ = std::move(atlas); - return atlas_; + atlas_map_[type] = atlas; + return atlas; } } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.h b/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.h index 8d36e05cc4d..6a6dd382721 100644 --- a/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.h +++ b/engine/src/flutter/impeller/typographer/lazy_glyph_atlas.h @@ -4,6 +4,8 @@ #pragma once +#include + #include "flutter/fml/macros.h" #include "impeller/renderer/context.h" #include "impeller/typographer/glyph_atlas.h" @@ -20,11 +22,13 @@ class LazyGlyphAtlas { void AddTextFrame(TextFrame frame); std::shared_ptr CreateOrGetGlyphAtlas( + GlyphAtlas::Type type, std::shared_ptr context) const; private: std::vector frames_; - mutable std::shared_ptr atlas_; + mutable std::unordered_map> + atlas_map_; FML_DISALLOW_COPY_AND_ASSIGN(LazyGlyphAtlas); }; diff --git a/engine/src/flutter/impeller/typographer/text_frame.cc b/engine/src/flutter/impeller/typographer/text_frame.cc index fc0f622344b..a4a05add870 100644 --- a/engine/src/flutter/impeller/typographer/text_frame.cc +++ b/engine/src/flutter/impeller/typographer/text_frame.cc @@ -29,6 +29,7 @@ bool TextFrame::AddTextRun(TextRun run) { if (!run.IsValid()) { return false; } + has_color_ |= run.HasColor(); runs_.emplace_back(std::move(run)); return true; } @@ -41,4 +42,8 @@ const std::vector& TextFrame::GetRuns() const { return runs_; } +bool TextFrame::HasColor() const { + return has_color_; +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/text_frame.h b/engine/src/flutter/impeller/typographer/text_frame.h index 854b6a2fb0b..fdbba9d6e5f 100644 --- a/engine/src/flutter/impeller/typographer/text_frame.h +++ b/engine/src/flutter/impeller/typographer/text_frame.h @@ -52,8 +52,13 @@ class TextFrame { /// const std::vector& GetRuns() const; + //---------------------------------------------------------------------------- + /// @brief Whether any run in this frame has color. + bool HasColor() const; + private: std::vector runs_; + bool has_color_ = false; }; } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/text_render_context.cc b/engine/src/flutter/impeller/typographer/text_render_context.cc index beecdaae516..c19e7ac01f1 100644 --- a/engine/src/flutter/impeller/typographer/text_render_context.cc +++ b/engine/src/flutter/impeller/typographer/text_render_context.cc @@ -33,6 +33,7 @@ const std::shared_ptr& TextRenderContext::GetContext() const { } std::shared_ptr TextRenderContext::CreateGlyphAtlas( + GlyphAtlas::Type type, const TextFrame& frame) const { size_t count = 0; FrameIterator iterator = [&]() -> const TextFrame* { @@ -42,7 +43,7 @@ std::shared_ptr TextRenderContext::CreateGlyphAtlas( } return nullptr; }; - return CreateGlyphAtlas(iterator); + return CreateGlyphAtlas(type, iterator); } } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/text_render_context.h b/engine/src/flutter/impeller/typographer/text_render_context.h index c725c03c5b1..7c2277070c8 100644 --- a/engine/src/flutter/impeller/typographer/text_render_context.h +++ b/engine/src/flutter/impeller/typographer/text_render_context.h @@ -38,10 +38,16 @@ class TextRenderContext { const std::shared_ptr& GetContext() const; using FrameIterator = std::function; + + // TODO(dnfield): Callers should not need to know which type of atlas to + // create. https://github.com/flutter/flutter/issues/111640 + virtual std::shared_ptr CreateGlyphAtlas( + GlyphAtlas::Type type, FrameIterator iterator) const = 0; - std::shared_ptr CreateGlyphAtlas(const TextFrame& frame) const; + std::shared_ptr CreateGlyphAtlas(GlyphAtlas::Type type, + const TextFrame& frame) const; protected: //---------------------------------------------------------------------------- diff --git a/engine/src/flutter/impeller/typographer/text_run.cc b/engine/src/flutter/impeller/typographer/text_run.cc index fdcc6edeb2e..f7f542a28bf 100644 --- a/engine/src/flutter/impeller/typographer/text_run.cc +++ b/engine/src/flutter/impeller/typographer/text_run.cc @@ -17,6 +17,7 @@ TextRun::~TextRun() = default; bool TextRun::AddGlyph(Glyph glyph, Point position) { glyphs_.emplace_back(GlyphPosition{glyph, position}); + has_color_ |= glyph.type == Glyph::Type::kBitmap; return true; } @@ -36,4 +37,8 @@ const Font& TextRun::GetFont() const { return font_; } +bool TextRun::HasColor() const { + return has_color_; +} + } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/text_run.h b/engine/src/flutter/impeller/typographer/text_run.h index f8891ba2046..24851c6dda7 100644 --- a/engine/src/flutter/impeller/typographer/text_run.h +++ b/engine/src/flutter/impeller/typographer/text_run.h @@ -68,10 +68,15 @@ class TextRun { /// const Font& GetFont() const; + //---------------------------------------------------------------------------- + /// @brief Whether any glyph in this run has color. + bool HasColor() const; + private: Font font_; std::vector glyphs_; bool is_valid_ = false; + bool has_color_ = false; }; } // namespace impeller diff --git a/engine/src/flutter/impeller/typographer/typographer_unittests.cc b/engine/src/flutter/impeller/typographer/typographer_unittests.cc index d0a0e318b76..42af6aea9a7 100644 --- a/engine/src/flutter/impeller/typographer/typographer_unittests.cc +++ b/engine/src/flutter/impeller/typographer/typographer_unittests.cc @@ -38,9 +38,9 @@ TEST_P(TypographerTest, CanCreateGlyphAtlas) { SkFont sk_font; auto blob = SkTextBlob::MakeFromString("hello", sk_font); ASSERT_TRUE(blob); - auto atlas = context->CreateGlyphAtlas(TextFrameFromTextBlob(blob)); + auto atlas = context->CreateGlyphAtlas(GlyphAtlas::Type::kAlphaBitmap, + TextFrameFromTextBlob(blob)); ASSERT_NE(atlas, nullptr); - ASSERT_FALSE(atlas->ContainsColorGlyph()); OpenPlaygroundHere([](RenderTarget&) { return true; }); }