[Impeller] backfilling TextContents unit tests (#161625)

issue: https://github.com/flutter/flutter/issues/149652

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
gaaclarke 2025-01-21 12:15:29 -08:00 committed by GitHub
parent bdecbaec9d
commit 52cfc8b073
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 312 additions and 121 deletions

View File

@ -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

View File

@ -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",
]
}

View File

@ -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<TextFrame>& frame,
Scalar scale,
const Matrix& entity_transform,
Vector2 offset,
std::optional<GlyphProperties> glyph_properties,
const std::shared_ptr<GlyphAtlas>& 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<Point, 6> 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<FrameBounds> 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<Point, 6> 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<VS::PerVertexData*>(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<FrameBounds> 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));

View File

@ -7,6 +7,7 @@
#include <memory>
#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<TextFrame>& frame,
Scalar scale,
const Matrix& entity_transform,
Vector2 offset,
std::optional<GlyphProperties> glyph_properties,
const std::shared_ptr<GlyphAtlas>& atlas);
private:
std::optional<GlyphProperties> GetGlyphProperties() const;

View File

@ -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<TextFrame> 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<SkFontMgr> 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<GlyphAtlas> CreateGlyphAtlas(
Context& context,
const TypographerContext* typographer_context,
HostBuffer& host_buffer,
GlyphAtlas::Type type,
Scalar scale,
const std::shared_ptr<GlyphAtlasContext>& atlas_context,
const std::shared_ptr<TextFrame>& 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<TextFrame> text_frame =
MakeTextFrame("1", "ahem.ttf", /*font_size=*/50);
std::shared_ptr<TypographerContext> context = TypographerContextSkia::Make();
std::shared_ptr<GlyphAtlasContext> atlas_context =
context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
std::shared_ptr<HostBuffer> host_buffer = HostBuffer::Create(
GetContext()->GetResourceAllocator(), GetContext()->GetIdleWaiter());
ASSERT_TRUE(context && context->IsValid());
std::shared_ptr<GlyphAtlas> 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<TextFrame> text_frame =
MakeTextFrame("1", "ahem.ttf", /*font_size=*/50);
std::shared_ptr<TypographerContext> context = TypographerContextSkia::Make();
std::shared_ptr<GlyphAtlasContext> atlas_context =
context->CreateGlyphAtlasContext(GlyphAtlas::Type::kAlphaBitmap);
std::shared_ptr<HostBuffer> host_buffer = HostBuffer::Create(
GetContext()->GetResourceAllocator(), GetContext()->GetIdleWaiter());
ASSERT_TRUE(context && context->IsValid());
Scalar font_scale = 2.f;
std::shared_ptr<GlyphAtlas> 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

View File

@ -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",

View File

@ -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];
}