1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 14:47:44 +00:00

LibGfx+LibWeb: Calculate and save glyph positions during layout

Previously, we determined the positions of glyphs for each text run at
the time of painting, which constituted a significant portion of the
painting process according to profiles. However, since we already go
through each glyph to figure out the width of each fragment during
layout, we can simultaneously gather data about the position of each
glyph in the layout phase and utilize this information in the painting
phase.

I had to update expectations for a couple of reference tests. These
updates are due to the fact that we now measure glyph positions during
layout using a 1x font, and then linearly scale each glyph's position
to device pixels during painting. This approach should be acceptable,
considering we measure a fragment's width and height with an unscaled
font during layout.
This commit is contained in:
Aliaksandr Kalenik 2023-12-02 18:38:05 +01:00 committed by Andreas Kling
parent 1a35621930
commit 681771d210
17 changed files with 65 additions and 42 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 KiB

After

Width:  |  Height:  |  Size: 685 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Before After
Before After

View file

@ -1422,7 +1422,7 @@ void Painter::draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, F
draw_glyph(glyph.position, glyph.code_point, *glyph.font, color); draw_glyph(glyph.position, glyph.code_point, *glyph.font, color);
} else { } else {
auto& emoji = draw_glyph_or_emoji.get<DrawEmoji>(); auto& emoji = draw_glyph_or_emoji.get<DrawEmoji>();
draw_emoji(emoji.position, *emoji.emoji, *emoji.font); draw_emoji(emoji.position.to_type<int>(), *emoji.emoji, *emoji.font);
} }
} }
@ -2445,7 +2445,7 @@ void Painter::draw_text_run(FloatPoint baseline_start, Utf8View const& string, F
draw_glyph(glyph.position, glyph.code_point, *glyph.font, color); draw_glyph(glyph.position, glyph.code_point, *glyph.font, color);
} else { } else {
auto& emoji = glyph_or_emoji.get<DrawEmoji>(); auto& emoji = glyph_or_emoji.get<DrawEmoji>();
draw_emoji(emoji.position, *emoji.emoji, *emoji.font); draw_emoji(emoji.position.to_type<int>(), *emoji.emoji, *emoji.font);
} }
}); });
} }

View file

@ -231,7 +231,7 @@ DrawGlyphOrEmoji prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIter
// If we didn't find a text glyph, or have an emoji variation selector or regional indicator, try to draw an emoji glyph. // If we didn't find a text glyph, or have an emoji variation selector or regional indicator, try to draw an emoji glyph.
if (auto const* emoji = Emoji::emoji_for_code_point_iterator(it)) { if (auto const* emoji = Emoji::emoji_for_code_point_iterator(it)) {
return DrawEmoji { return DrawEmoji {
.position = point.to_type<int>(), .position = point,
.emoji = emoji, .emoji = emoji,
.font = &font, .font = &font,
}; };
@ -255,15 +255,4 @@ DrawGlyphOrEmoji prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIter
}; };
} }
Vector<DrawGlyphOrEmoji> get_glyph_run(FloatPoint baseline_start, Utf8View const& string, Font const& font, IncludeLeftBearing include_left_bearing)
{
Vector<DrawGlyphOrEmoji> glyphs_or_emojis;
for_each_glyph_position(
baseline_start, string, font, [&](auto glyph_or_emoji) {
glyphs_or_emojis.append(glyph_or_emoji);
},
include_left_bearing);
return glyphs_or_emojis;
}
} }

View file

@ -82,7 +82,7 @@ struct DrawGlyph {
}; };
struct DrawEmoji { struct DrawEmoji {
IntPoint position; FloatPoint position;
Gfx::Bitmap const* emoji; Gfx::Bitmap const* emoji;
Font const* font; Font const* font;
}; };
@ -92,7 +92,7 @@ using DrawGlyphOrEmoji = Variant<DrawGlyph, DrawEmoji>;
Variant<DrawGlyph, DrawEmoji> prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, Font const& font); Variant<DrawGlyph, DrawEmoji> prepare_draw_glyph_or_emoji(FloatPoint point, Utf8CodePointIterator& it, Font const& font);
template<typename Callback> template<typename Callback>
void for_each_glyph_position(FloatPoint baseline_start, Utf8View string, Font const& font, Callback callback, IncludeLeftBearing include_left_bearing = IncludeLeftBearing::No) void for_each_glyph_position(FloatPoint baseline_start, Utf8View string, Font const& font, Callback callback, IncludeLeftBearing include_left_bearing = IncludeLeftBearing::No, Optional<float&> width = {})
{ {
float space_width = font.glyph_width(' ') + font.glyph_spacing(); float space_width = font.glyph_width(' ') + font.glyph_spacing();
@ -127,8 +127,9 @@ void for_each_glyph_position(FloatPoint baseline_start, Utf8View string, Font co
point.translate_by(glyph_width, 0); point.translate_by(glyph_width, 0);
last_code_point = code_point; last_code_point = code_point;
} }
if (width.has_value())
*width = point.x() - font.glyph_spacing();
} }
Vector<DrawGlyphOrEmoji> get_glyph_run(FloatPoint baseline_start, Utf8View const& string, Font const& font, IncludeLeftBearing include_left_bearing = IncludeLeftBearing::No);
} }

View file

@ -326,7 +326,8 @@ void InlineFormattingContext::generate_line_boxes(LayoutMode layout_mode)
item.margin_start, item.margin_start,
item.margin_end, item.margin_end,
item.width, item.width,
text_node.line_height()); text_node.line_height(),
item.glyph_run);
break; break;
} }
} }

View file

@ -187,12 +187,19 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
}; };
} }
CSSPixels chunk_width; Vector<Gfx::DrawGlyphOrEmoji> glyph_run;
float glyph_run_width = 0;
Gfx::for_each_glyph_position(
{ 0, 0 }, chunk.view, text_node.font(), [&](Gfx::DrawGlyphOrEmoji const& glyph_or_emoji) {
glyph_run.append(glyph_or_emoji);
return IterationDecision::Continue;
},
Gfx::IncludeLeftBearing::No, glyph_run_width);
if (m_text_node_context->is_last_chunk) if (!m_text_node_context->is_last_chunk)
chunk_width = CSSPixels::nearest_value_for(text_node.font().width(chunk.view)); glyph_run_width += text_node.font().glyph_spacing();
else
chunk_width = CSSPixels::nearest_value_for(text_node.font().width(chunk.view) + text_node.font().glyph_spacing()); CSSPixels chunk_width = CSSPixels::nearest_value_for(glyph_run_width);
// NOTE: We never consider `content: ""` to be collapsible whitespace. // NOTE: We never consider `content: ""` to be collapsible whitespace.
bool is_generated_empty_string = text_node.is_generated() && chunk.length == 0; bool is_generated_empty_string = text_node.is_generated() && chunk.length == 0;
@ -200,6 +207,7 @@ Optional<InlineLevelIterator::Item> InlineLevelIterator::next_without_lookahead(
Item item { Item item {
.type = Item::Type::Text, .type = Item::Type::Text,
.node = &text_node, .node = &text_node,
.glyph_run = move(glyph_run),
.offset_in_node = chunk.start, .offset_in_node = chunk.start,
.length_in_node = chunk.length, .length_in_node = chunk.length,
.width = chunk_width, .width = chunk_width,

View file

@ -32,6 +32,7 @@ public:
}; };
Type type {}; Type type {};
JS::GCPtr<Layout::Node const> node {}; JS::GCPtr<Layout::Node const> node {};
Vector<Gfx::DrawGlyphOrEmoji> glyph_run {};
size_t offset_in_node { 0 }; size_t offset_in_node { 0 };
size_t length_in_node { 0 }; size_t length_in_node { 0 };
CSSPixels width { 0.0f }; CSSPixels width { 0.0f };

View file

@ -15,18 +15,24 @@
namespace Web::Layout { namespace Web::Layout {
void LineBox::add_fragment(Node const& layout_node, int start, int length, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom) void LineBox::add_fragment(Node const& layout_node, int start, int length, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom, Span<Gfx::DrawGlyphOrEmoji const> glyph_run)
{ {
bool text_align_is_justify = layout_node.computed_values().text_align() == CSS::TextAlign::Justify; bool text_align_is_justify = layout_node.computed_values().text_align() == CSS::TextAlign::Justify;
if (!text_align_is_justify && !m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node) { if (!text_align_is_justify && !m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node) {
auto const fragment_width = m_fragments.last().width();
// The fragment we're adding is from the last Layout::Node on the line. // The fragment we're adding is from the last Layout::Node on the line.
// Expand the last fragment instead of adding a new one with the same Layout::Node. // Expand the last fragment instead of adding a new one with the same Layout::Node.
m_fragments.last().m_length = (start - m_fragments.last().m_start) + length; m_fragments.last().m_length = (start - m_fragments.last().m_start) + length;
m_fragments.last().set_width(m_fragments.last().width() + content_width); m_fragments.last().set_width(m_fragments.last().width() + content_width);
for (auto glyph : glyph_run) {
glyph.visit([&](auto& glyph) { glyph.position.translate_by(fragment_width.to_float(), 0); });
m_fragments.last().m_glyph_run.append(glyph);
}
} else { } else {
Vector<Gfx::DrawGlyphOrEmoji> glyph_run_copy { glyph_run };
CSSPixels x_offset = leading_margin + leading_size + m_width; CSSPixels x_offset = leading_margin + leading_size + m_width;
CSSPixels y_offset = 0; CSSPixels y_offset = 0;
m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, border_box_bottom }); m_fragments.append(LineBoxFragment { layout_node, start, length, CSSPixelPoint(x_offset, y_offset), CSSPixelSize(content_width, content_height), border_box_top, border_box_bottom, move(glyph_run_copy) });
} }
m_width += leading_margin + leading_size + content_width + trailing_size + trailing_margin; m_width += leading_margin + leading_size + content_width + trailing_size + trailing_margin;
m_height = max(m_height, content_height + border_box_top + border_box_bottom); m_height = max(m_height, content_height + border_box_top + border_box_bottom);

View file

@ -20,7 +20,7 @@ public:
CSSPixels bottom() const { return m_bottom; } CSSPixels bottom() const { return m_bottom; }
CSSPixels baseline() const { return m_baseline; } CSSPixels baseline() const { return m_baseline; }
void add_fragment(Node const& layout_node, int start, int length, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom); void add_fragment(Node const& layout_node, int start, int length, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, CSSPixels border_box_top, CSSPixels border_box_bottom, Span<Gfx::DrawGlyphOrEmoji const> = {});
Vector<LineBoxFragment> const& fragments() const { return m_fragments; } Vector<LineBoxFragment> const& fragments() const { return m_fragments; }
Vector<LineBoxFragment>& fragments() { return m_fragments; } Vector<LineBoxFragment>& fragments() { return m_fragments; }

View file

@ -7,6 +7,7 @@
#pragma once #pragma once
#include <LibGfx/Rect.h> #include <LibGfx/Rect.h>
#include <LibGfx/TextLayout.h>
#include <LibJS/Heap/GCPtr.h> #include <LibJS/Heap/GCPtr.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWeb/PixelUnits.h> #include <LibWeb/PixelUnits.h>
@ -17,7 +18,7 @@ class LineBoxFragment {
friend class LineBox; friend class LineBox;
public: public:
LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSSPixels border_box_bottom) LineBoxFragment(Node const& layout_node, int start, int length, CSSPixelPoint offset, CSSPixelSize size, CSSPixels border_box_top, CSSPixels border_box_bottom, Vector<Gfx::DrawGlyphOrEmoji> glyph_run = {})
: m_layout_node(layout_node) : m_layout_node(layout_node)
, m_start(start) , m_start(start)
, m_length(length) , m_length(length)
@ -25,6 +26,7 @@ public:
, m_size(size) , m_size(size)
, m_border_box_top(border_box_top) , m_border_box_top(border_box_top)
, m_border_box_bottom(border_box_bottom) , m_border_box_bottom(border_box_bottom)
, m_glyph_run(move(glyph_run))
{ {
} }
@ -71,6 +73,8 @@ public:
bool is_atomic_inline() const; bool is_atomic_inline() const;
Vector<Gfx::DrawGlyphOrEmoji> const& glyph_run() const { return m_glyph_run; }
private: private:
JS::NonnullGCPtr<Node const> m_layout_node; JS::NonnullGCPtr<Node const> m_layout_node;
int m_start { 0 }; int m_start { 0 };
@ -80,6 +84,7 @@ private:
CSSPixels m_border_box_top { 0 }; CSSPixels m_border_box_top { 0 };
CSSPixels m_border_box_bottom { 0 }; CSSPixels m_border_box_bottom { 0 };
CSSPixels m_baseline { 0 }; CSSPixels m_baseline { 0 };
Vector<Gfx::DrawGlyphOrEmoji> m_glyph_run;
}; };
} }

View file

@ -97,9 +97,9 @@ void LineBuilder::append_box(Box const& box, CSSPixels leading_size, CSSPixels t
}; };
} }
void LineBuilder::append_text_chunk(TextNode const& text_node, size_t offset_in_node, size_t length_in_node, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height) void LineBuilder::append_text_chunk(TextNode const& text_node, size_t offset_in_node, size_t length_in_node, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, Span<Gfx::DrawGlyphOrEmoji> glyph_run)
{ {
ensure_last_line_box().add_fragment(text_node, offset_in_node, length_in_node, leading_size, trailing_size, leading_margin, trailing_margin, content_width, content_height, 0, 0); ensure_last_line_box().add_fragment(text_node, offset_in_node, length_in_node, leading_size, trailing_size, leading_margin, trailing_margin, content_width, content_height, 0, 0, glyph_run);
m_max_height_on_current_line = max(m_max_height_on_current_line, content_height); m_max_height_on_current_line = max(m_max_height_on_current_line, content_height);
} }

View file

@ -25,7 +25,7 @@ public:
void break_line(ForcedBreak, Optional<CSSPixels> next_item_width = {}); void break_line(ForcedBreak, Optional<CSSPixels> next_item_width = {});
void append_box(Box const&, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin); void append_box(Box const&, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin);
void append_text_chunk(TextNode const&, size_t offset_in_node, size_t length_in_node, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height); void append_text_chunk(TextNode const&, size_t offset_in_node, size_t length_in_node, CSSPixels leading_size, CSSPixels trailing_size, CSSPixels leading_margin, CSSPixels trailing_margin, CSSPixels content_width, CSSPixels content_height, Span<Gfx::DrawGlyphOrEmoji>);
// Returns whether a line break occurred. // Returns whether a line break occurred.
bool break_if_needed(CSSPixels next_item_width) bool break_if_needed(CSSPixels next_item_width)

View file

@ -645,18 +645,24 @@ static void paint_text_fragment(PaintContext& context, Layout::TextNode const& t
auto text = text_node.text_for_rendering(); auto text = text_node.text_for_rendering();
DevicePixelPoint baseline_start { fragment_absolute_device_rect.x(), fragment_absolute_device_rect.y() + context.rounded_device_pixels(fragment.baseline()) }; DevicePixelPoint baseline_start { fragment_absolute_device_rect.x(), fragment_absolute_device_rect.y() + context.rounded_device_pixels(fragment.baseline()) };
Utf8View view { text.code_points().substring_view(fragment.start(), fragment.length()) }; auto const& scaled_font = fragment.layout_node().scaled_font(context);
Vector<Gfx::DrawGlyphOrEmoji> scaled_glyph_run;
auto& scaled_font = fragment.layout_node().scaled_font(context); scaled_glyph_run.ensure_capacity(fragment.glyph_run().size());
for (auto glyph : fragment.glyph_run()) {
painter.draw_text_run(baseline_start.to_type<int>(), view, scaled_font, text_node.computed_values().color(), fragment_absolute_device_rect.to_type<int>()); glyph.visit([&](auto& glyph) {
glyph.font = &scaled_font;
glyph.position = glyph.position.scaled(context.device_pixels_per_css_pixel());
});
scaled_glyph_run.append(move(glyph));
}
painter.draw_text_run(baseline_start.to_type<int>(), scaled_glyph_run, text_node.computed_values().color(), fragment_absolute_device_rect.to_type<int>());
auto selection_rect = context.enclosing_device_rect(fragment.selection_rect(text_node.font())).to_type<int>(); auto selection_rect = context.enclosing_device_rect(fragment.selection_rect(text_node.font())).to_type<int>();
if (!selection_rect.is_empty()) { if (!selection_rect.is_empty()) {
painter.fill_rect(selection_rect, CSS::SystemColor::highlight()); painter.fill_rect(selection_rect, CSS::SystemColor::highlight());
RecordingPainterStateSaver saver(painter); RecordingPainterStateSaver saver(painter);
painter.add_clip_rect(selection_rect); painter.add_clip_rect(selection_rect);
painter.draw_text_run(baseline_start.to_type<int>(), view, scaled_font, CSS::SystemColor::highlight_text(), fragment_absolute_device_rect.to_type<int>()); painter.draw_text_run(baseline_start.to_type<int>(), fragment.glyph_run(), CSS::SystemColor::highlight_text(), fragment_absolute_device_rect.to_type<int>());
} }
paint_text_decoration(context, text_node, fragment); paint_text_decoration(context, text_node, fragment);

View file

@ -33,7 +33,7 @@ CommandResult PaintingCommandExecutorCPU::draw_glyph_run(Vector<Gfx::DrawGlyphOr
painter.draw_glyph(glyph.position, glyph.code_point, *glyph.font, color); painter.draw_glyph(glyph.position, glyph.code_point, *glyph.font, color);
} else { } else {
auto& emoji = glyph_or_emoji.get<Gfx::DrawEmoji>(); auto& emoji = glyph_or_emoji.get<Gfx::DrawEmoji>();
painter.draw_emoji(emoji.position, *emoji.emoji, *emoji.font); painter.draw_emoji(emoji.position.to_type<int>(), *emoji.emoji, *emoji.font);
} }
} }
return CommandResult::Continue; return CommandResult::Continue;

View file

@ -214,11 +214,17 @@ void RecordingPainter::draw_signed_distance_field(Gfx::IntRect const& dst_rect,
}); });
} }
void RecordingPainter::draw_text_run(Gfx::IntPoint baseline_start, Utf8View string, Gfx::Font const& font, Color color, Gfx::IntRect const& rect) void RecordingPainter::draw_text_run(Gfx::IntPoint baseline_start, Span<Gfx::DrawGlyphOrEmoji const> glyph_run, Color color, Gfx::IntRect const& rect)
{ {
auto glyph_run = Gfx::get_glyph_run(state().translation.map(baseline_start).to_type<float>(), string, font); auto transformed_baseline_start = state().translation.map(baseline_start).to_type<float>();
Vector<Gfx::DrawGlyphOrEmoji> translated_glyph_run;
translated_glyph_run.ensure_capacity(glyph_run.size());
for (auto glyph : glyph_run) {
glyph.visit([&](auto& glyph) { glyph.position.translate_by(transformed_baseline_start); });
translated_glyph_run.append(glyph);
}
push_command(DrawGlyphRun { push_command(DrawGlyphRun {
.glyph_run = glyph_run, .glyph_run = move(translated_glyph_run),
.color = color, .color = color,
.rect = state().translation.map(rect), .rect = state().translation.map(rect),
}); });

View file

@ -460,7 +460,7 @@ public:
void draw_signed_distance_field(Gfx::IntRect const& dst_rect, Color color, Gfx::GrayscaleBitmap const& sdf, float smoothing); void draw_signed_distance_field(Gfx::IntRect const& dst_rect, Color color, Gfx::GrayscaleBitmap const& sdf, float smoothing);
// Streamlined text drawing routine that does no wrapping/elision/alignment. // Streamlined text drawing routine that does no wrapping/elision/alignment.
void draw_text_run(Gfx::IntPoint baseline_start, Utf8View string, Gfx::Font const& font, Color color, Gfx::IntRect const& rect); void draw_text_run(Gfx::IntPoint baseline_start, Span<Gfx::DrawGlyphOrEmoji const> glyph_run, Color color, Gfx::IntRect const& rect);
void add_clip_rect(Gfx::IntRect const& rect); void add_clip_rect(Gfx::IntRect const& rect);