perf: Add binary search for TXT word wrap

Use binary search instead of linear search for finding line break
positions. This significantly improves TXT file indexing performance,
especially with SD card fonts where getTextWidth() calls are slower.
This commit is contained in:
Eunchurn Park 2026-01-18 19:01:36 +09:00
parent 00134bbe80
commit fa4c63e54b
No known key found for this signature in database
GPG Key ID: 29D94D9C697E3F92

View File

@ -19,6 +19,70 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
// Cache file magic and version
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
// Find UTF-8 character boundary at or before pos
size_t findUtf8Boundary(const std::string& str, size_t pos) {
if (pos >= str.length()) return str.length();
// Move back if we're in the middle of a UTF-8 sequence
while (pos > 0 && (str[pos] & 0xC0) == 0x80) {
pos--;
}
return pos;
}
// Binary search to find max characters that fit in width
// Returns the position (byte offset) where to break the string
size_t findBreakPosition(GfxRenderer& renderer, int fontId, const std::string& line, int maxWidth) {
if (line.empty()) return 0;
// First check if the whole line fits
int fullWidth = renderer.getTextWidth(fontId, line.c_str());
if (fullWidth <= maxWidth) {
return line.length();
}
// Binary search for the break point
size_t low = 1; // At minimum 1 character
size_t high = line.length();
size_t bestFit = 1;
while (low < high) {
size_t mid = (low + high + 1) / 2;
mid = findUtf8Boundary(line, mid);
if (mid <= low) {
// Can't make progress, exit
break;
}
std::string substr = line.substr(0, mid);
int width = renderer.getTextWidth(fontId, substr.c_str());
if (width <= maxWidth) {
bestFit = mid;
low = mid;
} else {
high = mid - 1;
if (high > 0) {
high = findUtf8Boundary(line, high);
}
}
}
// Try to break at word boundary (space) if possible
if (bestFit > 0 && bestFit < line.length()) {
size_t spacePos = line.rfind(' ', bestFit);
if (spacePos != std::string::npos && spacePos > 0) {
// Check if breaking at space still fits
std::string atSpace = line.substr(0, spacePos);
if (renderer.getTextWidth(fontId, atSpace.c_str()) <= maxWidth) {
return spacePos;
}
}
}
return bestFit > 0 ? bestFit : 1; // At minimum, consume 1 character
}
} // namespace
void TxtReaderActivity::taskTrampoline(void* param) {
@ -303,36 +367,21 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
// Track position within this source line (in bytes from pos)
size_t lineBytePos = 0;
// Word wrap if needed
// Word wrap if needed - use binary search for performance with SD fonts
while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage) {
int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str());
// Use binary search to find break position (much faster than linear search)
size_t breakPos = findBreakPosition(renderer, cachedFontId, line, viewportWidth);
if (lineWidth <= viewportWidth) {
if (breakPos >= line.length()) {
// Whole line fits
outLines.push_back(line);
lineBytePos = displayLen; // Consumed entire display content
lineBytePos = displayLen;
line.clear();
break;
}
// Find break point
size_t breakPos = line.length();
while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) {
// Try to break at space
size_t spacePos = line.rfind(' ', breakPos - 1);
if (spacePos != std::string::npos && spacePos > 0) {
breakPos = spacePos;
} else {
// Break at character boundary for UTF-8
breakPos--;
// Make sure we don't break in the middle of a UTF-8 sequence
while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) {
breakPos--;
}
}
}
if (breakPos == 0) {
breakPos = 1;
breakPos = 1; // Ensure progress
}
outLines.push_back(line.substr(0, breakPos));