From d0eff986be39ba24cf83caf6e96de99416cb02a2 Mon Sep 17 00:00:00 2001
From: frosch <frosch@openttd.org>
Date: Sat, 6 Jul 2013 19:00:33 +0000
Subject: [PATCH] (svn r25570) -Add: cache for ParagraphLayouts.

---
 src/gfx_layout.cpp | 144 ++++++++++++++++++++++++++++++++-------------
 src/gfx_layout.h   |  40 ++++++++++++-
 src/openttd.cpp    |   3 +
 3 files changed, 145 insertions(+), 42 deletions(-)

diff --git a/src/gfx_layout.cpp b/src/gfx_layout.cpp
index cba5a7ddd9..01be1e0149 100644
--- a/src/gfx_layout.cpp
+++ b/src/gfx_layout.cpp
@@ -21,6 +21,9 @@
 #endif /* WITH_ICU */
 
 
+/** Cache of ParagraphLayout lines. */
+Layouter::LineCache Layouter::linecache;
+
 /** Cache of Font instances. */
 Layouter::FontColourMap Layouter::fonts[FS_END];
 
@@ -134,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);
 }
 
@@ -277,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.
@@ -300,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;
 
@@ -412,62 +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;
-
 	FontState state(colour, fontsize);
 	WChar c = 0;
 
 	do {
-		Font *f = GetFont(state.fontsize, state.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<const char **>(&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;
+		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<const char **>(&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);
 			}
-			f = GetFont(state.fontsize, state.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);
-		}
-		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);
+	} while (c != '\0');
 }
 
 /**
@@ -507,4 +535,40 @@ void Layouter::ResetFontCache(FontSize size)
 		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)
+{
+	LineCacheKey key;
+	key.state_before = state;
+	key.str.assign(str, len);
+	return linecache[key];
+}
+
+/**
+ * Clear line cache.
+ */
+void Layouter::ResetLineCache()
+{
+	linecache.clear();
+}
+
+/**
+ * Reduce the size of linecache if necessary to prevent infinite growth.
+ */
+void Layouter::ReduceLineCache()
+{
+	/* 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 ea9c34aae3..1215954cb4 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 <map>
+#include <string>
+
 #ifdef WITH_ICU
 #include "layout/ParagraphLayout.h"
 #define ICU_FONTINSTANCE : public LEFontInstance
@@ -32,6 +35,7 @@ struct FontState {
 	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) {}
 
 	/**
@@ -143,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 */
@@ -166,7 +171,36 @@ class Layouter : public AutoDeleteSmallVector<ParagraphLayout::Line *, 4> {
 	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.
+	/** 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<LineCacheKey, LineCacheItem> LineCache;
+	static LineCache linecache;
+
+	static LineCacheItem &GetCachedParagraphLayout(const char *str, size_t len, const FontState &state);
 
 	typedef SmallMap<TextColour, Font *> FontColourMap;
 	static FontColourMap fonts[FS_END];
@@ -177,6 +211,8 @@ public:
 	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 07124b84fe..90c5d56dbe 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 "linkgraph/linkgraphschedule.h"
@@ -1318,6 +1319,8 @@ void StateGameLoop()
 
 	ClearStorageChanges(false);
 
+	Layouter::ReduceLineCache();
+
 	if (_game_mode == GM_EDITOR) {
 		RunTileLoop();
 		CallVehicleTicks();