Improved fontmanager and textrenderer class with automatic cache cleanup

This commit is contained in:
2025-12-13 09:56:03 +08:00
parent 5b3e80f4fc
commit 1e2555a35b
5 changed files with 388 additions and 119 deletions

View File

@@ -1,44 +1,68 @@
#include "FontManager.h"
#include <stdexcept>
FontManager::FontManager() {
FontManager::FontManager() {
// 构造函数:初始化字体管理器
// m_fonts 映射表会在默认构造时初始化为空
}
FontManager::~FontManager() {
// 析构函数:清理所有加载的字体资源
for (auto& pair : m_fonts) {
// 关闭并释放每个字体资源
TTF_CloseFont(pair.second);
}
// 清空字体缓存表
m_fonts.clear();
}
TTF_Font* FontManager::loadFont(const std::string& fontID, int ptSize) {
// 构造字体文件的完整路径
std::string path = "assets/fonts/" + fontID;
std::string key = fontID + std::to_string(ptSize);
// 计算该字体在缓存中的唯一哈希键
size_t key = makeHash(fontID, ptSize);
// 检查字体是否已经被加载过
auto it = m_fonts.find(key);
// 检查字体是否已经加载
if (it != m_fonts.end()) {
// 字体已缓存,直接返回缓存的字体指针
return it->second;
}
TTF_Font* font = TTF_OpenFont(path.c_str(), ptSize);
// 从文件中加载字体
TTF_Font* font = TTF_OpenFont(path.c_str(), ptSize);
if (!font) {
throw std::runtime_error("无法加载字体: " + key);
// 字体加载失败,抛出异常
throw std::runtime_error("无法加载字体: " + fontID);
}
// 将新加载的字体存入缓存
m_fonts[key] = font;
return font;
}
TTF_Font* FontManager::getFont(const std::string& key, int ptSize) {
TTF_Font* FontManager::getFont(const std::string& fontID, int ptSize) {
// 计算该字体的哈希键
size_t key = makeHash(fontID, ptSize);
// 从字体缓存中查找该键对应的字体
auto it = m_fonts.find(key);
if (it != m_fonts.end()) {
// 字体已缓存,直接返回缓存的字体指针
return it->second;
}
return loadFont(key, ptSize);;
// 如果缓存中未找到自动调用loadFont加载字体
return loadFont(fontID, ptSize);
}
size_t FontManager::makeHash(const std::string& fontID, int ptSize) {
// 分别计算字体名称和大小的哈希值
size_t h1 = std::hash<std::string>{}(fontID);
size_t h2 = std::hash<int>{}(ptSize);
// 组合两个哈希值使用XOR运算和位左移组合两个独立的哈希值
// 这样可以确保不同的fontID或ptSize组合都会产生不同的哈希值
return h1 ^ (h2 << 1);
}

View File

@@ -1,26 +1,67 @@
/**
* @file FontManager.h
* @brief 字体加载和管理的实现使用SDL的ttf库扩展
* @author zhenyan121
* @date 2025-12-12
*/
#pragma once
#include <string>
#include <unordered_map>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
/**
* @class FontManager
* @brief 字体管理的类
*
* 用于加载字体,缓存字体
* 提供接口,用与获取字体缓存
*/
class FontManager {
public:
FontManager();
/**
* @brief 析构函数
* 清理已经加载的字体资源
*/
~FontManager();
// 加载字体(路径 + 字号)
// ptSize 为字号
/**
* @brief 加载字体
* 从字体文件中加载字体并将其缓存。不同大小的字体将被独立存储。
* @param fontID 字体文件名称的引用,不包含路径前缀
* @param ptSize 字体的磅值大小,不同的大小会产生不同的缓存键
* @return 返回加载字体的指针失败时抛出std::runtime_error异常
* @note 该函数会自动检查字体是否已缓存,如已缓存则直接返回缓存的字体
* @throw std::runtime_error 当字体文件无法打开时抛出异常
*/
TTF_Font* loadFont(const std::string& fontID, int ptSize);
// 获取已加载的字体
TTF_Font* getFont(const std::string& key, int ptSize);
/**
* @brief 获取字体
* 获取指定字体的指针。如果字体未被加载过将自动调用loadFont进行加载。
* @param fontID 字体文件名称的引用,不包含路径前缀
* @param ptSize 字体的磅值大小
* @return 返回获取到的字体指针,保证非空;失败时会抛出异常
* @note 该函数是获取字体的推荐方式,具有自动加载功能
* @throw std::runtime_error 当字体加载失败时抛出异常
*/
TTF_Font* getFont(const std::string& fontID, int ptSize);
private:
// 用哈希表存储字体
std::unordered_map<std::string, TTF_Font*> m_fonts;
std::unordered_map<size_t, TTF_Font*> m_fonts; ///< 字体缓存表使用哈希值作为键存储TTF_Font指针
/**
* @brief 计算组合哈希值
* 根据字体名称和大小计算唯一的哈希键用于缓存。采用黄金比例乘法混合算法。
* @param fontID 字体文件名称的引用
* @param ptSize 字体的磅值大小
* @return 返回计算得到的哈希值作为m_fonts字典的键
* @note 内部实现使用XOR运算和位移组合两个独立的哈希值
*/
size_t makeHash(const std::string& fontID, int ptSize);
};

View File

@@ -1,5 +1,6 @@
#include "TextRenderer.h"
#include <algorithm>
#include <vector>
TextRenderer::TextRenderer(SDL_Renderer* renderer, FontManager* fontManager) :
m_fontManager(fontManager),
m_renderer(renderer)
@@ -8,73 +9,105 @@ TextRenderer::TextRenderer(SDL_Renderer* renderer, FontManager* fontManager) :
}
TextRenderer::~TextRenderer() {
for (auto& pair : m_cache) {
SDL_DestroyTexture(pair.second.texture);
}
m_cache.clear();
clearCache();
}
void TextRenderer::renderText(const std::string& text, TextStyle style, int x, int y) {
auto key = style.hash();
std::pair<int, int> TextRenderer::getTextSize(const std::string& text, TextStyle style) {
auto key = makeHash(text, style);
auto it = m_cache.find(key);
// 查找缓存
if (it != m_cache.end()) {
// 更新最后访问时间
it->second.lastAccessTime = std::time(nullptr);
return {it->second.width, it->second.height};
}
// 创建并缓存纹理
CachedText cached = createAndCacheTexture(text, style);
if (!cached.texture) {
return {0, 0};
}
return {cached.width, cached.height};
}
void TextRenderer::renderText(const std::string& text, TextStyle style, int x, int y) {
auto key = makeHash(text, style);
auto it = m_cache.find(key);
// 查找缓存
if (it != m_cache.end()) {
// 更新最后访问时间
it->second.lastAccessTime = std::time(nullptr);
// 使用缓存的纹理, SDL_FRect为浮点数矩形
SDL_FRect dest = { static_cast<float>(x), static_cast<float>(y),
static_cast<float>(it->second.width),
static_cast<float>(it->second.height) };
SDL_FRect dest = { static_cast<float>(x), static_cast<float>(y),
static_cast<float>(it->second.width),
static_cast<float>(it->second.height) };
// 绘制材质 NULL 的含义:绘制整个纹理(从 (0,0) 到纹理的完整宽高) &dest 目标区域
SDL_RenderTexture(m_renderer, it->second.texture, NULL, &dest);
return;
}
// 创建新的纹理
TTF_Font* font = m_fontManager->getFont(style.fontID, style.fontSize);
if (!font) {
SDL_Log("错误:字体未找到 %s\n", style.fontID.c_str());
// 绘制材质 NULL 的含义:绘制整个纹理(从 (0,0) 到纹理的完整宽高) &dest 目标区域
SDL_RenderTexture(m_renderer, it->second.texture, NULL, &dest);
return;
}
// 创建文字表面
SDL_Surface* surface = TTF_RenderText_Solid(font, text.c_str(),text.length(), style.color);
if (!surface) {
printf("错误:无法创建文字表面\n");
return;
}
// 创建纹理
SDL_Texture* texture = SDL_CreateTextureFromSurface(m_renderer, surface);
if (!texture) {
SDL_Log("错误:无法创建纹理\n");
SDL_DestroySurface(surface);
return;
}
// 保存到缓存
CachedText cached;
cached.texture = texture;
cached.width = surface->w;
cached.height = surface->h;
m_cache[key] = cached;
// 创建并缓存纹理
CachedText cached = createAndCacheTexture(text, style);
if (!cached.texture) {
return;
}
// 渲染
SDL_FRect dest = { static_cast<float>(x), static_cast<float>(y),
static_cast<float>(surface->w),
static_cast<float>(surface->h) };
SDL_RenderTexture(m_renderer, texture, NULL, &dest);
// 清理表面
SDL_DestroySurface(surface);
static_cast<float>(cached.width),
static_cast<float>(cached.height) };
SDL_RenderTexture(m_renderer, cached.texture, NULL, &dest);
}
SDL_Texture* TextRenderer::createTextTexture(const std::string& text, const std::string& fontID, SDL_Color color) {
return nullptr;
TextRenderer::CachedText TextRenderer::createAndCacheTexture(const std::string& text, TextStyle style) {
CachedText result = {nullptr, 0, 0, std::time(nullptr)};
// 获取字体
TTF_Font* font = m_fontManager->getFont(style.fontID, style.fontSize);
if (!font) {
SDL_Log("错误:字体未找到 %s\n", style.fontID.c_str());
return result;
}
// 创建文字表面
SDL_Surface* surface = TTF_RenderText_Solid(font, text.c_str(),text.length(), style.color);
if (!surface) {
SDL_Log("错误:无法创建文字表面 '%s'\n", text.c_str());
return result;
}
// 创建纹理
SDL_Texture* texture = SDL_CreateTextureFromSurface(m_renderer, surface);
int width = surface->w;
int height = surface->h;
SDL_DestroySurface(surface); // 立即释放表面
if (!texture) {
SDL_Log("错误:无法创建纹理\n");
return result;
}
// 保存结果
result.texture = texture;
result.width = width;
result.height = height;
result.lastAccessTime = std::time(nullptr);
// 保存到缓存
auto key = makeHash(text, style);
m_cache[key] = result;
// 检查是否需要清理缓存
if (m_cache.size() > MAX_CACHE_SIZE) {
autoCleanCache();
}
return result;
}
@@ -85,3 +118,68 @@ std::string makeCacheKey(const std::string& text, const std::string& fontID, SDL
std::to_string(color.b) + "-" +
std::to_string(color.a);
}
size_t TextRenderer::makeHash(const std::string& text, TextStyle style) const {
// 使用组合哈希技术
size_t h1 = std::hash<std::string>{}(style.fontID);
size_t h2 = std::hash<std::string>{}(text);
size_t h3 = std::hash<int>{}(style.fontSize);
size_t h4 = std::hash<Uint8>{}(style.color.r);
size_t h5 = std::hash<Uint8>{}(style.color.g);
size_t h6 = std::hash<Uint8>{}(style.color.b);
size_t h7 = std::hash<Uint8>{}(style.color.a);
// 组合哈希(使用黄金比例乘法混合)
return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3) ^ (h5 << 4) ^ (h6 << 5) ^ (h7 << 6);
}
void TextRenderer::clearCache() {
for (auto& pair : m_cache) {
SDL_DestroyTexture(pair.second.texture);
}
m_cache.clear();
SDL_Log("文本缓存已清空\n");
}
void TextRenderer::autoCleanCache() {
// 如果缓存没有超过最大限制,则不进行清理
if (m_cache.size() <= MAX_CACHE_SIZE) {
return;
}
// 收集所有缓存项,按最后访问时间排序
std::vector<std::pair<size_t, time_t>> cacheItems;
cacheItems.reserve(m_cache.size());
for (const auto& pair : m_cache) {
cacheItems.emplace_back(pair.first, pair.second.lastAccessTime);
}
// 按最后访问时间从早到晚排序(最旧的在前)
std::sort(cacheItems.begin(), cacheItems.end(),
[](const auto& a, const auto& b) {
return a.second < b.second;
});
// 计算需要删除的数量
size_t itemsToRemove = m_cache.size() - MIN_CACHE_SIZE;
SDL_Log("缓存清理:删除 %zu / %zu 条目(当前缓存大小:%zu)\n",
itemsToRemove, m_cache.size(), m_cache.size());
// 删除最旧的缓存项
for (size_t i = 0; i < itemsToRemove && i < cacheItems.size(); ++i) {
auto it = m_cache.find(cacheItems[i].first);
if (it != m_cache.end()) {
SDL_DestroyTexture(it->second.texture);
m_cache.erase(it);
}
}
SDL_Log("缓存清理完成,当前缓存大小:%zu\n", m_cache.size());
}

View File

@@ -1,44 +1,167 @@
/**
* @file TextRenderer.h
* @brief 文本渲染器类,用于将带有特定样式的文本渲染到屏幕上
* @details 该类使用 SDL3 TTF 库进行文字渲染,并实现了纹理缓存机制以提高性能。
* 支持获取文本尺寸和渲染文本到指定位置。
* @author zhenyan121
* @date 2025-12-12
*/
#pragma once
#include <string>
#include <unordered_map>
#include <list>
#include <ctime>
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include "Textstyle.h"
#include "FontManager.h"
/**
* @class TextRenderer
* @brief 文本渲染器
* @details 负责管理文本渲染的生命周期,包括:
* - 文本纹理的创建和缓存
* - 文本尺寸的计算
* - 文本到指定屏幕位置的渲染
*
* 使用哈希缓存机制存储已渲染的文本纹理,避免重复创建相同的纹理。
*/
class TextRenderer {
private:
SDL_Renderer* m_renderer;
FontManager* m_fontManager;
// 缓存文字纹理
struct CachedText {
SDL_Texture* texture;
int width;
int height;
};
std::unordered_map<size_t, CachedText> m_cache;
// 创建材质
SDL_Texture* createTextTexture(const std::string& text, const std::string& fontID, SDL_Color color);
public:
/**
* @brief 构造函数
* @param renderer SDL 渲染器指针,用于创建纹理和执行渲染操作。不可为 nullptr
* @param fontManager 字体管理器指针,用于获取指定字体 ID 和大小的字体对象。不可为 nullptr
* @details 初始化文本渲染器,设置必要的 SDL 渲染器和字体管理器引用
*/
TextRenderer(SDL_Renderer* renderer, FontManager* fontManager);
/**
* @brief 析构函数
* @details 自动释放所有缓存中的 SDL 纹理资源,防止内存泄漏
*/
~TextRenderer();
/**
* @brief 获取文本渲染后的尺寸
* @param text 要计算尺寸的文本内容。支持任意 UTF-8 字符串
* @param style 文本样式,包含字体 ID、大小、颜色等信息
* @return 返回一个 pairfirst 为宽度像素second 为高度(像素);
* 如果渲染失败,返回 {0, 0}
* @details 该函数会检查缓存,如果文本纹理已存在则直接返回其尺寸;
* 否则创建新的纹理并缓存,然后返回尺寸。
* 这样可以避免重复渲染相同的文本。
* @note 返回的尺寸是矩形的宽和高
*/
std::pair<int, int> getTextSize(const std::string& text, TextStyle style);
//渲染文本
/**
* @brief 将文本渲染到指定位置
* @param text 要渲染的文本内容。支持任意 UTF-8 字符串
* @param style 文本样式,包含字体 ID、大小、颜色等信息
* @param x 文本左上角的 X 坐标(像素)
* @param y 文本左上角的 Y 坐标(像素)
* @details 该函数会检查缓存,如果文本纹理已存在则直接使用;
* 否则创建新的纹理、缓存并渲染。
* 使用浮点数坐标进行渲染,支持亚像素精度。
* @note (x, y) 为文本左上角的顶点坐标
*/
void renderText(const std::string& text, TextStyle style, int x, int y);
};
/**
* @brief 为文本和样式生成哈希值
* @param text 文本内容
* @param style 文本样式
* @return 返回组合哈希值,用于缓存键的生成
* @details 利用文本内容、字体 ID、字体大小和颜色信息生成唯一的哈希值
* 用于在缓存中快速查找已渲染的文本纹理
* @see m_cache
*/
size_t makeHash(const std::string& text, TextStyle style) const;
/**
* @brief 手动清理所有缓存
* @details 释放所有缓存中的纹理资源,完全清空缓存表
* @note 清理后任何文本都需要重新渲染
*/
void clearCache();
private:
/** @brief SDL 渲染器指针,用于创建纹理和执行 2D 渲染操作 */
SDL_Renderer* m_renderer;
/** @brief 字体管理器指针,用于管理和获取字体资源 */
FontManager* m_fontManager;
/**
* @struct CachedText
* @brief 缓存的文本纹理信息
* @details 存储已渲染文本的纹理及其尺寸信息,用于快速重用
*/
struct CachedText {
/** @brief 文本渲染后生成的 SDL 纹理指针 */
SDL_Texture* texture;
/** @brief 文本纹理的宽度(像素) */
int width;
/** @brief 文本纹理的高度(像素) */
int height;
/** @brief 该缓存项的最后访问时间戳(秒) */
time_t lastAccessTime;
};
/**
* @brief 文本纹理缓存容器
* @details 使用无序哈希表存储已渲染的文本纹理。
* 键是由文本内容、字体信息等生成的哈希值(通过 makeHash 生成);
* 值是对应的 CachedText 结构体,包含纹理指针和尺寸信息。
*
* 缓存机制的优势:
* - 避免重复渲染相同的文本
* - 提高渲染性能
* - 加速尺寸查询
* @see CachedText, makeHash()
*/
std::unordered_map<size_t, CachedText> m_cache;
/** @brief 最大缓存条目数,超过此数量会触发自动清理 */
static constexpr size_t MAX_CACHE_SIZE = 256;
/** @brief 缓存清理时保留的最少条目数(清理时会保留最常用的条目) */
static constexpr size_t MIN_CACHE_SIZE = 128;
/** @brief 自动清理缓存(当缓存超过最大限制时调用) */
void autoCleanCache();
/**
* @brief 创建文本纹理(暂未使用)
* @param text 文本内容
* @param fontID 字体 ID
* @param color 文本颜色
* @return 返回创建的纹理指针,创建失败时返回 nullptr
* @deprecated 该函数已被 createAndCacheTexture() 取代
*/
//SDL_Texture* createTextTexture(const std::string& text, const std::string& fontID, SDL_Color color);
/**
* @brief 创建并缓存文本纹理
* @param text 文本内容
* @param style 文本样式
* @return 返回 CachedText 结构体,包含创建的纹理和尺寸信息;
* 创建失败时返回空结构体texture 为 nullptr
* @details 该函数执行以下步骤:
* 1. 从字体管理器获取指定字体
* 2. 使用 TTF 库将文本渲染为表面Surface
* 3. 将表面转换为 SDL 纹理
* 4. 立即释放表面以节省内存
* 5. 将纹理和尺寸信息存储到缓存中
* 6. 返回结果
* @see CachedText, m_cache
*/
CachedText createAndCacheTexture(const std::string& text, TextStyle style);
};

View File

@@ -10,9 +10,9 @@
* 用于统一管理文本外观
*/
struct TextStyle {
std::string fontID; ///< 字体标识符在FontManager中注册的ID
int fontSize = 16; ///< 字体大小(像素)
SDL_Color color = {255, 255, 255, 255}; ///< 文本颜色RGBA
std::string fontID = "SourceHanSansSC-Regular.otf"; ///< 字体标识符在FontManager中注册的ID
int fontSize = 24; ///< 字体大小(像素)
SDL_Color color = {0, 0, 0, 255}; ///< 文本颜色RGBA
/**
* @brief 比较两个样式是否相等(用于缓存查找)
@@ -27,21 +27,4 @@ struct TextStyle {
color.b == other.color.b &&
color.a == other.color.a;
}
/**
* @brief 计算样式的哈希值用于unordered_map
* @return size_t 哈希值
*/
size_t hash() const {
// 使用组合哈希技术
size_t h1 = std::hash<std::string>{}(fontID);
size_t h2 = std::hash<int>{}(fontSize);
size_t h3 = std::hash<Uint8>{}(color.r);
size_t h4 = std::hash<Uint8>{}(color.g);
size_t h5 = std::hash<Uint8>{}(color.b);
size_t h6 = std::hash<Uint8>{}(color.a);
// 组合哈希(使用黄金比例乘法混合)
return h1 ^ (h2 << 1) ^ (h3 << 2) ^ (h4 << 3) ^ (h5 << 4) ^ (h6 << 5);
}
};