blob: 5c46309c60aabf98af2c0160fad2607c3357b2d2 [file]
// 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_contents.h"
#include <cstring>
#include <optional>
#include <utility>
#include "impeller/core/buffer_view.h"
#include "impeller/core/formats.h"
#include "impeller/core/sampler_descriptor.h"
#include "impeller/entity/entity.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/point.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/typographer/glyph_atlas.h"
namespace impeller {
namespace {
// On Linux we use FreeType and on Windows we use DirectWrite/GDI to rasterize
// glyphs. Because we render in linear space, light text on a dark background
// would look too thin without correction. To compensate, we calculate a
// contrast/gamma correction factor based on the text color's luminance, which
// is used in the shader to adjust the glyph's coverage.
constexpr bool kPlatformGammaCorrectionDefault =
#if FML_OS_LINUX || FML_OS_WIN
true;
#else
false;
#endif
// The contrast/gamma exponent applied in the shader ranges from 1.0 for black
// text to 1.0 + kMaxGammaCorrection for white text. This interpolates the
// exponent based on the text color's luminance. On Linux, 1.2 equates to a
// maximum 2.2 sRGB gamma. On Windows, DirectWrite and GDI employ higher base
// gamma and contrast enhancement, so 1.6 is used to match Skia's perceived
// visual weight and edge sharpness.
constexpr Scalar kMaxGammaCorrection =
#if FML_OS_WIN
1.6f;
#else
1.2f;
#endif
Point SizeToPoint(Size size) {
return Point(size.width, size.height);
}
} // namespace
using VS = GlyphAtlasPipeline::VertexShader;
using FS = GlyphAtlasPipeline::FragmentShader;
TextContents::TextContents() {}
TextContents::~TextContents() = default;
void TextContents::SetTextFrame(const std::shared_ptr<TextFrame>& frame) {
frame_ = frame;
}
void TextContents::SetColor(Color color) {
color_ = color;
}
Color TextContents::GetColor() const {
return color_.WithAlpha(color_.alpha * inherited_opacity_);
}
void TextContents::SetInheritedOpacity(Scalar opacity) {
inherited_opacity_ = opacity;
}
void TextContents::SetPosition(Point position) {
position_ = position;
}
void TextContents::SetScreenTransform(const Matrix& transform) {
screen_transform_ = transform;
}
void TextContents::SetForceTextColor(bool value) {
force_text_color_ = value;
}
std::optional<Rect> TextContents::GetCoverage(const Entity& entity) const {
return frame_->GetBounds().TransformBounds(entity.GetTransform());
}
void TextContents::SetTextProperties(
Color color,
const std::optional<StrokeParameters>& stroke) {
if (frame_->HasColor()) {
// Alpha is always applied when rendering, remove it here so
// we do not double-apply the alpha.
properties_.tone_or_color = color.WithAlpha(1.0);
} else {
properties_.tone_or_color = GlyphProperties::ComputeTone(color);
}
properties_.stroke = stroke;
}
namespace {
Scalar AttractToOne(Scalar x) {
// Epsilon was decided by looking at the floating point inaccuracies in
// the ScaledK test.
const Scalar epsilon = 0.005f;
if (std::abs(x - 1.f) < epsilon) {
return 1.f;
}
if (std::abs(x + 1.f) < epsilon) {
return -1.f;
}
return x;
}
} // namespace
void TextContents::ComputeVertexData(VS::PerVertexData* vtx_contents,
const Matrix& entity_offset_transform,
const std::shared_ptr<TextFrame>& frame,
Point position,
const Matrix& screen_transform,
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, 4> unit_points = {Point{0, 0}, Point{1, 0},
Point{0, 1}, Point{1, 1}};
ISize atlas_size = atlas->GetTexture()->GetSize();
bool is_translation_scale = entity_offset_transform.IsTranslationScaleOnly();
Matrix basis_transform = entity_offset_transform.Basis();
VS::PerVertexData vtx;
size_t i = 0u;
const Matrix frame_transform =
screen_transform * Matrix::MakeTranslation(position);
Rational rounded_scale =
TextFrame::RoundScaledFontSize(frame_transform.GetMaxBasisLengthXY());
Scalar inverted_rounded_scale = static_cast<Scalar>(rounded_scale.Invert());
Matrix unscaled_basis =
basis_transform *
Matrix::MakeScale({inverted_rounded_scale, inverted_rounded_scale, 1});
// In typical scales < 48x these values should be -1 or 1. We round to
// those to avoid inaccuracies.
unscaled_basis.m[0] = AttractToOne(unscaled_basis.m[0]);
unscaled_basis.m[5] = AttractToOne(unscaled_basis.m[5]);
// Compute the device origin of the entire frame.
Point screen_offset = (entity_offset_transform * Point(0, 0));
for (const TextRun& run : frame->GetRuns()) {
const Font& font = run.GetFont();
const ScaledFont scaled_font{.font = font, .scale = rounded_scale};
const FontGlyphAtlas* font_atlas = atlas->GetFontGlyphAtlas(scaled_font);
if (!font_atlas) {
VALIDATION_LOG << "Could not find font in the atlas.";
// We will not find glyph bounds data for any characters in this run.
break;
}
// Adjust glyph position based on the subpixel rounding used by the font.
//
// This value is really only used in the is_translation_scale case below,
// but that usage appears inside a pair of nested loops so we compute it
// once here for the common case for use many times below.
// For the other case, this is a fairly quick computation if we are
// only doing it just once.
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;
}
for (const TextRun::GlyphPosition& glyph_position :
run.GetGlyphPositions()) {
SubpixelPosition subpixel = TextFrame::ComputeSubpixelPosition(
glyph_position, font.GetAxisAlignment(), frame_transform);
SubpixelGlyph subpixel_glyph(glyph_position.glyph, subpixel,
glyph_properties);
FrameBounds frame_bounds =
font_atlas->FindGlyphBounds(subpixel_glyph).value_or(FrameBounds{});
// If frame_bounds.is_placeholder is true, either this set of attributes
// were not captured by the FirstPass dispatcher or 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) {
VALIDATION_LOG << "Frame bounds are not present in the atlas "
<< font_atlas;
continue;
}
// 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 = frame_bounds.atlas_bounds.GetLeftTop() / atlas_size;
Point uv_size =
SizeToPoint(frame_bounds.atlas_bounds.GetSize()) / atlas_size;
for (const Point& point : unit_points) {
Point position;
if (is_translation_scale) {
Point unrounded_glyph_position =
// This is for RTL text.
unscaled_basis * frame_bounds.glyph_bounds.GetLeftTop() +
(basis_transform * glyph_position.position);
Point screen_glyph_position =
(screen_offset + unrounded_glyph_position + subpixel_adjustment)
.Floor();
position =
(screen_glyph_position +
(unscaled_basis * point * frame_bounds.glyph_bounds.GetSize()))
.Round();
} else {
Rect scaled_bounds =
frame_bounds.glyph_bounds.Scale(inverted_rounded_scale);
position = entity_offset_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 {
Color color = GetColor();
if (color.IsTransparent()) {
return true;
}
GlyphAtlas::Type type = frame_->GetAtlasType();
const std::shared_ptr<GlyphAtlas>& atlas =
renderer.GetLazyGlyphAtlas()->CreateOrGetGlyphAtlas(
*renderer.GetContext(), renderer.GetTransientsDataBuffer(), type);
if (!atlas || !atlas->IsValid()) {
VALIDATION_LOG << "Cannot render glyphs without prepared atlas.";
return false;
}
// Information shared by all glyph draw calls.
pass.SetCommandLabel("TextFrame");
auto opts = OptionsFromPassAndEntity(pass, entity);
opts.primitive_type = PrimitiveType::kTriangle;
pass.SetPipeline(renderer.GetGlyphAtlasPipeline(opts));
// Common vertex uniforms for all glyphs.
VS::FrameInfo frame_info;
frame_info.mvp =
Entity::GetShaderTransform(entity.GetShaderClipDepth(), pass, Matrix());
const Matrix& entity_transform = entity.GetTransform();
bool is_translation_scale = entity_transform.IsTranslationScaleOnly();
VS::BindFrameInfo(
pass, renderer.GetTransientsDataBuffer().EmplaceUniform(frame_info));
FS::FragInfo frag_info;
frag_info.use_text_color = force_text_color_ ? 1.0 : 0.0;
frag_info.text_color = ToVector(color.Premultiply());
frag_info.is_color_glyph = type == GlyphAtlas::Type::kColorBitmap;
bool enable_gamma_correction = frame_->GetEnableGammaCorrection().value_or(
kPlatformGammaCorrectionDefault);
if (enable_gamma_correction) {
// Calculate relative luminance using Rec. 709 luma coefficients.
Scalar luma =
color.red * 0.2126f + color.green * 0.7152f + color.blue * 0.0722f;
frag_info.text_contrast = 1.0f + luma * kMaxGammaCorrection;
} else {
frag_info.text_contrast = 1.0f;
}
FS::BindFragInfo(
pass, renderer.GetTransientsDataBuffer().EmplaceUniform(frag_info));
SamplerDescriptor sampler_desc;
if (is_translation_scale) {
// When the transform is translation+scale only, we normally use nearest-
// neighbor sampling for pixel-perfect text. However, if the X and Y
// scales differ significantly (non-uniform / anisotropic scaling, e.g.
// Transform.scale(scaleY: 2)), the glyph atlas entry is rasterized at
// max(|scaleX|,|scaleY|) uniformly and the compensating unscaled_basis
// squeezes one axis, causing a minification. Nearest-neighbor during
// minification discards texel columns/rows, producing jagged diagonals
// and varying stroke weights. Fall back to bilinear in that case.
// See https://github.com/flutter/flutter/issues/182143
constexpr Scalar kMinScaleForRatio = 0.001f;
constexpr Scalar kAnisotropicScaleThreshold = 1.15f;
const Scalar sx = entity_transform.GetBasisX().GetLength();
const Scalar sy = entity_transform.GetBasisY().GetLength();
const Scalar ratio = (sx > sy) ? sx / std::max(sy, kMinScaleForRatio)
: sy / std::max(sx, kMinScaleForRatio);
if (ratio > kAnisotropicScaleThreshold) {
// Non-uniform scale — use bilinear to avoid aliasing.
sampler_desc.min_filter = MinMagFilter::kLinear;
sampler_desc.mag_filter = MinMagFilter::kLinear;
} else {
sampler_desc.min_filter = MinMagFilter::kNearest;
sampler_desc.mag_filter = MinMagFilter::kNearest;
}
} else {
// Currently, we only propagate the scale of the transform to the atlas
// renderer, so if the transform has more than just a translation, we turn
// on linear sampling to prevent crunchiness caused by the pixel grid not
// being perfectly aligned.
// The downside is that this slightly over-blurs rotated/skewed text.
sampler_desc.min_filter = MinMagFilter::kLinear;
sampler_desc.mag_filter = MinMagFilter::kLinear;
}
// No mipmaps for glyph atlas (glyphs are generated at exact scales).
sampler_desc.mip_filter = MipFilter::kBase;
FS::BindGlyphAtlasSampler(
pass, // command
atlas->GetTexture(), // texture
renderer.GetContext()->GetSamplerLibrary()->GetSampler(
sampler_desc) // sampler
);
HostBuffer& data_host_buffer = renderer.GetTransientsDataBuffer();
HostBuffer& indexes_host_buffer = renderer.GetTransientsIndexesBuffer();
size_t glyph_count = 0;
for (const auto& run : frame_->GetRuns()) {
glyph_count += run.GetGlyphPositions().size();
}
size_t vertex_count = glyph_count * 4;
size_t index_count = glyph_count * 6;
BufferView buffer_view = data_host_buffer.Emplace(
vertex_count * sizeof(VS::PerVertexData), alignof(VS::PerVertexData),
[&](uint8_t* data) {
VS::PerVertexData* vtx_contents =
reinterpret_cast<VS::PerVertexData*>(data);
ComputeVertexData(/*vtx_contents=*/vtx_contents,
/*entity_transform=*/entity.GetTransform(),
/*frame=*/frame_,
/*position=*/position_,
/*screen_transform=*/screen_transform_,
/*glyph_properties=*/properties_,
/*atlas=*/atlas);
});
BufferView index_buffer_view = indexes_host_buffer.Emplace(
index_count * sizeof(uint16_t), alignof(uint16_t), [&](uint8_t* data) {
uint16_t* indices = reinterpret_cast<uint16_t*>(data);
size_t j = 0;
for (auto i = 0u; i < glyph_count; i++) {
size_t base = i * 4;
indices[j++] = base + 0;
indices[j++] = base + 1;
indices[j++] = base + 2;
indices[j++] = base + 1;
indices[j++] = base + 2;
indices[j++] = base + 3;
}
});
pass.SetVertexBuffer(std::move(buffer_view));
pass.SetIndexBuffer(index_buffer_view, IndexType::k16bit);
pass.SetElementCount(index_count);
return pass.Draw().ok();
}
} // namespace impeller