From 909502dc4111aebf366f06a6f18a558e224f841c Mon Sep 17 00:00:00 2001 From: rubidium Date: Wed, 10 Jul 2013 19:41:31 +0000 Subject: [PATCH] (svn r25583) [1.3] -Backport from trunk: - Fix: Layouter caused significant slowdown with text heavy windows, cache it to make it managable (r25574, r25570, r25569, r25567, r25564) --- src/fontcache.cpp | 3 + src/gfx.cpp | 52 ++++-------- src/gfx_layout.cpp | 196 ++++++++++++++++++++++++++++++++------------- src/gfx_layout.h | 86 +++++++++++++++++++- src/openttd.cpp | 3 + 5 files changed, 245 insertions(+), 95 deletions(-) diff --git a/src/fontcache.cpp b/src/fontcache.cpp index b51cecaf6c..9e0fd1e166 100644 --- a/src/fontcache.cpp +++ b/src/fontcache.cpp @@ -17,6 +17,7 @@ #include "core/smallmap_type.hpp" #include "strings_func.h" #include "zoom_type.h" +#include "gfx_layout.h" #include "table/sprites.h" #include "table/control_codes.h" @@ -39,6 +40,7 @@ FontCache::FontCache(FontSize fs) : parent(FontCache::Get(fs)), fs(fs), height(_ { assert(parent == NULL || this->fs == parent->fs); FontCache::caches[this->fs] = this; + Layouter::ResetFontCache(this->fs); } /** Clean everything up. */ @@ -46,6 +48,7 @@ FontCache::~FontCache() { assert(this->fs == parent->fs); FontCache::caches[this->fs] = this->parent; + Layouter::ResetFontCache(this->fs); } diff --git a/src/gfx.cpp b/src/gfx.cpp index c37c305adb..e0a7c060fe 100644 --- a/src/gfx.cpp +++ b/src/gfx.cpp @@ -51,43 +51,6 @@ byte _colour_gradient[COLOUR_END][8]; static void GfxMainBlitterViewport(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub = NULL, SpriteID sprite_id = SPR_CURSOR_MOUSE); static void GfxMainBlitter(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub = NULL, SpriteID sprite_id = SPR_CURSOR_MOUSE, ZoomLevel zoom = ZOOM_LVL_NORMAL); -/** - * Text drawing parameters, which can change while drawing a line, but are kept between multiple parts - * of the same text, e.g. on line breaks. - */ -struct DrawStringParams { - FontSize fontsize; - TextColour cur_colour, prev_colour; - - DrawStringParams(TextColour colour, FontSize fontsize) : fontsize(fontsize), cur_colour(colour), prev_colour(colour) {} - - /** - * Switch to new colour \a c. - * @param c New colour to use. - */ - inline void SetColour(TextColour c) - { - assert(c >= TC_BLUE && c <= TC_BLACK); - this->prev_colour = this->cur_colour; - this->cur_colour = c; - } - - /** Switch to previous colour. */ - inline void SetPreviousColour() - { - Swap(this->cur_colour, this->prev_colour); - } - - /** - * Switch to using a new font \a f. - * @param f New font to use. - */ - inline void SetFontSize(FontSize f) - { - this->fontsize = f; - } -}; - static ReusableBuffer _cursor_backup; /** @@ -453,9 +416,22 @@ static int DrawLayoutLine(ParagraphLayout::Line *line, int y, int left, int righ * will be drawn in the right direction. * @param underline Whether to underline what has been drawn or not. * @param fontsize The size of the initial characters. + * @return In case of left or center alignment the right most pixel we have drawn to. + * In case of right alignment the left most pixel we have drawn to. */ int DrawString(int left, int right, int top, const char *str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize) { + /* The string may contain control chars to change the font, just use the biggest font for clipping. */ + int max_height = max(max(FONT_HEIGHT_SMALL, FONT_HEIGHT_NORMAL), max(FONT_HEIGHT_LARGE, FONT_HEIGHT_MONO)); + + /* Funny glyphs may extent outside the usual bounds, so relax the clipping somewhat. */ + int extra = max_height / 2; + + if (_cur_dpi->top + _cur_dpi->height + extra < top || _cur_dpi->top > top + max_height + extra || + _cur_dpi->left + _cur_dpi->width + extra < left || _cur_dpi->left > right + extra) { + return 0; + } + Layouter layout(str, INT32_MAX, colour, fontsize); if (layout.Length() == 0) return 0; @@ -475,6 +451,8 @@ int DrawString(int left, int right, int top, const char *str, TextColour colour, * will be drawn in the right direction. * @param underline Whether to underline what has been drawn or not. * @param fontsize The size of the initial characters. + * @return In case of left or center alignment the right most pixel we have drawn to. + * In case of right alignment the left most pixel we have drawn to. */ int DrawString(int left, int right, int top, StringID str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize) { diff --git a/src/gfx_layout.cpp b/src/gfx_layout.cpp index 84755ade80..b477093743 100644 --- a/src/gfx_layout.cpp +++ b/src/gfx_layout.cpp @@ -20,6 +20,14 @@ #include #endif /* WITH_ICU */ + +/** Cache of ParagraphLayout lines. */ +Layouter::LineCache *Layouter::linecache; + +/** Cache of Font instances. */ +Layouter::FontColourMap Layouter::fonts[FS_END]; + + /** * Construct a new font. * @param size The font size to use for this font. @@ -129,6 +137,8 @@ ParagraphLayout *Layouter::GetParagraphLayout(UChar *buff, UChar *buff_end, Font } LEErrorCode status = LE_NO_ERROR; + /* ParagraphLayout does not copy "buff", so it must stay valid. + * "runs" is copied according to the ICU source, but the documentation does not specify anything, so this might break somewhen. */ return new ParagraphLayout(buff, length, &runs, NULL, NULL, NULL, _current_text_dir == TD_RTL ? UBIDI_DEFAULT_RTL : UBIDI_DEFAULT_LTR, false, status); } @@ -272,6 +282,14 @@ ParagraphLayout::ParagraphLayout(WChar *buffer, int length, FontMap &runs) : buf assert(runs.End()[-1].first == length); } +/** + * Reset the position to the start of the paragraph. + */ +void ParagraphLayout::reflow() +{ + this->buffer = this->buffer_begin; +} + /** * Construct a new line with a maximum width. * @param max_width The maximum width of the string. @@ -295,7 +313,7 @@ ParagraphLayout::Line *ParagraphLayout::nextLine(int max_width) } const WChar *begin = this->buffer; - WChar *last_space = NULL; + const WChar *last_space = NULL; const WChar *last_char = begin; int width = 0; @@ -407,75 +425,77 @@ ParagraphLayout *Layouter::GetParagraphLayout(WChar *buff, WChar *buff_end, Font */ Layouter::Layouter(const char *str, int maxw, TextColour colour, FontSize fontsize) { - const CharType *buffer_last = lastof(this->buffer); - CharType *buff = this->buffer; - - TextColour cur_colour = colour, prev_colour = colour; + FontState state(colour, fontsize); WChar c = 0; do { - Font *f = new Font(fontsize, cur_colour); - CharType *buff_begin = buff; - FontMap fontMapping; + /* Scan string for end of line */ + const char *lineend = str; + for (;;) { + size_t len = Utf8Decode(&c, lineend); + if (c == '\0' || c == '\n') break; + lineend += len; + } - /* - * Go through the whole string while adding Font instances to the font map - * whenever the font changes, and convert the wide characters into a format - * usable by ParagraphLayout. - */ - for (; buff < buffer_last;) { - c = Utf8Consume(const_cast(&str)); - if (c == '\0' || c == '\n') { - break; - } else if (c >= SCC_BLUE && c <= SCC_BLACK) { - prev_colour = cur_colour; - cur_colour = (TextColour)(c - SCC_BLUE); - } else if (c == SCC_PREVIOUS_COLOUR) { // Revert to the previous colour. - Swap(prev_colour, cur_colour); - } else if (c == SCC_TINYFONT) { - fontsize = FS_SMALL; - } else if (c == SCC_BIGFONT) { - fontsize = FS_LARGE; - } else { - buff += AppendToBuffer(buff, buffer_last, c); - continue; + LineCacheItem& line = GetCachedParagraphLayout(str, lineend - str, state); + if (line.layout != NULL) { + /* Line is in cache */ + str = lineend + 1; + state = line.state_after; + line.layout->reflow(); + } else { + /* Line is new, layout it */ + const CharType *buffer_last = lastof(line.buffer); + CharType *buff_begin = line.buffer; + CharType *buff = buff_begin; + FontMap &fontMapping = line.runs; + Font *f = GetFont(state.fontsize, state.cur_colour); + + /* + * Go through the whole string while adding Font instances to the font map + * whenever the font changes, and convert the wide characters into a format + * usable by ParagraphLayout. + */ + for (; buff < buffer_last;) { + c = Utf8Consume(const_cast(&str)); + if (c == '\0' || c == '\n') { + break; + } else if (c >= SCC_BLUE && c <= SCC_BLACK) { + state.SetColour((TextColour)(c - SCC_BLUE)); + } else if (c == SCC_PREVIOUS_COLOUR) { // Revert to the previous colour. + state.SetPreviousColour(); + } else if (c == SCC_TINYFONT) { + state.SetFontSize(FS_SMALL); + } else if (c == SCC_BIGFONT) { + state.SetFontSize(FS_LARGE); + } else { + buff += AppendToBuffer(buff, buffer_last, c); + continue; + } + + if (!fontMapping.Contains(buff - buff_begin)) { + fontMapping.Insert(buff - buff_begin, f); + } + f = GetFont(state.fontsize, state.cur_colour); } + /* Better safe than sorry. */ + *buff = '\0'; + if (!fontMapping.Contains(buff - buff_begin)) { fontMapping.Insert(buff - buff_begin, f); - *this->fonts.Append() = f; - } else { - delete f; } - f = new Font(fontsize, cur_colour); + line.layout = GetParagraphLayout(buff_begin, buff, fontMapping); + line.state_after = state; } - /* Better safe than sorry. */ - *buff = '\0'; - - if (!fontMapping.Contains(buff - buff_begin)) { - fontMapping.Insert(buff - buff_begin, f); - *this->fonts.Append() = f; - } - ParagraphLayout *p = GetParagraphLayout(buff_begin, buff, fontMapping); - /* Copy all lines into a local cache so we can reuse them later on more easily. */ ParagraphLayout::Line *l; - while ((l = p->nextLine(maxw)) != NULL) { + while ((l = line.layout->nextLine(maxw)) != NULL) { *this->Append() = l; } - delete p; - - } while (c != '\0' && buff < buffer_last); -} - -/** Free everything we allocated. */ -Layouter::~Layouter() -{ - for (Font **iter = this->fonts.Begin(); iter != this->fonts.End(); iter++) { - delete *iter; - } + } while (c != '\0'); } /** @@ -491,3 +511,71 @@ Dimension Layouter::GetBounds() } return d; } + +/** + * Get a static font instance. + */ +Font *Layouter::GetFont(FontSize size, TextColour colour) +{ + FontColourMap::iterator it = fonts[size].Find(colour); + if (it != fonts[size].End()) return it->second; + + Font *f = new Font(size, colour); + *fonts[size].Append() = FontColourMap::Pair(colour, f); + return f; +} + +/** + * Reset cached font information. + * @param size Font size to reset. + */ +void Layouter::ResetFontCache(FontSize size) +{ + for (FontColourMap::iterator it = fonts[size].Begin(); it != fonts[size].End(); ++it) { + delete it->second; + } + fonts[size].Clear(); + + /* We must reset the linecache since it references the just freed fonts */ + ResetLineCache(); +} + +/** + * Get reference to cache item. + * If the item does not exist yet, it is default constructed. + * @param str Source string of the line (including colour and font size codes). + * @param len Length of \a str in bytes (no termination). + * @param state State of the font at the beginning of the line. + * @return Reference to cache item. + */ +Layouter::LineCacheItem &Layouter::GetCachedParagraphLayout(const char *str, size_t len, const FontState &state) +{ + if (linecache == NULL) { + /* Create linecache on first access to avoid trouble with initialisation order of static variables. */ + linecache = new LineCache(); + } + + LineCacheKey key; + key.state_before = state; + key.str.assign(str, len); + return (*linecache)[key]; +} + +/** + * Clear line cache. + */ +void Layouter::ResetLineCache() +{ + if (linecache != NULL) linecache->clear(); +} + +/** + * Reduce the size of linecache if necessary to prevent infinite growth. + */ +void Layouter::ReduceLineCache() +{ + if (linecache != NULL) { + /* TODO LRU cache would be fancy, but not exactly necessary */ + if (linecache->size() > 4096) ResetLineCache(); + } +} diff --git a/src/gfx_layout.h b/src/gfx_layout.h index 0fadd51263..c252d15ebd 100644 --- a/src/gfx_layout.h +++ b/src/gfx_layout.h @@ -16,6 +16,9 @@ #include "gfx_func.h" #include "core/smallmap_type.hpp" +#include +#include + #ifdef WITH_ICU #include "layout/ParagraphLayout.h" #define ICU_FONTINSTANCE : public LEFontInstance @@ -23,6 +26,45 @@ #define ICU_FONTINSTANCE #endif /* WITH_ICU */ +/** + * Text drawing parameters, which can change while drawing a line, but are kept between multiple parts + * of the same text, e.g. on line breaks. + */ +struct FontState { + FontSize fontsize; ///< Current font size. + TextColour cur_colour; ///< Current text colour. + TextColour prev_colour; ///< Text colour from before the last colour switch. + + FontState() : fontsize(FS_END), cur_colour(TC_INVALID), prev_colour(TC_INVALID) {} + FontState(TextColour colour, FontSize fontsize) : fontsize(fontsize), cur_colour(colour), prev_colour(colour) {} + + /** + * Switch to new colour \a c. + * @param c New colour to use. + */ + inline void SetColour(TextColour c) + { + assert(c >= TC_BLUE && c <= TC_BLACK); + this->prev_colour = this->cur_colour; + this->cur_colour = c; + } + + /** Switch to previous colour. */ + inline void SetPreviousColour() + { + Swap(this->cur_colour, this->prev_colour); + } + + /** + * Switch to using a new font \a f. + * @param f New font to use. + */ + inline void SetFontSize(FontSize f) + { + this->fontsize = f; + } +}; + /** * Container with information about a font. */ @@ -105,10 +147,11 @@ public: }; const WChar *buffer_begin; ///< Begin of the buffer. - WChar *buffer; ///< The current location in the buffer. + const WChar *buffer; ///< The current location in the buffer. FontMap &runs; ///< The fonts we have to use for this paragraph. ParagraphLayout(WChar *buffer, int length, FontMap &runs); + void reflow(); Line *nextLine(int max_width); }; #endif /* !WITH_ICU */ @@ -128,13 +171,48 @@ class Layouter : public AutoDeleteSmallVector { size_t AppendToBuffer(CharType *buff, const CharType *buffer_last, WChar c); ParagraphLayout *GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &fontMapping); - CharType buffer[DRAW_STRING_BUFFER]; ///< Buffer for the text that is going to be drawn. - SmallVector fonts; ///< The fonts needed for drawing. + /** Key into the linecache */ + struct LineCacheKey { + FontState state_before; ///< Font state at the beginning of the line. + std::string str; ///< Source string of the line (including colour and font size codes). + + /** Comparison operator for std::map */ + bool operator<(const LineCacheKey &other) const + { + if (this->state_before.fontsize != other.state_before.fontsize) return this->state_before.fontsize < other.state_before.fontsize; + if (this->state_before.cur_colour != other.state_before.cur_colour) return this->state_before.cur_colour < other.state_before.cur_colour; + if (this->state_before.prev_colour != other.state_before.prev_colour) return this->state_before.prev_colour < other.state_before.prev_colour; + return this->str < other.str; + } + }; + /** Item in the linecache */ + struct LineCacheItem { + /* Stuff that cannot be freed until the ParagraphLayout is freed */ + CharType buffer[DRAW_STRING_BUFFER]; ///< Accessed by both ICU's and our ParagraphLayout::nextLine. + FontMap runs; ///< Accessed by our ParagraphLayout::nextLine. + + FontState state_after; ///< Font state after the line. + ParagraphLayout *layout; ///< Layout of the line. + + LineCacheItem() : layout(NULL) {} + ~LineCacheItem() { delete layout; } + }; + typedef std::map LineCache; + static LineCache *linecache; + + static LineCacheItem &GetCachedParagraphLayout(const char *str, size_t len, const FontState &state); + + typedef SmallMap FontColourMap; + static FontColourMap fonts[FS_END]; + static Font *GetFont(FontSize size, TextColour colour); public: Layouter(const char *str, int maxw = INT32_MAX, TextColour colour = TC_FROMSTRING, FontSize fontsize = FS_NORMAL); - ~Layouter(); Dimension GetBounds(); + + static void ResetFontCache(FontSize size); + static void ResetLineCache(); + static void ReduceLineCache(); }; #endif /* GFX_LAYOUT_H */ diff --git a/src/openttd.cpp b/src/openttd.cpp index f81086c27d..c56697347e 100644 --- a/src/openttd.cpp +++ b/src/openttd.cpp @@ -61,6 +61,7 @@ #include "game/game_config.hpp" #include "town.h" #include "subsidy_func.h" +#include "gfx_layout.h" #include @@ -1308,6 +1309,8 @@ void StateGameLoop() ClearStorageChanges(false); + Layouter::ReduceLineCache(); + if (_game_mode == GM_EDITOR) { RunTileLoop(); CallVehicleTicks();