1
0
Fork 0

Codechange: Use LRUCache for layouted LineCache.

This avoids needing to manually manage the size of the cache.
pull/10004/merge
Peter Nelson 2025-05-04 09:40:00 +01:00 committed by Peter Nelson
parent 8c4f8af66e
commit 9735fbbaa1
5 changed files with 94 additions and 53 deletions

View File

@ -35,7 +35,7 @@
/** Cache of ParagraphLayout lines. */
Layouter::LineCache *Layouter::linecache;
std::unique_ptr<Layouter::LineCache> 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<LineCache>(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();
}
/**

View File

@ -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 <stack>
#include <string_view>
/**
@ -24,12 +24,13 @@
struct FontState {
FontSize fontsize; ///< Current font size.
TextColour cur_colour; ///< Current text colour.
std::stack<TextColour, std::vector<TextColour>> colour_stack; ///< Stack of colours to assist with colour switching.
std::vector<TextColour> 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 <typename T> struct std::hash<std::vector<T>> {
size_t operator()(const std::vector<T> &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<size_t>{}(std::size(vec)),
[](const size_t &a, const size_t &b) -> size_t { return a ^ b; },
[](const T &x) -> size_t { return std::hash<T>{}(x); });
}
};
template <> struct std::hash<FontState> {
std::size_t operator()(const FontState &state) const noexcept
{
size_t h1 = std::hash<FontSize>{}(state.fontsize);
size_t h2 = std::hash<TextColour>{}(state.cur_colour);
size_t h3 = std::hash<std::vector<TextColour>>{}(state.colour_stack);
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
/**
* Container with information about a font.
*/
@ -149,20 +171,19 @@ class Layouter : public std::vector<const ParagraphLayouter::Line *> {
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<Layouter::LineCacheQuery>;
struct LineCacheHash;
/** Comparison operator for LineCacheKey and LineCacheQuery */
template <typename Key1, typename Key2>
bool operator()(const Key1 &lhs, const Key2 &rhs) const
struct LineCacheEqualTo {
using is_transparent = void;
template <typename Tlhs, typename Trhs>
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<LineCacheKey, LineCacheItem, LineCacheCompare> LineCache;
static LineCache *linecache;
using LineCache = LRUCache<LineCacheKey, LineCacheItem, LineCacheHash, LineCacheEqualTo>;
static std::unique_ptr<LineCache> 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<Layouter::LineCacheQuery> {
std::size_t operator()(const Layouter::LineCacheQuery &state) const noexcept
{
size_t h1 = std::hash<std::string_view>{}(state.str);
size_t h2 = std::hash<FontState>{}(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<Layouter::LineCacheQuery>{}(LineCacheQuery{query.state_before, query.str}); }
std::size_t operator()(const Layouter::LineCacheQuery &query) const { return std::hash<Layouter::LineCacheQuery>{}(query); }
};
#endif /* GFX_LAYOUT_H */

View File

@ -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 <class Tkey, class Tdata>
template <class Tkey, class Tdata, class Thash = std::hash<Tkey>, class Tequality = std::equal_to<>>
class LRUCache {
private:
typedef std::pair<Tkey, Tdata> Tpair;
typedef typename std::list<Tpair>::iterator Titer;
using PairType = std::pair<Tkey, Tdata>;
using StorageType = std::list<PairType>;
using IteratorType = StorageType::iterator;
using LookupType = std::unordered_map<Tkey, IteratorType, Thash, Tequality>;
std::list<Tpair> data; ///< Ordered list of all items.
std::unordered_map<Tkey, Titer> 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 */

View File

@ -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();

View File

@ -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