Skip to content
This repository was archived by the owner on Jan 31, 2025. It is now read-only.

Commit ea4c543

Browse files
authored
Accelerate text rendering. (#4674)
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.
1 parent da8143d commit ea4c543

File tree

3 files changed

+72
-68
lines changed

3 files changed

+72
-68
lines changed

src/OrbitGl/QtTextRenderer.cpp

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -109,52 +109,39 @@ void QtTextRenderer::AddText(const char* text, float x, float y, float z, TextFo
109109
if (text_length == 0) {
110110
return;
111111
}
112-
const float width_entire_text = GetStringWidth(text, formatting.font_size);
113112
const float height_entire_text = GetStringHeight(text, formatting.font_size);
114113
Vec2i pen_pos = viewport_->WorldToScreen(Vec2(x, y));
115114
LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY(
116115
{{static_cast<float>(pen_pos[0]), static_cast<float>(pen_pos[1])}, z});
117-
const float max_width =
118-
formatting.max_size == -1.f ? FLT_MAX : viewport_->WorldToScreen({formatting.max_size, 0})[0];
119-
// Find out how many characters from text can fit into max_width via a binary search.
120-
size_t idx_min = 1;
121-
size_t idx_max = text_length;
122-
if (GetStringWidth(text_as_qstring.left(1), formatting.font_size) > max_width) {
123-
return;
124-
}
125-
if (width_entire_text <= max_width) {
126-
idx_min = text_length;
127-
}
128-
while (idx_max - idx_min > 1) {
129-
const size_t candidate_idx = (idx_min + idx_max) / 2;
130-
const QString candidate_string = text_as_qstring.left(candidate_idx);
131-
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
132-
idx_max = candidate_idx;
133-
} else {
134-
idx_min = candidate_idx;
135-
}
136-
}
137-
text_as_qstring = text_as_qstring.left(idx_min);
116+
const int max_width = static_cast<int>(viewport_->WorldToScreen({formatting.max_size, 0})[0]);
117+
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
118+
font.setPixelSize(formatting.font_size);
119+
QFontMetrics metrics(font);
138120
float y_offset = GetYOffsetFromAlignment(formatting.valign, height_entire_text);
139121
const float single_line_height = GetStringHeight(".", formatting.font_size);
140122
QStringList lines = text_as_qstring.split("\n");
123+
float max_line_width = 0.f;
141124
for (const auto& line : lines) {
142-
const float width = GetStringWidth(line, formatting.font_size);
125+
QString elided_line =
126+
formatting.max_size == -1.f ? line : metrics.elidedText(line, Qt::ElideRight, max_width);
127+
const float width = GetStringWidth(elided_line, formatting.font_size);
128+
max_line_width = std::max(max_line_width, width);
143129
const float x_offset = GetXOffsetFromAlignment(formatting.halign, width);
144-
stored_text_[transformed.z].emplace_back(
145-
line, std::lround(transformed.xy[0] + x_offset), std::lround(transformed.xy[1] + y_offset),
146-
std::lround(width), std::lround(single_line_height), formatting);
130+
stored_text_[transformed.z].emplace_back(elided_line, std::lround(transformed.xy[0] + x_offset),
131+
std::lround(transformed.xy[1] + y_offset),
132+
std::lround(width), std::lround(single_line_height),
133+
formatting);
147134
y_offset += single_line_height;
148135
}
149136

150137
if (out_text_pos != nullptr) {
151138
(*out_text_pos)[0] =
152-
transformed.xy[0] + GetXOffsetFromAlignment(formatting.halign, width_entire_text);
139+
transformed.xy[0] + GetXOffsetFromAlignment(formatting.halign, max_line_width);
153140
(*out_text_pos)[1] =
154141
transformed.xy[1] + GetYOffsetFromAlignment(formatting.valign, height_entire_text);
155142
}
156143
if (out_text_size != nullptr) {
157-
*out_text_size = Vec2(width_entire_text, height_entire_text);
144+
*out_text_size = Vec2(max_line_width, height_entire_text);
158145
}
159146
}
160147

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

183-
// Test if the entire string fits.
184170
const float max_width =
185171
formatting.max_size == -1.f ? FLT_MAX : viewport_->WorldToScreen({formatting.max_size, 0})[0];
186-
const float entire_width = GetStringWidth(text, formatting.font_size);
187-
if (entire_width <= max_width) {
188-
AddText(text, x, y, z, formatting);
189-
return entire_width;
190-
}
191-
// The entire string does not fit. We try to fit something of the form
192-
// leading_text + "... " + trailing_text
193-
// where leading_text is a variable amount of characters from the beginning of the string and
194-
// trailing_text is specified by the trailing_chars_length parameter.
195172
const QString trailing_text = text_as_qstring.right(trailing_chars_length);
196173
const QString leading_text = text_as_qstring.left(text_length - trailing_chars_length);
197-
const size_t leading_length = leading_text.length();
198-
const QString ellipsis_plus_trailing_text = QString("... ") + trailing_text;
199-
size_t fitting_chars_count = 1;
200-
QString candidate_string = leading_text.left(fitting_chars_count) + ellipsis_plus_trailing_text;
201-
202-
// Test if we can fit the minimal ellipsised string.
203-
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
204-
// We can't fit any ellipsised string: Even with one leading character the thing becomes too
205-
// long. So we let AddText truncate the entire string.
206-
Vec2 dims;
207-
AddText(text, x, y, z, formatting, nullptr, &dims);
208-
return dims[0];
209-
}
210-
211-
// Binary search between 1 and leading_length (we know 1 works and leading_length does not).
212-
size_t idx_min = 1;
213-
size_t idx_max = leading_length;
214-
while (idx_max - idx_min > 1) {
215-
size_t candidate_idx = (idx_min + idx_max) / 2;
216-
candidate_string = leading_text.left(candidate_idx) + ellipsis_plus_trailing_text;
217-
if (GetStringWidth(candidate_string, formatting.font_size) > max_width) {
218-
idx_max = candidate_idx;
219-
} else {
220-
idx_min = candidate_idx;
174+
QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
175+
font.setPixelSize(formatting.font_size);
176+
QFontMetrics metrics(font);
177+
const float trailing_text_width = GetStringWidth(trailing_text, formatting.font_size);
178+
QString elided_text;
179+
if (trailing_text_width < max_width) {
180+
elided_text = metrics.elidedText(leading_text, Qt::ElideRight,
181+
static_cast<int>(max_width - trailing_text_width));
182+
}
183+
if (elided_text.isEmpty()) {
184+
// We can't fit any elided string with the trailing characters preserved so we elide the entire
185+
// string and accept that the trailing characters are truncated.
186+
elided_text = metrics.elidedText(text_as_qstring, Qt::ElideRight, static_cast<int>(max_width));
187+
if (elided_text.isEmpty()) {
188+
return 0.f;
221189
}
190+
return AddFittingSingleLineText(elided_text, x, y, z, formatting);
222191
}
223-
fitting_chars_count = idx_min;
224-
candidate_string = leading_text.left(fitting_chars_count) + ellipsis_plus_trailing_text;
225-
const std::string candiate_as_std_string = candidate_string.toStdString();
226-
AddText(candiate_as_std_string.c_str(), x, y, z, formatting);
227-
return GetStringWidth(candidate_string, formatting.font_size);
192+
return AddFittingSingleLineText(elided_text + trailing_text, x, y, z, formatting);
228193
}
229194

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

224+
float QtTextRenderer::GetMinimumTextWidth(uint32_t font_size) {
225+
auto minimum_string_width_it = minimum_string_width_cache_.find(font_size);
226+
if (minimum_string_width_it != minimum_string_width_cache_.end()) {
227+
return minimum_string_width_it->second;
228+
}
229+
// Only if we can fit one wide (hence the "W") character plus the ellipsis dots we start rendering
230+
// text. Otherwise we leave the space empty.
231+
constexpr char const* kMinimumString = "W...";
232+
const float width = GetStringWidth(kMinimumString, font_size);
233+
minimum_string_width_cache_[font_size] = width;
234+
return width;
235+
}
236+
237+
float QtTextRenderer::AddFittingSingleLineText(const QString& text, float x, float y, float z,
238+
TextFormatting formatting) {
239+
const float width = GetStringWidth(text, formatting.font_size);
240+
const float single_line_height = GetStringHeight(".", formatting.font_size);
241+
Vec2i pen_pos = viewport_->WorldToScreen(Vec2(x, y));
242+
LayeredVec2 transformed = translations_.TranslateXYZAndFloorXY(
243+
{{static_cast<float>(pen_pos[0]), static_cast<float>(pen_pos[1])}, z});
244+
const float x_offset = GetXOffsetFromAlignment(formatting.halign, width);
245+
float y_offset = GetYOffsetFromAlignment(formatting.valign, single_line_height);
246+
stored_text_[transformed.z].emplace_back(
247+
text, std::lround(transformed.xy[0] + x_offset), std::lround(transformed.xy[1] + y_offset),
248+
std::lround(width), std::lround(single_line_height), formatting);
249+
return width;
250+
}
251+
259252
} // namespace orbit_gl

src/OrbitGl/include/OrbitGl/QtTextRenderer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ class QtTextRenderer : public TextRenderer {
4242

4343
private:
4444
[[nodiscard]] float GetStringWidth(const QString& text, uint32_t font_size);
45+
[[nodiscard]] float GetMinimumTextWidth(uint32_t font_size);
46+
[[nodiscard]] float AddFittingSingleLineText(const QString& text, float x, float y, float z,
47+
TextFormatting formatting);
4548

4649
struct StoredText {
4750
StoredText() = default;
@@ -55,6 +58,7 @@ class QtTextRenderer : public TextRenderer {
5558
TextFormatting formatting;
5659
};
5760
absl::flat_hash_map<float, std::vector<StoredText>> stored_text_;
61+
absl::flat_hash_map<uint32_t, float> minimum_string_width_cache_;
5862
};
5963

6064
} // namespace orbit_gl

src/OrbitGl/include/OrbitGl/TextRendererInterface.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@ class TextRendererInterface {
3636
virtual void RenderLayer(QPainter* painter, float layer) = 0;
3737
[[nodiscard]] virtual std::vector<float> GetLayers() const = 0;
3838

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

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

0 commit comments

Comments
 (0)