Skip to content

Commit

Permalink
Accelerate text rendering. (#4674)
Browse files Browse the repository at this point in the history
The algorithm fitting the text into a given width was slow as reported
here: #4626

This PR replaces the code with the methods Qt provides for this purpose.

Since Qt does things different from what was implemented before we get
some changed behaviour:
Most notably we don't render text into any timers that are too small to
fit at least "W...". Also, instead of truncating short texts, we now
truncate and add "...".

Since the behaviour is changed it is a bit hard estimate the performance
gain. Measurements suggest ~4-5x with test cases that previously did a
lot of calls in the code fitting the strings into a given width. I think
this largely solves the issue in the bug above.
  • Loading branch information
danielfenner authored Jan 4, 2023
1 parent da8143d commit ea4c543
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 68 deletions.
129 changes: 61 additions & 68 deletions src/OrbitGl/QtTextRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,52 +109,39 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
if (text_length == 0) {
return;
}
const float width_entire_text = GetStringWidth(text, formatting.font_size);
const float height_entire_text = GetStringHeight(text, formatting.font_size);
Vec2i pen_pos = viewport_->WorldToScreen(Vec2(x, y));
LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY(
{{static_cast<float>(pen_pos[0]), static_cast<float>(pen_pos[1])}, z});
const float max_width =
formatting.max_size == -1.f ? FLT_MAX : viewport_->WorldToScreen({formatting.max_size, 0})[0];
// Find out how many characters from text can fit into max_width via a binary search.
size_t idx_min = 1;
size_t idx_max = text_length;
if (GetStringWidth(text_as_qstring.left(1), formatting.font_size) > max_width) {
return;
}
if (width_entire_text <= max_width) {
idx_min = text_length;
}
while (idx_max - idx_min > 1) {
const size_t candidate_idx = (idx_min + idx_max) / 2;
const QString candidate_string = text_as_qstring.left(candidate_idx);
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
idx_max = candidate_idx;
} else {
idx_min = candidate_idx;
}
}
text_as_qstring = text_as_qstring.left(idx_min);
const int max_width = static_cast<int>(viewport_->WorldToScreen({formatting.max_size, 0})[0]);
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
font.setPixelSize(formatting.font_size);
QFontMetrics metrics(font);
float y_offset = GetYOffsetFromAlignment(formatting.valign, height_entire_text);
const float single_line_height = GetStringHeight(".", formatting.font_size);
QStringList lines = text_as_qstring.split("\n");
float max_line_width = 0.f;
for (const auto& line : lines) {
const float width = GetStringWidth(line, formatting.font_size);
QString elided_line =
formatting.max_size == -1.f ? line : metrics.elidedText(line, Qt::ElideRight, max_width);
const float width = GetStringWidth(elided_line, formatting.font_size);
max_line_width = std::max(max_line_width, width);
const float x_offset = GetXOffsetFromAlignment(formatting.halign, width);
stored_text_[transformed.z].emplace_back(
line, std::lround(transformed.xy[0] + x_offset), std::lround(transformed.xy[1] + y_offset),
std::lround(width), std::lround(single_line_height), formatting);
stored_text_[transformed.z].emplace_back(elided_line, std::lround(transformed.xy[0] + x_offset),
std::lround(transformed.xy[1] + y_offset),
std::lround(width), std::lround(single_line_height),
formatting);
y_offset += single_line_height;
}

if (out_text_pos != nullptr) {
(*out_text_pos)[0] =
transformed.xy[0] + GetXOffsetFromAlignment(formatting.halign, width_entire_text);
transformed.xy[0] + GetXOffsetFromAlignment(formatting.halign, max_line_width);
(*out_text_pos)[1] =
transformed.xy[1] + GetYOffsetFromAlignment(formatting.valign, height_entire_text);
}
if (out_text_size != nullptr) {
*out_text_size = Vec2(width_entire_text, height_entire_text);
*out_text_size = Vec2(max_line_width, height_entire_text);
}
}

Expand All @@ -176,55 +163,33 @@ float QtTextRenderer::AddTextTrailingCharsPrioritized(const char* text, float x,
}
// Early-out: If we can't fit a single char, there's no use to do all the expensive
// calculations below - this is a major bottleneck in some cases
if (formatting.max_size >= 0 && GetStringWidth(".", formatting.font_size) > formatting.max_size) {
if (formatting.max_size >= 0 && GetMinimumTextWidth(formatting.font_size) > formatting.max_size) {
return 0.f;
}

// Test if the entire string fits.
const float max_width =
formatting.max_size == -1.f ? FLT_MAX : viewport_->WorldToScreen({formatting.max_size, 0})[0];
const float entire_width = GetStringWidth(text, formatting.font_size);
if (entire_width <= max_width) {
AddText(text, x, y, z, formatting);
return entire_width;
}
// The entire string does not fit. We try to fit something of the form
// leading_text + "... " + trailing_text
// where leading_text is a variable amount of characters from the beginning of the string and
// trailing_text is specified by the trailing_chars_length parameter.
const QString trailing_text = text_as_qstring.right(trailing_chars_length);
const QString leading_text = text_as_qstring.left(text_length - trailing_chars_length);
const size_t leading_length = leading_text.length();
const QString ellipsis_plus_trailing_text = QString("... ") + trailing_text;
size_t fitting_chars_count = 1;
QString candidate_string = leading_text.left(fitting_chars_count) + ellipsis_plus_trailing_text;

// Test if we can fit the minimal ellipsised string.
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
// We can't fit any ellipsised string: Even with one leading character the thing becomes too
// long. So we let AddText truncate the entire string.
Vec2 dims;
AddText(text, x, y, z, formatting, nullptr, &dims);
return dims[0];
}

// Binary search between 1 and leading_length (we know 1 works and leading_length does not).
size_t idx_min = 1;
size_t idx_max = leading_length;
while (idx_max - idx_min > 1) {
size_t candidate_idx = (idx_min + idx_max) / 2;
candidate_string = leading_text.left(candidate_idx) + ellipsis_plus_trailing_text;
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
idx_max = candidate_idx;
} else {
idx_min = candidate_idx;
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
font.setPixelSize(formatting.font_size);
QFontMetrics metrics(font);
const float trailing_text_width = GetStringWidth(trailing_text, formatting.font_size);
QString elided_text;
if (trailing_text_width < max_width) {
elided_text = metrics.elidedText(leading_text, Qt::ElideRight,
static_cast<int>(max_width - trailing_text_width));
}
if (elided_text.isEmpty()) {
// We can't fit any elided string with the trailing characters preserved so we elide the entire
// string and accept that the trailing characters are truncated.
elided_text = metrics.elidedText(text_as_qstring, Qt::ElideRight, static_cast<int>(max_width));
if (elided_text.isEmpty()) {
return 0.f;
}
return AddFittingSingleLineText(elided_text, x, y, z, formatting);
}
fitting_chars_count = idx_min;
candidate_string = leading_text.left(fitting_chars_count) + ellipsis_plus_trailing_text;
const std::string candiate_as_std_string = candidate_string.toStdString();
AddText(candiate_as_std_string.c_str(), x, y, z, formatting);
return GetStringWidth(candidate_string, formatting.font_size);
return AddFittingSingleLineText(elided_text + trailing_text, x, y, z, formatting);
}

float QtTextRenderer::GetStringWidth(const char* text, uint32_t font_size) {
Expand Down Expand Up @@ -256,4 +221,32 @@ float QtTextRenderer::GetStringWidth(const QString& text, uint32_t font_size) {
return GetStringWidth(text_as_string.c_str(), font_size);
}

float QtTextRenderer::GetMinimumTextWidth(uint32_t font_size) {
auto minimum_string_width_it = minimum_string_width_cache_.find(font_size);
if (minimum_string_width_it != minimum_string_width_cache_.end()) {
return minimum_string_width_it->second;
}
// Only if we can fit one wide (hence the "W") character plus the ellipsis dots we start rendering
// text. Otherwise we leave the space empty.
constexpr char const* kMinimumString = "W...";
const float width = GetStringWidth(kMinimumString, font_size);
minimum_string_width_cache_[font_size] = width;
return width;
}

float QtTextRenderer::AddFittingSingleLineText(const QString& text, float x, float y, float z,
TextFormatting formatting) {
const float width = GetStringWidth(text, formatting.font_size);
const float single_line_height = GetStringHeight(".", formatting.font_size);
Vec2i pen_pos = viewport_->WorldToScreen(Vec2(x, y));
LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY(
{{static_cast<float>(pen_pos[0]), static_cast<float>(pen_pos[1])}, z});
const float x_offset = GetXOffsetFromAlignment(formatting.halign, width);
float y_offset = GetYOffsetFromAlignment(formatting.valign, single_line_height);
stored_text_[transformed.z].emplace_back(
text, std::lround(transformed.xy[0] + x_offset), std::lround(transformed.xy[1] + y_offset),
std::lround(width), std::lround(single_line_height), formatting);
return width;
}

} // namespace orbit_gl
4 changes: 4 additions & 0 deletions src/OrbitGl/include/OrbitGl/QtTextRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class QtTextRenderer : public TextRenderer {

private:
[[nodiscard]] float GetStringWidth(const QString& text, uint32_t font_size);
[[nodiscard]] float GetMinimumTextWidth(uint32_t font_size);
[[nodiscard]] float AddFittingSingleLineText(const QString& text, float x, float y, float z,
TextFormatting formatting);

struct StoredText {
StoredText() = default;
Expand All @@ -55,6 +58,7 @@ class QtTextRenderer : public TextRenderer {
TextFormatting formatting;
};
absl::flat_hash_map<float, std::vector<StoredText>> stored_text_;
absl::flat_hash_map<uint32_t, float> minimum_string_width_cache_;
};

} // namespace orbit_gl
Expand Down
7 changes: 7 additions & 0 deletions src/OrbitGl/include/OrbitGl/TextRendererInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,17 @@ class TextRendererInterface {
virtual void RenderLayer(QPainter* painter, float layer) = 0;
[[nodiscard]] virtual std::vector<float> GetLayers() const = 0;

// Add a - potentially multiline - text at the given position and z-layer and with the specifier
// formatting. If formatting.max_size is set all lines are elided to fit into this width.
virtual void AddText(const char* text, float x, float y, float z, TextFormatting formatting) = 0;
virtual void AddText(const char* text, float x, float y, float z, TextFormatting formatting,
Vec2* out_text_pos, Vec2* out_text_size) = 0;

// Add a single line of text at the given position and z-layer and with the specifier formatting.
// The renderer will shorten the text if the width exceeds formatting.max_size. The shortening
// will happen in a way that tries to preserve the given numnber of trailing characters.
// This is mainly used to preserve the duration in the text of time intervals. E.g. something like
// "MyVeryLongButNotSoImportantMethodName 2.35 ms" will render as "MyVery...2.35 ms".
virtual float AddTextTrailingCharsPrioritized(const char* text, float x, float y, float z,
TextFormatting formatting,
size_t trailing_chars_length) = 0;
Expand Down

0 comments on commit ea4c543

Please sign in to comment.