diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 417abf50b2..1b9049d599 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -160,6 +160,7 @@ ../../../flutter/impeller/entity/contents/filters/matrix_filter_contents_unittests.cc ../../../flutter/impeller/entity/contents/host_buffer_unittests.cc ../../../flutter/impeller/entity/contents/test +../../../flutter/impeller/entity/contents/text_contents_unittests.cc ../../../flutter/impeller/entity/contents/tiled_texture_contents_unittests.cc ../../../flutter/impeller/entity/draw_order_resolver_unittests.cc ../../../flutter/impeller/entity/entity_pass_target_unittests.cc diff --git a/engine/src/flutter/impeller/entity/BUILD.gn b/engine/src/flutter/impeller/entity/BUILD.gn index 735081d4f8..0f256bb678 100644 --- a/engine/src/flutter/impeller/entity/BUILD.gn +++ b/engine/src/flutter/impeller/entity/BUILD.gn @@ -248,6 +248,7 @@ impeller_component("entity_unittests") { "contents/filters/inputs/filter_input_unittests.cc", "contents/filters/matrix_filter_contents_unittests.cc", "contents/host_buffer_unittests.cc", + "contents/text_contents_unittests.cc", "contents/tiled_texture_contents_unittests.cc", "draw_order_resolver_unittests.cc", "entity_pass_target_unittests.cc", @@ -266,5 +267,6 @@ impeller_component("entity_unittests") { "../playground:playground_test", "//flutter/display_list/testing:display_list_testing", "//flutter/impeller/typographer/backends/skia:typographer_skia_backend", + "//flutter/third_party/txt", ] } diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.cc b/engine/src/flutter/impeller/entity/contents/text_contents.cc index 4d4a130480..1835c5b435 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.cc +++ b/engine/src/flutter/impeller/entity/contents/text_contents.cc @@ -11,7 +11,6 @@ #include "impeller/core/buffer_view.h" #include "impeller/core/formats.h" #include "impeller/core/sampler_descriptor.h" -#include "impeller/entity/contents/content_context.h" #include "impeller/entity/entity.h" #include "impeller/geometry/color.h" #include "impeller/geometry/point.h" @@ -20,6 +19,9 @@ namespace impeller { +using VS = GlyphAtlasPipeline::VertexShader; +using FS = GlyphAtlasPipeline::FragmentShader; + TextContents::TextContents() = default; TextContents::~TextContents() = default; @@ -72,6 +74,130 @@ void TextContents::SetTextProperties(Color color, } } +void TextContents::ComputeVertexData( + VS::PerVertexData* vtx_contents, + const std::shared_ptr& frame, + Scalar scale, + const Matrix& entity_transform, + Vector2 offset, + std::optional glyph_properties, + const std::shared_ptr& atlas) { + // Common vertex information for all glyphs. + // All glyphs are given the same vertex information in the form of a + // unit-sized quad. The size of the glyph is specified in per instance data + // and the vertex shader uses this to size the glyph correctly. The + // interpolated vertex information is also used in the fragment shader to + // sample from the glyph atlas. + + constexpr std::array unit_points = {Point{0, 0}, Point{1, 0}, + Point{0, 1}, Point{1, 0}, + Point{0, 1}, Point{1, 1}}; + + ISize atlas_size = atlas->GetTexture()->GetSize(); + bool is_translation_scale = entity_transform.IsTranslationScaleOnly(); + Matrix basis_transform = entity_transform.Basis(); + + VS::PerVertexData vtx; + size_t i = 0u; + size_t bounds_offset = 0u; + for (const TextRun& run : frame->GetRuns()) { + const Font& font = run.GetFont(); + Scalar rounded_scale = TextFrame::RoundScaledFontSize(scale); + FontGlyphAtlas* font_atlas = nullptr; + + // Adjust glyph position based on the subpixel rounding + // used by the font. + Point subpixel_adjustment(0.5, 0.5); + switch (font.GetAxisAlignment()) { + case AxisAlignment::kNone: + break; + case AxisAlignment::kX: + subpixel_adjustment.x = 0.125; + break; + case AxisAlignment::kY: + subpixel_adjustment.y = 0.125; + break; + case AxisAlignment::kAll: + subpixel_adjustment.x = 0.125; + subpixel_adjustment.y = 0.125; + break; + } + + Point screen_offset = (entity_transform * Point(0, 0)); + for (const TextRun::GlyphPosition& glyph_position : + run.GetGlyphPositions()) { + const FrameBounds& frame_bounds = frame->GetFrameBounds(bounds_offset); + bounds_offset++; + auto atlas_glyph_bounds = frame_bounds.atlas_bounds; + auto glyph_bounds = frame_bounds.glyph_bounds; + + // If frame_bounds.is_placeholder is true, this is the first frame + // the glyph has been rendered and so its atlas position was not + // known when the glyph was recorded. Perform a slow lookup into the + // glyph atlas hash table. + if (frame_bounds.is_placeholder) { + if (!font_atlas) { + font_atlas = + atlas->GetOrCreateFontGlyphAtlas(ScaledFont{font, rounded_scale}); + } + + if (!font_atlas) { + VALIDATION_LOG << "Could not find font in the atlas."; + continue; + } + Point subpixel = TextFrame::ComputeSubpixelPosition( + glyph_position, font.GetAxisAlignment(), offset, rounded_scale); + + std::optional maybe_atlas_glyph_bounds = + font_atlas->FindGlyphBounds(SubpixelGlyph{ + glyph_position.glyph, // + subpixel, // + glyph_properties // + }); + if (!maybe_atlas_glyph_bounds.has_value()) { + VALIDATION_LOG << "Could not find glyph position in the atlas."; + continue; + } + atlas_glyph_bounds = maybe_atlas_glyph_bounds.value().atlas_bounds; + } + + Rect scaled_bounds = glyph_bounds.Scale(1.0 / rounded_scale); + // For each glyph, we compute two rectangles. One for the vertex + // positions and one for the texture coordinates (UVs). The atlas + // glyph bounds are used to compute UVs in cases where the + // destination and source sizes may differ due to clamping the sizes + // of large glyphs. + Point uv_origin = + (atlas_glyph_bounds.GetLeftTop() - Point(0.5, 0.5)) / atlas_size; + Point uv_size = (atlas_glyph_bounds.GetSize() + Point(1, 1)) / atlas_size; + + Point unrounded_glyph_position = + basis_transform * + (glyph_position.position + scaled_bounds.GetLeftTop()); + + Point screen_glyph_position = + (screen_offset + unrounded_glyph_position + subpixel_adjustment) + .Floor(); + + for (const Point& point : unit_points) { + Point position; + if (is_translation_scale) { + position = (screen_glyph_position + + (basis_transform * point * scaled_bounds.GetSize())) + .Round(); + } else { + position = entity_transform * + (glyph_position.position + scaled_bounds.GetLeftTop() + + point * scaled_bounds.GetSize()); + } + vtx.uv = uv_origin + (uv_size * point); + vtx.position = position; + vtx_contents[i++] = vtx; + } + } + } +} + bool TextContents::Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { @@ -100,17 +226,12 @@ bool TextContents::Render(const ContentContext& renderer, opts.primitive_type = PrimitiveType::kTriangle; pass.SetPipeline(renderer.GetGlyphAtlasPipeline(opts)); - using VS = GlyphAtlasPipeline::VertexShader; - using FS = GlyphAtlasPipeline::FragmentShader; - // Common vertex uniforms for all glyphs. VS::FrameInfo frame_info; frame_info.mvp = Entity::GetShaderTransform(entity.GetShaderClipDepth(), pass, Matrix()); - ISize atlas_size = atlas->GetTexture()->GetSize(); bool is_translation_scale = entity.GetTransform().IsTranslationScaleOnly(); Matrix entity_transform = entity.GetTransform(); - Matrix basis_transform = entity_transform.Basis(); VS::BindFrameInfo(pass, renderer.GetTransientsBuffer().EmplaceUniform(frame_info)); @@ -147,17 +268,6 @@ bool TextContents::Render(const ContentContext& renderer, sampler_desc) // sampler ); - // Common vertex information for all glyphs. - // All glyphs are given the same vertex information in the form of a - // unit-sized quad. The size of the glyph is specified in per instance data - // and the vertex shader uses this to size the glyph correctly. The - // interpolated vertex information is also used in the fragment shader to - // sample from the glyph atlas. - - constexpr std::array unit_points = {Point{0, 0}, Point{1, 0}, - Point{0, 1}, Point{1, 0}, - Point{0, 1}, Point{1, 1}}; - auto& host_buffer = renderer.GetTransientsBuffer(); size_t vertex_count = 0; for (const auto& run : frame_->GetRuns()) { @@ -168,112 +278,11 @@ bool TextContents::Render(const ContentContext& renderer, BufferView buffer_view = host_buffer.Emplace( vertex_count * sizeof(VS::PerVertexData), alignof(VS::PerVertexData), [&](uint8_t* contents) { - VS::PerVertexData vtx; VS::PerVertexData* vtx_contents = reinterpret_cast(contents); - size_t i = 0u; - size_t bounds_offset = 0u; - for (const TextRun& run : frame_->GetRuns()) { - const Font& font = run.GetFont(); - Scalar rounded_scale = TextFrame::RoundScaledFontSize(scale_); - FontGlyphAtlas* font_atlas = nullptr; - - // Adjust glyph position based on the subpixel rounding - // used by the font. - Point subpixel_adjustment(0.5, 0.5); - switch (font.GetAxisAlignment()) { - case AxisAlignment::kNone: - break; - case AxisAlignment::kX: - subpixel_adjustment.x = 0.125; - break; - case AxisAlignment::kY: - subpixel_adjustment.y = 0.125; - break; - case AxisAlignment::kAll: - subpixel_adjustment.x = 0.125; - subpixel_adjustment.y = 0.125; - break; - } - - Point screen_offset = (entity_transform * Point(0, 0)); - for (const TextRun::GlyphPosition& glyph_position : - run.GetGlyphPositions()) { - const FrameBounds& frame_bounds = - frame_->GetFrameBounds(bounds_offset); - bounds_offset++; - auto atlas_glyph_bounds = frame_bounds.atlas_bounds; - auto glyph_bounds = frame_bounds.glyph_bounds; - - // If frame_bounds.is_placeholder is true, this is the first frame - // the glyph has been rendered and so its atlas position was not - // known when the glyph was recorded. Perform a slow lookup into the - // glyph atlas hash table. - if (frame_bounds.is_placeholder) { - if (!font_atlas) { - font_atlas = atlas->GetOrCreateFontGlyphAtlas( - ScaledFont{font, rounded_scale}); - } - - if (!font_atlas) { - VALIDATION_LOG << "Could not find font in the atlas."; - continue; - } - Point subpixel = TextFrame::ComputeSubpixelPosition( - glyph_position, font.GetAxisAlignment(), offset_, - rounded_scale); - - std::optional maybe_atlas_glyph_bounds = - font_atlas->FindGlyphBounds(SubpixelGlyph{ - glyph_position.glyph, // - subpixel, // - GetGlyphProperties() // - }); - if (!maybe_atlas_glyph_bounds.has_value()) { - VALIDATION_LOG << "Could not find glyph position in the atlas."; - continue; - } - atlas_glyph_bounds = - maybe_atlas_glyph_bounds.value().atlas_bounds; - } - - Rect scaled_bounds = glyph_bounds.Scale(1.0 / rounded_scale); - // For each glyph, we compute two rectangles. One for the vertex - // positions and one for the texture coordinates (UVs). The atlas - // glyph bounds are used to compute UVs in cases where the - // destination and source sizes may differ due to clamping the sizes - // of large glyphs. - Point uv_origin = - (atlas_glyph_bounds.GetLeftTop() - Point(0.5, 0.5)) / - atlas_size; - Point uv_size = - (atlas_glyph_bounds.GetSize() + Point(1, 1)) / atlas_size; - - Point unrounded_glyph_position = - basis_transform * - (glyph_position.position + scaled_bounds.GetLeftTop()); - - Point screen_glyph_position = - (screen_offset + unrounded_glyph_position + subpixel_adjustment) - .Floor(); - - for (const Point& point : unit_points) { - Point position; - if (is_translation_scale) { - position = (screen_glyph_position + - (basis_transform * point * scaled_bounds.GetSize())) - .Round(); - } else { - position = entity_transform * (glyph_position.position + - scaled_bounds.GetLeftTop() + - point * scaled_bounds.GetSize()); - } - vtx.uv = uv_origin + (uv_size * point); - vtx.position = position; - vtx_contents[i++] = vtx; - } - } - } + ComputeVertexData(vtx_contents, frame_, scale_, + /*entity_transform=*/entity_transform, offset_, + GetGlyphProperties(), atlas); }); pass.SetVertexBuffer(std::move(buffer_view)); diff --git a/engine/src/flutter/impeller/entity/contents/text_contents.h b/engine/src/flutter/impeller/entity/contents/text_contents.h index 091da3d748..6e5861f371 100644 --- a/engine/src/flutter/impeller/entity/contents/text_contents.h +++ b/engine/src/flutter/impeller/entity/contents/text_contents.h @@ -7,6 +7,7 @@ #include +#include "impeller/entity/contents/content_context.h" #include "impeller/entity/contents/contents.h" #include "impeller/geometry/color.h" #include "impeller/typographer/font_glyph_pair.h" @@ -61,6 +62,15 @@ class TextContents final : public Contents { const Entity& entity, RenderPass& pass) const override; + static void ComputeVertexData( + GlyphAtlasPipeline::VertexShader::PerVertexData* vtx_contents, + const std::shared_ptr& frame, + Scalar scale, + const Matrix& entity_transform, + Vector2 offset, + std::optional glyph_properties, + const std::shared_ptr& atlas); + private: std::optional GetGlyphProperties() const; diff --git a/engine/src/flutter/impeller/entity/contents/text_contents_unittests.cc b/engine/src/flutter/impeller/entity/contents/text_contents_unittests.cc new file mode 100644 index 0000000000..ae54ef945f --- /dev/null +++ b/engine/src/flutter/impeller/entity/contents/text_contents_unittests.cc @@ -0,0 +1,167 @@ +// 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 "flutter/impeller/geometry/geometry_asserts.h" +#include "flutter/impeller/renderer/testing/mocks.h" +#include "flutter/testing/testing.h" +#include "impeller/entity/contents/text_contents.h" +#include "impeller/playground/playground_test.h" +#include "impeller/typographer/backends/skia/text_frame_skia.h" +#include "impeller/typographer/backends/skia/typographer_context_skia.h" +#include "third_party/googletest/googletest/include/gtest/gtest.h" +#include "txt/platform.h" + +#pragma GCC diagnostic ignored "-Wunreachable-code" + +namespace impeller { +namespace testing { + +using TextContentsTest = PlaygroundTest; +INSTANTIATE_PLAYGROUND_SUITE(TextContentsTest); + +using ::testing::Return; + +namespace { +std::shared_ptr MakeTextFrame(const std::string& text, + const std::string_view& font_fixture, + Scalar font_size) { + auto c_font_fixture = std::string(font_fixture); + auto mapping = flutter::testing::OpenFixtureAsSkData(c_font_fixture.c_str()); + if (!mapping) { + return nullptr; + } + sk_sp font_mgr = txt::GetDefaultFontManager(); + SkFont sk_font(font_mgr->makeFromData(mapping), font_size); + auto blob = SkTextBlob::MakeFromString(text.c_str(), sk_font); + if (!blob) { + return nullptr; + } + + return MakeTextFrameFromTextBlobSkia(blob); +} + +std::shared_ptr CreateGlyphAtlas( + Context& context, + const TypographerContext* typographer_context, + HostBuffer& host_buffer, + GlyphAtlas::Type type, + Scalar scale, + const std::shared_ptr& atlas_context, + const std::shared_ptr& frame) { + frame->SetPerFrameData(scale, /*offset=*/{0, 0}, + /*properties=*/std::nullopt); + return typographer_context->CreateGlyphAtlas(context, type, host_buffer, + atlas_context, {frame}); +} + +Rect PerVertexDataPositionToRect( + GlyphAtlasPipeline::VertexShader::PerVertexData data[6]) { + Scalar right = FLT_MIN; + Scalar left = FLT_MAX; + Scalar top = FLT_MAX; + Scalar bottom = FLT_MIN; + for (int i = 0; i < 6; ++i) { + right = std::max(right, data[i].position.x); + left = std::min(left, data[i].position.x); + top = std::min(top, data[i].position.y); + bottom = std::max(bottom, data[i].position.y); + } + + return Rect::MakeLTRB(left, top, right, bottom); +} + +Rect PerVertexDataUVToRect( + GlyphAtlasPipeline::VertexShader::PerVertexData data[6], + ISize texture_size) { + Scalar right = FLT_MIN; + Scalar left = FLT_MAX; + Scalar top = FLT_MAX; + Scalar bottom = FLT_MIN; + for (int i = 0; i < 6; ++i) { + right = std::max(right, data[i].uv.x * texture_size.width); + left = std::min(left, data[i].uv.x * texture_size.width); + top = std::min(top, data[i].uv.y * texture_size.height); + bottom = std::max(bottom, data[i].uv.y * texture_size.height); + } + + return Rect::MakeLTRB(left, top, right, bottom); +} + +} // namespace + +TEST_P(TextContentsTest, SimpleComputeVertexData) { +#ifndef FML_OS_MACOSX + GTEST_SKIP() << "Results aren't stable across linux and macos."; +#endif + + GlyphAtlasPipeline::VertexShader::PerVertexData data[6]; + + std::shared_ptr text_frame = + MakeTextFrame("1", "ahem.ttf", /*font_size=*/50); + + std::shared_ptr context = TypographerContextSkia::Make(); + std::shared_ptr atlas_context = + context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap); + std::shared_ptr host_buffer = HostBuffer::Create( + GetContext()->GetResourceAllocator(), GetContext()->GetIdleWaiter()); + ASSERT_TRUE(context && context->IsValid()); + std::shared_ptr atlas = + CreateGlyphAtlas(*GetContext(), context.get(), *host_buffer, + GlyphAtlas::Type::kAlphaBitmap, /*scale=*/1.0f, + atlas_context, text_frame); + + ISize texture_size = atlas->GetTexture()->GetSize(); + TextContents::ComputeVertexData(data, text_frame, /*scale=*/1.0, + /*entity_transform=*/Matrix(), + /*offset=*/Vector2(0, 0), + /*glyph_properties=*/std::nullopt, atlas); + + Rect position_rect = PerVertexDataPositionToRect(data); + Rect uv_rect = PerVertexDataUVToRect(data, texture_size); + // The -1 offset comes from Skia in `ComputeGlyphSize`. So since the font size + // is 50, the math appears to be to get back a 50x50 rect and apply 1 pixel + // of padding. + EXPECT_RECT_NEAR(position_rect, Rect::MakeXYWH(-1, -41, 52, 52)); + // (0.5, 0.5) gets us sampling from the exact middle of the first pixel, the + // extra width takes us 0.5 past the end of the glyph too to sample fully the + // last pixel. + EXPECT_RECT_NEAR(uv_rect, Rect::MakeXYWH(0.5, 0.5, 53, 53)); +} + +TEST_P(TextContentsTest, SimpleComputeVertexData2x) { +#ifndef FML_OS_MACOSX + GTEST_SKIP() << "Results aren't stable across linux and macos."; +#endif + + GlyphAtlasPipeline::VertexShader::PerVertexData data[6]; + + std::shared_ptr text_frame = + MakeTextFrame("1", "ahem.ttf", /*font_size=*/50); + + std::shared_ptr context = TypographerContextSkia::Make(); + std::shared_ptr atlas_context = + context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap); + std::shared_ptr host_buffer = HostBuffer::Create( + GetContext()->GetResourceAllocator(), GetContext()->GetIdleWaiter()); + ASSERT_TRUE(context && context->IsValid()); + Scalar font_scale = 2.f; + std::shared_ptr atlas = CreateGlyphAtlas( + *GetContext(), context.get(), *host_buffer, + GlyphAtlas::Type::kAlphaBitmap, font_scale, atlas_context, text_frame); + + ISize texture_size = atlas->GetTexture()->GetSize(); + TextContents::ComputeVertexData( + data, text_frame, font_scale, + /*entity_transform=*/Matrix::MakeScale({font_scale, font_scale, 1}), + /*offset=*/Vector2(0, 0), + /*glyph_properties=*/std::nullopt, atlas); + + Rect position_rect = PerVertexDataPositionToRect(data); + Rect uv_rect = PerVertexDataUVToRect(data, texture_size); + EXPECT_RECT_NEAR(position_rect, Rect::MakeXYWH(-1, -81, 102, 102)); + EXPECT_RECT_NEAR(uv_rect, Rect::MakeXYWH(0.5, 0.5, 103, 103)); +} + +} // namespace testing +} // namespace impeller diff --git a/engine/src/flutter/impeller/fixtures/BUILD.gn b/engine/src/flutter/impeller/fixtures/BUILD.gn index 0e97078395..f3a81d1cc6 100644 --- a/engine/src/flutter/impeller/fixtures/BUILD.gn +++ b/engine/src/flutter/impeller/fixtures/BUILD.gn @@ -102,6 +102,7 @@ impellerc("runtime_stages") { test_fixtures("file_fixtures") { fixtures = [ + "//flutter/third_party/txt/third_party/fonts/ahem.ttf", "//flutter/third_party/txt/third_party/fonts/HomemadeApple.ttf", "//flutter/third_party/txt/third_party/fonts/NotoColorEmoji.ttf", "//flutter/third_party/txt/third_party/fonts/Roboto-Medium.ttf", diff --git a/engine/src/flutter/impeller/typographer/text_frame.cc b/engine/src/flutter/impeller/typographer/text_frame.cc index 870ff17821..b8395fd074 100644 --- a/engine/src/flutter/impeller/typographer/text_frame.cc +++ b/engine/src/flutter/impeller/typographer/text_frame.cc @@ -141,6 +141,7 @@ bool TextFrame::IsFrameComplete() const { } const FrameBounds& TextFrame::GetFrameBounds(size_t index) const { + FML_DCHECK(index < bound_values_.size()); return bound_values_[index]; }