diff --git a/src/gfx_layout.cpp b/src/gfx_layout.cpp index 2be45e046d..8c43f7b2c0 100644 --- a/src/gfx_layout.cpp +++ b/src/gfx_layout.cpp @@ -35,7 +35,7 @@ /** Cache of ParagraphLayout lines. */ -Layouter::LineCache *Layouter::linecache; +std::unique_ptr Layouter::linecache; /** Cache of Font instances. */ Layouter::FontColourMap Layouter::fonts[FS_END]; @@ -390,19 +390,20 @@ Layouter::LineCacheItem &Layouter::GetCachedParagraphLayout(std::string_view str { if (linecache == nullptr) { /* Create linecache on first access to avoid trouble with initialisation order of static variables. */ - linecache = new LineCache(); + linecache = std::make_unique(4096); } - if (auto match = linecache->find(LineCacheQuery{state, str}); - match != linecache->end()) { - return match->second; + if (auto match = linecache->GetIfValid(LineCacheQuery{state, str}); + match != nullptr) { + return *match; } /* Create missing entry */ LineCacheKey key; key.state_before = state; key.str.assign(str); - return (*linecache)[std::move(key)]; + linecache->Insert(key, {}); + return *linecache->GetIfValid(key); } /** @@ -410,18 +411,7 @@ Layouter::LineCacheItem &Layouter::GetCachedParagraphLayout(std::string_view str */ void Layouter::ResetLineCache() { - if (linecache != nullptr) linecache->clear(); -} - -/** - * Reduce the size of linecache if necessary to prevent infinite growth. - */ -void Layouter::ReduceLineCache() -{ - if (linecache != nullptr) { - /* TODO LRU cache would be fancy, but not exactly necessary */ - if (linecache->size() > 4096) ResetLineCache(); - } + if (linecache != nullptr) linecache->Clear(); } /** diff --git a/src/gfx_layout.h b/src/gfx_layout.h index 1b0dc6705f..b37741a59b 100644 --- a/src/gfx_layout.h +++ b/src/gfx_layout.h @@ -10,11 +10,11 @@ #ifndef GFX_LAYOUT_H #define GFX_LAYOUT_H +#include "misc/lrucache.hpp" #include "fontcache.h" #include "gfx_func.h" #include "core/math_func.hpp" -#include #include /** @@ -24,12 +24,13 @@ struct FontState { FontSize fontsize; ///< Current font size. TextColour cur_colour; ///< Current text colour. - - std::stack> colour_stack; ///< Stack of colours to assist with colour switching. + std::vector colour_stack; ///< Stack of colours to assist with colour switching. FontState() : fontsize(FS_END), cur_colour(TC_INVALID) {} FontState(TextColour colour, FontSize fontsize) : fontsize(fontsize), cur_colour(colour) {} + auto operator<=>(const FontState &) const = default; + /** * Switch to new colour \a c. * @param c New colour to use. @@ -47,8 +48,8 @@ struct FontState { inline void PopColour() { if (colour_stack.empty()) return; - SetColour(colour_stack.top()); - colour_stack.pop(); + SetColour(colour_stack.back()); + colour_stack.pop_back(); } /** @@ -56,7 +57,7 @@ struct FontState { */ inline void PushColour() { - colour_stack.push(this->cur_colour); + colour_stack.push_back(this->cur_colour); } /** @@ -69,6 +70,27 @@ struct FontState { } }; +template struct std::hash> { + size_t operator()(const std::vector &vec) const + { + /* This is not an optimal hash algorithm but in most cases this is empty and therefore the same anyway. */ + return std::transform_reduce(std::begin(vec), std::end(vec), + std::hash{}(std::size(vec)), + [](const size_t &a, const size_t &b) -> size_t { return a ^ b; }, + [](const T &x) -> size_t { return std::hash{}(x); }); + } +}; + +template <> struct std::hash { + std::size_t operator()(const FontState &state) const noexcept + { + size_t h1 = std::hash{}(state.fontsize); + size_t h2 = std::hash{}(state.cur_colour); + size_t h3 = std::hash>{}(state.colour_stack); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } +}; + /** * Container with information about a font. */ @@ -149,20 +171,19 @@ class Layouter : public std::vector { std::string_view str; ///< Source string of the line (including colour and font size codes). }; - /** Comparator for std::map */ - struct LineCacheCompare { - using is_transparent = void; ///< Enable map queries with various key types + friend struct std::hash; + struct LineCacheHash; - /** Comparison operator for LineCacheKey and LineCacheQuery */ - template - bool operator()(const Key1 &lhs, const Key2 &rhs) const + struct LineCacheEqualTo { + using is_transparent = void; + + template + bool operator()(const Tlhs &lhs, const Trhs &rhs) const { - if (lhs.state_before.fontsize != rhs.state_before.fontsize) return lhs.state_before.fontsize < rhs.state_before.fontsize; - if (lhs.state_before.cur_colour != rhs.state_before.cur_colour) return lhs.state_before.cur_colour < rhs.state_before.cur_colour; - if (lhs.state_before.colour_stack != rhs.state_before.colour_stack) return lhs.state_before.colour_stack < rhs.state_before.colour_stack; - return lhs.str < rhs.str; + return lhs.state_before == rhs.state_before && lhs.str == rhs.str; } }; + public: /** Item in the linecache */ struct LineCacheItem { @@ -179,8 +200,8 @@ public: int cached_width = 0; ///< Width used for the cached layout. }; private: - typedef std::map LineCache; - static LineCache *linecache; + using LineCache = LRUCache; + static std::unique_ptr linecache; static LineCacheItem &GetCachedParagraphLayout(std::string_view str, const FontState &state); @@ -197,10 +218,25 @@ public: static void Initialize(); static void ResetFontCache(FontSize size); static void ResetLineCache(); - static void ReduceLineCache(); }; ParagraphLayouter::Position GetCharPosInString(std::string_view str, size_t pos, FontSize start_fontsize = FS_NORMAL); ptrdiff_t GetCharAtPosition(std::string_view str, int x, FontSize start_fontsize = FS_NORMAL); +template <> struct std::hash { + std::size_t operator()(const Layouter::LineCacheQuery &state) const noexcept + { + size_t h1 = std::hash{}(state.str); + size_t h2 = std::hash{}(state.state_before); + return h1 ^ (h2 << 1); + } +}; + +struct Layouter::LineCacheHash { + using is_transparent = void; + + std::size_t operator()(const Layouter::LineCacheKey &query) const { return std::hash{}(LineCacheQuery{query.state_before, query.str}); } + std::size_t operator()(const Layouter::LineCacheQuery &query) const { return std::hash{}(query); } +}; + #endif /* GFX_LAYOUT_H */ diff --git a/src/misc/lrucache.hpp b/src/misc/lrucache.hpp index 81ac0ae3a0..7267cfb128 100644 --- a/src/misc/lrucache.hpp +++ b/src/misc/lrucache.hpp @@ -15,17 +15,20 @@ /** * Size limited cache with a least recently used eviction strategy. + * @note If a comparator is provided, the lookup type is a std::map, otherwise std::unordered_map is used. * @tparam Tkey Type of the cache key. * @tparam Tdata Type of the cache item. */ -template +template , class Tequality = std::equal_to<>> class LRUCache { private: - typedef std::pair Tpair; - typedef typename std::list::iterator Titer; + using PairType = std::pair; + using StorageType = std::list; + using IteratorType = StorageType::iterator; + using LookupType = std::unordered_map; - std::list data; ///< Ordered list of all items. - std::unordered_map lookup; ///< Map of keys to items. + StorageType data; ///< Ordered list of all items. + LookupType lookup; ///< Map of keys to items. const size_t capacity; ///< Number of items to cache. @@ -41,7 +44,7 @@ public: * @param key The key to search. * @return True, if the key was found. */ - inline bool Contains(const Tkey key) + inline bool Contains(const Tkey &key) { return this->lookup.find(key) != this->lookup.end(); } @@ -51,11 +54,12 @@ public: * @param key Key under which the item should be stored. * @param item Item to insert. */ - void Insert(const Tkey key, Tdata &&item) + void Insert(const Tkey &key, Tdata &&item) { - if (this->Contains(key)) { + auto it = this->lookup.find(key); + if (it != this->lookup.end()) { /* Replace old value. */ - this->lookup[key]->second = std::move(item); + it->second->second = std::move(item); return; } @@ -85,7 +89,7 @@ public: * @return The item value. * @note Throws if item not found. */ - inline const Tdata &Get(const Tkey key) + inline const Tdata &Get(const Tkey &key) { auto it = this->lookup.find(key); if (it == this->lookup.end()) throw std::out_of_range("item not found"); @@ -94,6 +98,23 @@ public: return this->data.front().second; } + + /** + * Get an item from the cache. + * @param key The key to look up. + * @return The item value. + * @note Throws if item not found. + */ + inline Tdata *GetIfValid(const auto &key) + { + auto it = this->lookup.find(key); + if (it == this->lookup.end()) return nullptr; + + /* Move to front if needed. */ + this->data.splice(this->data.begin(), this->data, it->second); + + return &this->data.front().second; + } }; #endif /* LRUCACHE_HPP */ diff --git a/src/openttd.cpp b/src/openttd.cpp index a07b6b95c5..a87e1d0353 100644 --- a/src/openttd.cpp +++ b/src/openttd.cpp @@ -1234,8 +1234,6 @@ void StateGameLoop() PerformanceMeasurer framerate(PFE_GAMELOOP); PerformanceAccumulator::Reset(PFE_GL_LANDSCAPE); - Layouter::ReduceLineCache(); - if (_game_mode == GM_EDITOR) { BasePersistentStorageArray::SwitchMode(PSM_ENTER_GAMELOOP); RunTileLoop(); diff --git a/src/townname.cpp b/src/townname.cpp index b14d1128a9..aa264a6dba 100644 --- a/src/townname.cpp +++ b/src/townname.cpp @@ -137,10 +137,6 @@ bool GenerateTownName(Randomizer &randomizer, uint32_t *townnameparts, TownNames { TownNameParams par(_settings_game.game_creation.town_name); - /* This function is called very often without entering the gameloop - * in between. So reset layout cache to prevent it from growing too big. */ - Layouter::ReduceLineCache(); - /* Do not set i too low, since when we run out of names, we loop * for #tries only one time anyway - then we stop generating more * towns. Do not set it too high either, since looping through all