缓存系统
一、缓存体系概览
TGFX 的多级缓存架构针对图形渲染中存在的核心性能瓶颈进行了系统化设计:
- GPU 资源创建与销毁开销巨大:纹理、缓冲区等 GPU 资源的创建和销毁涉及驱动层调用,耗时显著
- 文字渲染性能瓶颈突出:字形光栅化和纹理上传构成文字渲染的主要性能开销
- 内存分配碎片化问题:频繁的小对象分配导致内存碎片,破坏数据局部性
- 跨线程资源管理复杂性:渲染线程与业务线程间需要安全、高效地传递资源所有权
┌───────────────────────────────────────────────────────────────────────┐
│ TGFX 多级缓存架构 │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
│ │ ResourceCache │ │ GlobalCache │ │ AtlasStrikeCache │ │
│ │ (GPU资源缓存) │ │ (全局静态资源) │ │ (文字图集缓存) │ │
│ │ │ │ │ │ │ │
│ │ • 纹理 │ │ • Shader 程序 │ │ • 字形位图 │ │
│ │ • 缓冲区 │ │ • 渐变纹理 │ │ • 图集纹理 │ │
│ │ • 渲染目标 │ │ • 索引缓冲区 │ │ • Plot 管理 │ │
│ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ BlockAllocator │ │
│ │ (内存池分配器) │ │
│ │ • 绘制命令对象 • 顶点数据 • 实例数据 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ReturnQueue │ │
│ │ (跨线程资源回收) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
二、GPU 资源缓存(ResourceCache)
2.1 设计背景与挑战
GPU 资源(包括纹理、缓冲区、渲染目标)的创建与销毁涉及驱动层调用,开销显著:
| 操作 | 典型耗时区间 | 主要影响因素 |
|---|---|---|
| 纹理创建 | 0.5-2ms | 涉及显存分配、格式转换、驱动层命令提交 |
| 纹理销毁 | 0.1-0.5ms | 需等待 GPU 完成使用,避免使用后释放 |
| Buffer 创建 | 0.1-0.5ms | 显存分配与 CPU 端映射开销 |
若每帧都创建和销毁资源,会引发以下问题:
- 帧率不稳定:资源创建耗时不可预测,导致帧间延迟波动
- 显存碎片化:频繁分配/释放产生内存碎片,降低显存利用率
- CPU/GPU 同步开销:销毁前需等待 GPU 完成使用,增加同步等待时间
因此,ResourceCache 的核心设计目标是:通过复用 GPU 资源,将创建/销毁开销分摊至多帧,降低单帧性能波动。
2.2 核心设计理念与双 Key 机制
ResourceCache 面临的核心设计挑战是:如何平衡"精确匹配"与"规格复用"这两种不同的资源查找需求?
场景 1:图片纹理渲染
- 图片 A 的纹理只能供图片 A 使用,需要基于内容身份进行精确匹配
场景 2:离屏渲染目标
- 1024×1024 RGBA 的 FBO 可供任意同规格的离屏渲染使用,只需基于尺寸/格式进行规格匹配
TGFX 采用双 Key 机制解决这一设计矛盾:
┌─────────────────────────────────────────────────────────────────┐
│ ResourceCache │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ScratchKey │ │ UniqueKey │ │
│ │ (规格匹配) │ │ (身份匹配) │ │
│ │ │ │ │ │
│ │ 多对多:同规格资源 │ │ 一对一:精确绑定 │ │
│ │ 可互换使用 │ │ 同一资源多处共享 │ │
│ │ │ │ │ │
│ │ 适用:临时 Buffer │ │ 适用:图片纹理 │ │
│ │ 离屏渲染目标 │ │ 路径缓存 │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ 关键规则:UniqueKey 优先级高于 ScratchKey │
│ 资源设置 UniqueKey 后,ScratchKey 自动失效 │
└─────────────────────────────────────────────────────────────────┘
这一设计实现了资源查找的灵活性与精确性的平衡:对于可互换使用的临时资源使用 ScratchKey 进行规格匹配,对于内容敏感的资源使用 UniqueKey 进行精确匹配。
2.3 ScratchKey:规格匹配与复用机制
设计原理:ScratchKey 本质上是资源"规格"的哈希值,只要规格相同,资源就可以互换使用,实现了规格层面的资源共享。
使用 ScratchKey 的资源类型:
| 资源类 | ScratchKey 组成 | 典型使用场景 |
|---|---|---|
DefaultTextureView | 类型标识 + 宽度 + 高度 + 格式 + mipmap 标记 | 临时纹理、离屏渲染纹理 |
TextureRenderTarget | 类型标识 + 宽度 + 高度 + 格式 + 采样数 + mipmap 标记 | 离屏渲染目标(FBO) |
BufferResource | 类型标识 + 大小 + 使用标志 | 顶点缓冲、索引缓冲、Uniform缓冲 |
关键设计要点:
- 类型标识唯一性:每个资源类通过
UniqueID::Next()生成唯一的类型标识,确保不同类型资源的 ScratchKey 不会冲突 - 位压缩优化:
format和mipmapped使用位或运算压缩到同一个uint32_t,有效节省 Key 空间 - adopted 参数控制:只有当资源被"接管"(adopted=true)时才设置 ScratchKey,外部持有的资源不参与复用,避免误用
2.4 UniqueKey:精确身份匹配机制
设计原理:UniqueKey 是资源"身份"的唯一标识符,一个 Key 只能绑定一个特定资源,用于内容绑定场景,实现精确的资源身份匹配。
使用 UniqueKey 的资源类型:
| 资源类/场景 | UniqueKey 来源 | 典型使用场景 |
|---|---|---|
RasterizedImage → TextureView | LazyUniqueKey + mipmapped 标志 | 图片纹理缓存,同一图片在多个位置绘制时共享同一纹理 |
PathRef → 路径三角化缓存 | LazyUniqueKey | 矢量路径的三角化结果缓存,避免重复三角化计算 |
PathShape | PathRef::GetUniqueKey(path) | Shape 的缓存 Key,用于形状数据的复用 |
| Clip 蒙版纹理 | PathRef::GetUniqueKey(clip) + AA 标志 | 裁剪路径的蒙版纹理,支持抗锯齿效果 |
// UniqueKey 生成示例(RasterizedImage)
UniqueKey RasterizedImage::getTextureKey(float cacheScale) const {
auto textureKey = uniqueKey.get(); // LazyUniqueKey 延迟获取
if (hasMipmaps()) {
static const uint32_t MipmapFlag = UniqueID::Next();
textureKey = UniqueKey::Append(textureKey, &MipmapFlag, 1); // 追加 mipmap 标志
}
return textureKey;
}
// 路径的 UniqueKey 生成
UniqueKey PathRef::GetUniqueKey(const Path& path) {
return path.pathRef->uniqueKey.get(); // 每个 PathRef 有独立的 LazyUniqueKey
}
关键设计要点:
- LazyUniqueKey 延迟初始化机制:通过
LazyUniqueKey延迟获取 UniqueKey,避免未实际使用的资源占用 Key 空间,提高存储效率 - UniqueDomain 引用计数管理:UniqueKey 内部持有
UniqueDomain*指针,通过引用计数管理生命周期,当所有引用释放时 Key 自动失效 - Key 追加与派生机制:通过
UniqueKey::Append()在基础 Key 上追加标志位(如 mipmapped、抗锯齿等),派生出相关但不同的 Key,支持灵活的变体管理
2.5 缓存资源管理机制
ResourceCache 的资源管理涉及四个关键机制:状态追踪、淘汰策略、引用保护和资源降级。
2.5.1 双链表状态管理模型
ResourceCache 需要准确追踪资源的"使用状态"以决定是否可以清理。核心设计问题:如何高效判断一个资源是否正在被使用?
TGFX 的解决方案:采用链表位置表示资源状态
┌───────────────────────────────────────────────────────────────────┐
│ nonpurgeableResources │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 正在被使用的资源(shared_ptr 存活) │ │
│ │ 不参与 LRU 淘汰 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ shared_ptr 引用归零 │ ReturnQueue 通知 │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ purgeableResources │ │
│ │ 空闲资源,按 LRU 顺序排列 │ │
│ │ 可被复用 或 被淘汰 │ │
│ └─────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
2.5.2 LRU与帧过期结合的分层淘汰策略
资源清理面临的核心权衡:清理过于激进会导致频繁的资源重建开销,而清理过于保守则会导致内存溢出风险。
TGFX 采用分层淘汰策略来平衡这一矛盾:
┌──────────────────────────────────────────────────────────────────┐
│ 淘汰优先级(从高到低) │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 第 1 层:超出内存限制 (maxBytes = 512MB) │
│ │ │
│ ▼ │
│ 第 2 层:超过帧过期时间 (expirationFrames = 120帧) │
│ │ │
│ ▼ │
│ 第 3 层:Scratch 资源快速过期 (SCRATCH_EXPIRATION_FRAMES = 2帧) │
│ │
└──────────────────────────────────────────────────────────────────┘
为什么 Scratch 资源过期更快(2 帧 vs 120 帧)?
- Scratch 资源是"规格复用"的,同规格资源可能积累很多
- 2 帧足以覆盖帧间波动(如某帧需要更多临时 Buffer)
- 快速过期避免无用 Scratch 资源占用内存
2.5.3 双重引用计数:解决帧间资源保护问题
设计背景:UniqueKey 资源(如图片纹理)在帧间会变成 purgeable 状态,但不应该被立即淘汰——下一帧可能仍需要使用。
核心挑战:如何准确区分"暂时无人使用"和"永久不再需要"的资源?
TGFX 的解决方案:采用两层引用计数机制
┌─────────────────────────────────────────────────────────────────┐
│ 第 1 层:shared_ptr 引用计数 → "当前帧谁在用" │
│ │
│ • GPU 资源的 shared_ptr 随 DrawOp 等临时对象存活 │
│ • 帧结束时 DrawOp 销毁 → shared_ptr 释放 → 资源变 purgeable │
│ • 作用:控制资源在哪个链表 │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 第 2 层:UniqueKey.useCount → "将来谁可能用" │
│ │
│ • ImageSource 等持久对象持有 UniqueKey 的拷贝 │
│ • ImageSource 存活 → useCount > 1 → hasExternalReferences=true │
│ • 作用:保护 purgeable 资源不被淘汰 │
│ │
└─────────────────────────────────────────────────────────────────┘
典型流程:
帧 1: 绘制图片 A
└── findUniqueResource(keyA) → 创建纹理 → shared_ptr 存活
└── 纹理在 nonpurgeableResources
└── useCount = 2 (ImageSource + Resource 各持有一份 UniqueKey)
帧 1 结束:
└── shared_ptr 随 DrawOp 销毁 → 纹理变 purgeable
└── 纹理进入 purgeableResources
└── 但 useCount = 2 → hasExternalReferences = true → 受保护
帧 2: 又绘制图片 A
└── findUniqueResource(keyA) → 命中缓存 → 纹理捞回 nonpurgeable
2.5.4 UniqueKey 降级策略:最大化资源复用价值
设计问题:当 UniqueKey 资源长期未使用且触发 LRU 淘汰时,直接删除是否是最优选择?
关键洞察:一个纹理通常同时具备两个"身份":
- UniqueKey(身份标识):"我是图片 A 的纹理"
- ScratchKey(规格标识):"我是 1024×1024 RGBA 纹理"
降级策略:与其完全删除资源,不如只移除身份标识、保留规格属性,实现资源价值的最大化利用
// purgeResourcesByLRU 中的降级逻辑
if (resource->hasExternalReferences() && !resource->scratchKey.empty()) {
removeUniqueKey(resource); // 移除身份
resource->lastUsedTime = currentFrameTime; // 重置使用时间
AddToList(purgeableResources, resource); // 续命
continue; // 不删除
}
降级后的资源状态:
- 失去 UniqueKey → 无法通过
findUniqueResource(keyA)找到 - 保留 ScratchKey → 可通过
findScratchResource(1024x1024_RGBA)复用 - 避免了 GPU 资源重建开销
完整的淘汰决策树:
资源触发 LRU 淘汰
│
├─ hasExternalReferences = false → 直接删除
│
└─ hasExternalReferences = true
│
├─ scratchResourceOnly = true(Scratch 过期清理阶段)→ 跳过(不淘汰)
│ (即使 Scratch 资源超过2帧未使用,只要外部仍持有引用就保留)
│
└─ scratchResourceOnly = false(内存超限/帧过期清理阶段)
│
├─ 有 ScratchKey → 降级(移除 UniqueKey,保留 ScratchKey,
│ 更新 lastUsedTime 并移到 LRU 末尾,获得"第二次机会")
│
└─ 无 ScratchKey → 直接删除(无法降级,只能释放)
注:
purgeResourcesByLRU内部有一个scratchResourceOnly参数,当它为true时(即 Scratch 快速过期阶段),对于有外部引用(hasExternalReferences() == true)的资源会直接跳过,不执行淘汰也不执行降级。这确保了即便 Scratch 资源超过2帧未使用,只要外部持有者(如ImageSource)仍然关心该资源,就不会被清除。降级逻辑只在内存超限或帧过期触发的清理路径中生效。对于
scratchResourceOnly = false且hasExternalReferences() = true但 没有 ScratchKey 的资源:由于无法降级为 Scratch 资源(没有规格键可用于后续复用),代码不会进入降级分支,而是直接执行removeResource()删除。这类资源通常是纯 UniqueKey 资源(如通过assignUniqueKey显式绑定但本身没有 ScratchKey 的资源),失去 UniqueKey 后没有复用价值,因此直接释放是合理的。
2.6 ResourceCache 架构总览
┌─────────────────────────────────────────────────────────────────────┐
│ ResourceCache │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ scratchKeyMap │ │ uniqueKeyMap │ │
│ │ (规格 → 资源列表) │ │ (身份 → 单个资源) │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ └────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ nonpurgeableResources ◄──────────────────────────────────┐ │ │
│ │ (正在使用) │ │ │
│ └────────────────────────────┬──────────────────────────────┼─┘ │
│ │ shared_ptr 归零 │ │
│ ▼ │ │
│ ┌────────────────────────────────────────────────────────┐ │ │
│ │ purgeableResources (LRU 顺序) │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
│ │ │最新 │→│ │→│ │→│最旧 │ → 淘汰/降级 │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │ │
│ └────────────────────────────────────────────────────────┘ │ │
│ │ refResource() │ │
│ └──────────────────────────────┘ │
│ │
│ 淘汰策略: │
│ • 超出 512MB → 从 LRU 尾部开始清理 │
│ • 超过 120 帧未使用 → 淘汰 │
│ • Scratch 资源 2 帧未使用 → 淘汰 │
│ • UniqueKey 资源触发淘汰但有外部引用 → 降级为 Scratch │
│ │
└─────────────────────────────────────────────────────────────────────┘
三、全局静态缓存(GlobalCache)
3.1 设计目标与定位
GlobalCache 管理与 ResourceCache 性质不同的 GPU 资源,这些资源在 Context 生命周期内需要持续存活。与 ResourceCache 的核心区别在于:GlobalCache 中的资源不参与帧过期淘汰,而是采用永久缓存或固定容量上限的 LRU 淘汰策略:
- Shader 程序缓存:已编译的 GPU 渲染管线,避免重复的 Shader 编译开销
- 渐变纹理缓存:复杂渐变色生成的纹理,避免重复生成计算
- 索引缓冲区缓存:各类几何图元的索引模板,实现全局共享
- Uniform Buffer 管理:采用三缓冲机制,消除 CPU/GPU 同步等待
- 静态资源缓存:通用键值存储,维护 Context 级别的持久资源
3.2 与 ResourceCache 的分工设计
GlobalCache 与 ResourceCache 的职责划分体现了 TGFX 的核心设计原则:按照资源更新频率和复用模式进行分层管理。
| 对比维度 | ResourceCache | GlobalCache |
|---|---|---|
| 管理对象 | 动态 GPU 资源(纹理、缓冲区等) | 静态/高频复用对象(渲染管线、索引模板等) |
| 生命周期 | 采用 LRU 淘汰、帧过期机制 | 随 Context 存在,按固定数量上限 LRU 或永久驻留 |
| 淘汰策略 | 基于帧计数 + 内存上限的渐进式淘汰 | Program/Gradient 有数量上限 LRU,其余永久驻留 |
| 内存计量 | 计入 maxBytes 限制 | 不计入缓存限制 |
| Key 类型 | ScratchKey / UniqueKey | BytesKey / UniqueKey |
| 所属成员 | Context::_resourceCache | Context::_globalCache |
- ResourceCache 管理"内容敏感"的资源(图片纹理、顶点缓冲等),这类资源内容会频繁变化,需要精细的淘汰策略
- GlobalCache 管理"结构性"的资源(渲染管线、索引模板、Uniform 缓冲区等),这些是渲染引擎的基础设施,变化较少但创建成本极高
3.3 Shader 程序缓存
// src/gpu/GlobalCache.cpp(文件级 static 常量,非类成员)
static constexpr size_t MAX_PROGRAM_COUNT = 128; // 最多缓存 128 个程序
// src/gpu/GlobalCache.h
class GlobalCache {
private:
std::list<Program*> programLRU = {}; // LRU 链表
BytesKeyMap<std::shared_ptr<Program>> programMap = {}; // Key -> Program 映射
};
Program 的内部结构:
// src/gpu/Program.h
class Program {
private:
BytesKey programKey = {}; // 缓存 Key
std::list<Program*>::iterator cachedPosition; // LRU 链表位置
std::shared_ptr<RenderPipeline> pipeline = nullptr; // 编译好的渲染管线
std::unique_ptr<UniformData> vertexUniformData = nullptr; // 顶点阶段 Uniform
std::unique_ptr<UniformData> fragmentUniformData = nullptr; // 片段阶段 Uniform
};
programKey 的构成:
Program 的缓存 Key 由渲染管线的所有关键参数组合而成:
// src/gpu/ProgramInfo.cpp - getProgram()
BytesKey programKey = {};
geometryProcessor->computeProcessorKey(context, &programKey); // 几何处理器的 Key
for (const auto& processor : fragmentProcessors) {
processor->computeProcessorKey(context, &programKey); // 所有片段处理器的 Key
}
if (xferProcessor != nullptr) {
xferProcessor->computeProcessorKey(context, &programKey); // 混合传输处理器的 Key
}
programKey.write(static_cast<uint32_t>(blendMode)); // 混合模式
programKey.write(static_cast<uint32_t>(getOutputSwizzle().asKey())); // 输出通道重排
programKey.write(static_cast<uint32_t>(cullMode)); // 裁剪模式
programKey.write(static_cast<uint32_t>(renderTarget->format())); // 渲染目标格式
programKey.write(static_cast<uint32_t>(renderTarget->sampleCount())); // MSAA 采样数
每个 Processor 的 computeProcessorKey() 是虚函数,由 Processor 基类定义,各子类各自实现,将自身的特征参数写入 BytesKey。
缓存流程:
1. 用 GeometryProcessor + FragmentProcessors + XferProcessor + BlendMode 等
计算出一个 BytesKey(programKey)
2. globalCache->findProgram(programKey)
→ 命中:提升到 LRU 头部,返回
→ 未命中:ProgramBuilder::CreateProgram() 编译新程序
3. globalCache->addProgram(programKey, program)
→ push_front 到 LRU
→ 超过 128 个时,pop_back 淘汰最久未用的
设计要点:
- BytesKey 匹配:使用渲染管线所有特征参数的组合哈希查找,相同的 Processor 组合 + 混合模式 + 渲染目标格式 → 同一个 Program
- LRU 淘汰:超过 128 个程序时淘汰最久未用的
- 编译一次复用多次:相同 Shader 组合只编译一次
- 为什么不放在 ResourceCache? Program(渲染管线)不是 GPU 纹理/缓冲这类"显存资源",而是 Shader 编译产物。它不占 GPU 显存的"资源池",不适用 ScratchKey/UniqueKey 的复用模型。且 Program 创建代价极高(Shader 编译),销毁代价低,适合用简单的固定数量上限 LRU 管理
3.4 渐变纹理缓存
// src/gpu/GlobalCache.cpp(文件级 static 常量,非类成员)
static constexpr size_t MAX_NUM_CACHED_GRADIENT_BITMAPS = 32; // 最多 32 个渐变纹理
// src/gpu/GlobalCache.h
struct GradientTexture {
std::shared_ptr<TextureProxy> textureProxy = nullptr;
BytesKey gradientKey = {};
std::list<GradientTexture*>::iterator cachedPosition = {};
};
// GlobalCache 成员(注意:gradientTextures 使用 unique_ptr 独占所有权)
std::list<GradientTexture*> gradientLRU = {};
BytesKeyMap<std::unique_ptr<GradientTexture>> gradientTextures = {};
Key 组成:
// src/gpu/GlobalCache.cpp
std::shared_ptr<TextureProxy> GlobalCache::getGradient(const Color* colors,
const float* positions, int count) {
BytesKey bytesKey = {};
for (int i = 0; i < count; ++i) {
bytesKey.write(colors[i].red);
bytesKey.write(colors[i].green);
bytesKey.write(colors[i].blue);
bytesKey.write(colors[i].alpha);
bytesKey.write(positions[i]);
}
// 查找或创建渐变纹理...
}
设计要点:
- 颜色 + 位置 作为 Key:相同渐变参数复用同一纹理
- LRU 淘汰:超过 32 个时淘汰最久未用的
- 1D 纹理:渐变纹理宽度固定,颜色沿 U 方向采样
- 触发条件:当渐变色挡数超过
UnrolledBinaryGradientColorizer能处理的上限时(>8 个区间 / >16 个色挡),退化为纹理采样方案,此时通过globalCache->getGradient()获取缓存纹理 - 为什么限制 32 个? 渐变纹理本身很小(通常是 1D 窄条纹理),但种类可能很多。32 个足以覆盖大部分 UI 场景的渐变复用
3.5 索引缓冲区缓存
GlobalCache 缓存各种图元绘制所需的索引缓冲区,共 13 种,全部是懒加载 + 永不销毁:
┌─────────────── 填充矩形 ───────────────┐
│ aaQuadIndexBuffer (AA 四边形) │ 8 顶点/矩形
│ nonAAQuadIndexBuffer (非AA 四边形) │ 4 顶点/矩形
└─────────────────────────────────────────┘
┌─────────────── 圆角矩形 ───────────────┐
│ rRectFillIndexBuffer (AA 填充) │ 16 顶点/矩形
│ rRectStrokeIndexBuffer (AA 描边) │ 16 顶点/矩形
│ nonAARRectIndexBuffer (非AA) │ 4 顶点/矩形
└─────────────────────────────────────────┘
┌─────────── 矩形描边 (3种接头) ──────────┐
│ aaRectMiterStrokeIndexBuffer │ 16 顶点/矩形
│ aaRectBevelStrokeIndexBuffer │ 24 顶点/矩形
│ aaRectRoundStrokeIndexBuffer │ 24 顶点/矩形
│ nonAARectMiterStrokeIndexBuffer │ 8 顶点/矩形
│ nonAARectBevelStrokeIndexBuffer │ 12 顶点/矩形
│ nonAARectRoundStrokeIndexBuffer │ 20 顶点/矩形
└─────────────────────────────────────────┘
┌─────────────── Hairline ────────────────┐
│ hairlineLineIndexBuffer (细线-直线) │ 6 顶点/线段
│ hairlineQuadIndexBuffer (细线-曲线) │ 5 顶点/曲线
└─────────────────────────────────────────┘
调用者映射:
| 调用者 | 获取的索引 |
|---|---|
RectDrawOp | getRectIndexBuffer(aa, lineJoin) |
RRectDrawOp | getRRectIndexBuffer(stroke, aaType) |
AtlasTextOp | getRectIndexBuffer(aa, nullopt) |
Quads3DDrawOp | getRectIndexBuffer(aa, nullopt) |
HairlineLineOp | getHairlineLineIndexBuffer() |
HairlineQuadOp | getHairlineQuadIndexBuffer() |
设计要点:
- 按需创建:首次使用时才创建索引缓冲区
- 全局共享:所有绘制操作共享同一索引缓冲区
- 批量绘制:索引模式支持多图元批量绘制(如
MaxNumRects)
3.6 Uniform Buffer 三缓冲机制
为避免 CPU 更新 Uniform 数据时与 GPU 产生同步等待,GlobalCache 实现了三缓冲机制:
// src/gpu/GlobalCache.h
static constexpr uint32_t UNIFORM_BUFFER_COUNT = 3;
static constexpr size_t MAX_UNIFORM_BUFFER_SIZE = 64 * 1024; // 64KB 默认最小值
struct UniformBufferPacket {
std::vector<std::shared_ptr<GPUBuffer>> gpuBuffers = {};
size_t bufferIndex = 0; // 当前使用的 Buffer 索引
size_t cursor = 0; // 当前 Buffer 内的偏移
};
std::array<UniformBufferPacket, UNIFORM_BUFFER_COUNT> tripleUniformBuffer = {};
uint32_t tripleUniformBufferIndex = 0;
uint64_t counter = 0;
SlidingWindowTracker maxUniformBufferTracker = {10}; // 追踪峰值使用量(窗口大小 10 帧)
三缓冲工作原理:
帧 N: [Buffer Set 0] ◄── CPU 写入
帧 N: [Buffer Set 1] ◄── GPU 执行中
帧 N: [Buffer Set 2] ◄── GPU 队列中
帧 N+1: [Buffer Set 1] ◄── CPU 写入(轮换)
帧 N+1: [Buffer Set 2] ◄── GPU 执行中
帧 N+1: [Buffer Set 0] ◄── GPU 完成,可安全复用
每帧重置:
// src/gpu/GlobalCache.cpp
void GlobalCache::resetUniformBuffer() {
counter++; // counter 为 uint64_t 类型
tripleUniformBufferIndex = counter % UNIFORM_BUFFER_COUNT;
// 注意:先递增再取模,因此从初始 counter=0 开始调用时,
// 实际切换序列为 index: 1 → 2 → 0 → 1 → 2 → 0 ...
// 而初始帧使用的是 index=0,所以整体使用顺序仍为 0,1,2,0,1,2...
if (counter == UNIFORM_BUFFER_COUNT) {
counter = 0; // 每轮重置,避免 counter 无限增长
}
auto& currentBuffer = tripleUniformBuffer[tripleUniformBufferIndex];
// 使用滑动窗口追踪的峰值,避免过度分配
size_t maxReuseSize = maxUniformBufferTracker.getMaxValue();
if (maxReuseSize > 0 && currentBuffer.gpuBuffers.size() > maxReuseSize) {
currentBuffer.gpuBuffers.resize(maxReuseSize);
}
currentBuffer.bufferIndex = 0;
currentBuffer.cursor = 0;
}
设计要点:
- 避免同步等待:三缓冲确保 CPU 写入的 Buffer 不会被 GPU 正在使用
- 对齐要求:Uniform Buffer 偏移按
uboOffsetAlignment对齐 - 自适应容量:使用
SlidingWindowTracker追踪历史峰值,自动释放多余 Buffer - 实际 Buffer 大小:
max(shaderCaps->maxUBOSize, 64KB),取 GPU 支持的最大值和 64KB 中的较大者
分配流程(findOrCreateUniformBuffer):
1. 每次绘制调用请求一块 uniform 数据空间
2. 在当前帧的 Packet 中查找空间:
a. 当前 buffer 有足够空间 → cursor 后移,直接返回
b. 空间不够 → 切到 Packet 内的下一个 GPUBuffer
c. 没有下一个 → 创建新 GPUBuffer 并记录到 SlidingWindowTracker
3. 单个 buffer 大小固定为 max(shaderCaps->maxUBOSize, 64KB)
SlidingWindowTracker 的自动裁剪机制:
// resetUniformBuffer() 中:
size_t maxReuseSize = maxUniformBufferTracker.getMaxValue(); // 最近10帧峰值
if (maxReuseSize > 0 && currentBuffer.gpuBuffers.size() > maxReuseSize) {
currentBuffer.gpuBuffers.resize(maxReuseSize); // 裁剪多余 buffer
}
如果最近 10 帧内某个 Packet 最多只用了 2 个 GPUBuffer,就把多余的释放掉,避免常驻内存过大。
3.7 静态资源缓存
除了上述专用缓存,GlobalCache 还提供通用的静态资源缓存:
// src/gpu/GlobalCache.h
ResourceKeyMap<std::shared_ptr<Resource>> staticResources = {};
std::shared_ptr<Resource> findStaticResource(const UniqueKey& uniqueKey);
void addStaticResource(const UniqueKey& uniqueKey, std::shared_ptr<Resource> resource);
3.8 GlobalCache 架构总览
Context
│
├── ResourceCache (动态资源池)
│ ├── nonpurgeableResources: 正在使用的
│ ├── purgeableResources: 可回收的 (LRU + 帧过期)
│ ├── scratchKeyMap: 按规格查找
│ └── uniqueKeyMap: 按身份查找
│
└── GlobalCache (全局静态缓存)
│
├── [1] Program 缓存 ← LRU, max 128
│ 编译好的渲染管线,避免重复 Shader 编译
│ Key = GeometryProcessor + FragmentProcessors + Blend 等的组合哈希
│
├── [2] Gradient 纹理缓存 ← LRU, max 32
│ 复杂渐变(>8区间)退化为纹理采样时的渐变纹理
│ Key = 所有颜色 RGBA + 位置的哈希
│
├── [3] Uniform Buffer 三缓冲 ← 自动扩缩容
│ GPU 端的 Uniform 数据区,三缓冲避免 CPU/GPU 竞争
│ SlidingWindowTracker(10帧) 动态裁剪闲置 buffer
│
├── [4] Index Buffer 单例 ← 懒加载, 永驻
│ 13种几何图元的索引模板
│ 矩形/圆角矩形/描边/Hairline 等
│
└── [5] Static Resource ← 永驻
通用键值存储,Context 级别的持久资源
四、文字图集缓存(Atlas System)
4.1 设计背景
文字渲染是 2D 图形渲染的核心功能之一,但字形光栅化的计算开销相当显著:
- 字形光栅化开销:每个字形都需要调用字体引擎进行光栅化计算
- 纹理上传开销:光栅化后的位图需要上传到 GPU 纹理,增加数据传输负担
- 组合多样性挑战:不同字体、字号、样式组合数量巨大,导致缓存管理复杂
针对这些问题,TGFX 设计了一套图集系统(Atlas System) 来优化文字渲染性能和资源利用效率。
4.2 Atlas 架构设计
┌─────────────────────────────────────────────────────────────────────┐
│ Atlas System │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AtlasStrikeCache │ │
│ │ (字形数据缓存) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ AtlasStrike │ │ AtlasStrike │ │ AtlasStrike │ ... │ │
│ │ │ (Font A) │ │ (Font B) │ │ (Font C) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ glyphMap: │ │ glyphMap: │ │ glyphMap: │ │ │
│ │ │ A→AtlasGlyph│ │ 你→AtlasGlyph│ │ あ→AtlasGlyph│ │ │
│ │ │ B→AtlasGlyph│ │ 好→AtlasGlyph│ │ い→AtlasGlyph│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AtlasManager │ │
│ │ (图集纹理管理) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ Atlas │ │ │
│ │ │ │ │ │
│ │ │ Page 0 Page 1 Page 2 │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │┌──┬──┬──┬─┐│ │┌──┬──┬──┬─┐│ │ │ │ │ │
│ │ │ ││P0│P1│P2│..││ ││P0│P1│..│ ││ │ (未分配) │ │ │ │
│ │ │ │├──┼──┼──┼─┤│ │├──┼──┼──┼─┤│ │ │ │ │ │
│ │ │ ││P4│P5│P6│..││ ││ │ │ │ ││ │ │ │ │ │
│ │ │ │└──┴──┴──┴─┘│ │└──┴──┴──┴─┘│ │ │ │ │ │
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
│ │ │ 2048×2048(默认) 2048×2048(默认) (按需分配) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
4.3 三级缓存与驱逐策略
Atlas 系统采用三级缓存架构,从 CPU 到 GPU 逐层管理字形数据:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 三级缓存架构 │
├──────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ 第一级:AtlasStrikeCache(CPU 侧元数据缓存) │ │
│ │ │ │
│ │ 存储内容:字形的元数据 + 光栅化位图数据(CPU 内存) │ │
│ │ 缓存粒度:以 Strike(字体配置)为单位 │ │
│ │ 容量限制:4MB / 2048 个 Strike │ │
│ │ 淘汰策略:LRU(最近最少使用) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ AtlasStrike │ ←→ │ AtlasStrike │ ←→ │ AtlasStrike │ (LRU 链表) │ │
│ │ │ (最近使用) │ │ │ │ (最久未用) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ ↓ │ │
│ │ 继续使用 超限时淘汰 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 字形数据上传到 GPU │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ 第二级:Plot LRU(GPU 纹理内页面级缓存) │ │
│ │ │ │
│ │ 存储内容:字形位图在 Atlas 纹理中的位置 │ │
│ │ 缓存粒度:以 Plot(512×512 纹理分区)为单位 │ │
│ │ 容量限制:PlotRecentlyUsedCount = 32 次 flush │ │
│ │ 淘汰策略:MRU 链表(最近使用在前) │ │
│ │ │ │
│ │ Page 0 (2048×2048) │ │
│ │ ┌──────┬──────┬──────┬──────┐ │ │
│ │ │Plot 0│Plot 1│Plot 2│Plot 3│ PlotList: │ │
│ │ ├──────┼──────┼──────┼──────┤ [P0] → [P3] → [P1] → [P2] │ │
│ │ │Plot 4│Plot 5│Plot 6│Plot 7│ ↑最近 最久未用↓ │ │
│ │ ├──────┼──────┼──────┼──────┤ │ │
│ │ │Plot 8│Plot 9│... │... │ 超过 32 次 flush 未用 → resetRects() │ │
│ │ └──────┴──────┴──────┴──────┘ (清空 Plot,字形需重新光栅化) │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 每次 flush 后执行 compact() │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ 第三级:Atlas Compact(整体页面压缩) │ │
│ │ │ │
│ │ 存储内容:整个 Atlas Page(2048×2048 纹理) │ │
│ │ 缓存粒度:以 Page 为单位 │ │
│ │ 容量限制:AtlasRecentlyUsedCount = 128 次 flush │ │
│ │ 淘汰策略:尾页迁移压缩 + 整页释放 │ │
│ │ │ │
│ │ compact() 流程: │ │
│ │ │ │
│ │ Page 0 Page 1 Page 2 (最后一页) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │████░░░░│ │██░░░░░░│ │█░░░░░░░│ █=活跃 ░=空闲 │ │
│ │ │████░░░░│ │░░░░░░░░│ │░░░░░░░░│ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ ↑ │ │ │
│ │ └─────── 迁移 ────────────┘ │ │
│ │ │ │
│ │ 迁移后: │ │
│ │ Page 0 Page 1 Page 2 │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │████░░░░│ │███░░░░░│ │ │ → deactivateLastPage() │ │
│ │ │████░░░░│ │░░░░░░░░│ │ 释放! │ 释放整页 GPU 纹理 │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
三级缓存对比:
| 级别 | 位置 | 缓存粒度 | 淘汰触发条件 | 淘汰策略 | 淘汰代价 |
|---|---|---|---|---|---|
| 一级 AtlasStrikeCache | CPU 内存 | Strike(字体配置) | 超过 4MB 或 2048 个 | LRU | 低:仅释放 CPU 内存 |
| 二级 Plot LRU | GPU 纹理内 | Plot(512×512) | 32 次 flush 未用 | MRU 链表尾部 | 中:Plot 内字形需重新光栅化 |
| 三级 Atlas Compact | GPU 纹理 | Page(2048×2048) | 128 次 flush + 活跃 <1/4 | 尾页迁移压缩 | 高:涉及纹理拷贝和释放 |
注:
PlotRecentlyUsedCount = 32和AtlasRecentlyUsedCount = 128定义在src/core/Atlas.cpp中(文件局部static constexpr),分别控制 Plot 级别和 Atlas 级别的"最近使用"判断阈值。compact() 的触发条件为:本帧 Atlas 被使用过 或 Atlas 已连续超过 128 次 flush 未被使用(后者确保长期不活跃的 Atlas 最终也会被清理)。
淘汰流程时序:
每次 drawTextBlob() 调用:
│
├─→ AtlasStrikeCache.findOrCreateStrike()
│ │
│ └─→ 超限? → purgeOldestStrike() [一级淘汰]
│
└─→ AtlasManager.addCellToAtlas()
│
└─→ Plot 满? → 淘汰 PlotList 尾部 [二级淘汰]
每次 flush() 结束:
│
└─→ Atlas.compact()
│
├─→ 标记 32 次未用 Plot 为 available
│
└─→ 最后一页活跃 <1/4?
│
└─→ 迁移 + deactivateLastPage() [三级淘汰]
设计意图:
| 级别 | 优化目标 |
|---|---|
| 一级 | 减少 CPU 光栅化开销(光栅化是最耗时的操作) |
| 二级 | 减少 纹理上传次数(复用 GPU 纹理空间) |
| 三级 | 减少 GPU 显存占用(释放不再需要的整页纹理) |
核心思想:从细粒度到粗粒度,逐级回收资源,平衡性能与内存。
4.4 Atlas 核心实现
本节详细介绍 Atlas 系统的核心组件实现,包括字形数据缓存、纹理管理、淘汰策略和帧追踪机制。
4.4.1 AtlasStrikeCache(字形数据缓存)
// src/core/AtlasStrikeCache.h
class AtlasStrikeCache {
private:
static constexpr size_t MemorySizeLimit = 4 * 1024 * 1024; // 4MB 内存限制
static constexpr size_t StrikeCountLimit = 2048; // 最多 2048 个 Strike
BytesKeyMap<std::shared_ptr<AtlasStrike>> strikes = {};
BytesKeyMap<std::list<std::shared_ptr<AtlasStrike>>::iterator> lruMap = {};
std::list<std::shared_ptr<AtlasStrike>> lruList = {}; // LRU 淘汰链表
size_t totalMemoryUsed = 0;
};
AtlasStrike:
// src/core/AtlasStrikeCache.h
class AtlasStrike {
private:
BytesKey key = {}; // 字体 + 字号 + 样式
BlockAllocator allocator{512}; // 字形数据内存池
std::unordered_map<GlyphID, AtlasGlyph*> glyphMap = {};
std::set<GlyphID> emptyGlyphs = {}; // 空白字形缓存(空格等)
size_t memoryUsed = 0;
};
4.4.2 Atlas 纹理管理
// src/core/Atlas.h
class Atlas {
public:
static constexpr int MaxCellSize = 256; // 单个字形最大尺寸
private:
PixelFormat pixelFormat = PixelFormat::Unknown;
std::vector<std::shared_ptr<TextureProxy>> textureProxies = {};
std::vector<Page> pages = {};
int textureWidth = 2048; // 纹理宽度
int textureHeight = 2048; // 纹理高度
int plotWidth = 512; // Plot 宽度
int plotHeight = 512; // Plot 高度
};
Plot(纹理分区):
// src/core/AtlasTypes.h
class Plot {
public:
static constexpr int CellPadding = 1; // 字形间隔,避免采样越界
bool addRect(int width, int height, AtlasLocator* atlasLocator);
void resetRects(); // 重置 Plot 以复用
void setLastUseToken(AtlasToken token); // 设置最后使用的帧
private:
AtlasToken _lastUseToken = AtlasToken::InvalidToken();
uint32_t _flushesSinceLastUsed = 0; // 距上次使用的帧数
RectPackSkyline rectPack; // Skyline 装箱算法
PlotLocator _plotLocator;
};
4.4.3 Plot 淘汰策略
Atlas 使用 MRU(Most Recently Used) 策略管理 Plot:
// src/core/Atlas.h
struct Page {
std::unique_ptr<std::unique_ptr<Plot>[]> plotArray;
PlotList plotList; // MRU 链表,最近使用的在前
};
淘汰流程:
- 新字形需要空间时,检查当前 Page 的 Plot
- 如果所有 Plot 都满了,淘汰
plotList末尾(最久未用)的 Plot - 淘汰时调用
resetRects()清空 Plot,但不删除纹理 - 被淘汰 Plot 中的字形需要重新光栅化
4.4.4 AtlasToken 机制
AtlasToken 是一个单调递增的帧计数器,用于追踪 Plot 的使用时间:
// src/core/AtlasTypes.h
class AtlasToken {
private:
explicit AtlasToken(uint64_t sequenceNumber) : sequenceNumber(sequenceNumber) {
}
uint64_t sequenceNumber = 0;
};
class AtlasTokenTracker {
public:
AtlasToken nextToken() const {
return currentToken.next();
}
void advanceToken() {
++currentToken;
}
private:
AtlasToken currentToken = AtlasToken::InvalidToken();
};
使用示例:
// 渲染字形时更新 Plot 的使用时间
void AtlasManager::setPlotUseToken(PlotUseUpdater&, const PlotLocator&,
MaskFormat, AtlasToken) const;
// 每帧结束时推进 Token
void AtlasManager::postFlush();
五、内存池(BlockAllocator)
5.1 Arena/Bump分配器核心设计
BlockAllocator 是 TGFX 的 Arena/Bump 分配器,整个渲染管线几乎都在使用:
分配方式: 块增长策略:
┌────────────────────┐ Block 0: initBlockSize (如 16KB)
│ Block 0 │ Block 1: initBlockSize × 2
│ ┌──────────────┐ │ Block 2: initBlockSize × 4
│ │ obj1 │ │ ...
│ │ obj2 │ │ Block N: min(Block_{N-1} × 2, maxBlockSize)
│ │ obj3 │ │
│ │ ↓ │ │ 超大请求: 直接 malloc 精确大小
│ │ offset │ │
│ └──────────────┘ │ 64 字节缓存行对齐
└────────────────────┘
核心设计特点:
| 设计 | 说明 |
|---|---|
| 只分配不释放 | Bump Pointer:在当前块内推进 offset 指针,O(1) 分配 |
| 指数增长 | 块大小逐倍翻倍,减少 malloc 次数 |
| 批量回收 | clear() 重置所有块的 offset 为 0,整块复用 |
| 智能收缩 | 配合 SlidingWindowTracker(滑动窗口 10 帧),clear(maxReuseSize) 只保留近期最大使用量以内的内存块,多余的 free 掉 |
| 异步安全 | addReference() 返回带自定义 deleter 的 shared_ptr,异步线程持有引用期间 clear() 会阻塞等待(mutex + condition_variable) |
| 所有权转移 | release() 把已用内存块交给 BlockBuffer(RAII 容器),BlockAllocator 自身重置 |
使用范围(覆盖整个渲染管线):
| 使用者 | 初始块大小 | 用途 |
|---|---|---|
DrawingBuffer.drawingAllocator | 16KB | DrawOp、RenderTask、FragmentProcessor 等所有绘制对象 |
DrawingBuffer.vertexAllocator | 16KB | 顶点数据 |
DrawingBuffer.instanceAllocator | 16KB | 实例数据 |
PictureContext.blockAllocator | 256B | 绘制录制命令(PictureRecord) |
AtlasStrike.allocator | 512B | AtlasGlyph 元数据对象 |
AtlasUploadTask | — | 字形像素数据临时存储 |
配套智能指针:
| 类型 | 说明 |
|---|---|
PlacementPtr<T> | 类似 unique_ptr,析构时只调 ~T(),不 free 内存(内存属于 BlockAllocator) |
PlacementArray<T> | 预分配内存上的定长数组 |
5.2 PlacementPtr 零开销抽象
PlacementPtr 是一个智能指针,管理在预分配内存中构造的对象:
// src/core/utils/PlacementPtr.h
template <typename T>
class PlacementPtr {
public:
// 只调用析构函数,不释放内存(内存由 BlockAllocator 统一管理)
~PlacementPtr() {
if (pointer) {
pointer->~T();
}
}
// 禁止拷贝,只允许移动
PlacementPtr(const PlacementPtr&) = delete;
PlacementPtr& operator=(const PlacementPtr&) = delete;
PlacementPtr(PlacementPtr&& other) noexcept;
// ...
private:
T* pointer = nullptr;
};
使用示例:
// 在 BlockAllocator 中创建对象
template <typename T, typename... Args>
PlacementPtr<T> make(Args&&... args) {
void* memory = allocate(sizeof(T));
if (!memory) {
return nullptr;
}
return PlacementPtr<T>(new (memory) T(std::forward<Args>(args)...));
}
与 std::unique_ptr 的对比:
| 维度 | PlacementPtr | std::unique_ptr |
|---|---|---|
| 内存管理 | 不负责释放 | 自动 delete |
| 开销 | 仅指针大小 | 指针 + 删除器(可能更大) |
| 适用场景 | 内存池分配 | 通用堆分配 |
5.3 DrawingBuffer 的三 BlockAllocator 设计
DrawingBuffer 使用三个独立的 BlockAllocator,各司其职:
// src/gpu/DrawingBuffer.h
class DrawingBuffer {
private:
BlockAllocator drawingAllocator = {}; // CPU 对象
BlockAllocator vertexAllocator = {}; // 顶点数据
BlockAllocator instanceAllocator = {}; // 实例数据
SlidingWindowTracker drawingMaxValueTracker = {10};
SlidingWindowTracker vertexMaxValueTracker = {10};
SlidingWindowTracker instanceMaxValueTracker = {10};
};
为什么要分开?核心原因:数据的消费方式完全不同:
| Allocator | 存什么 | 消费者 | 生命周期 |
|---|---|---|---|
drawingAllocator | DrawOp、RenderTask、FragmentProcessor 等 CPU 对象 | CPU 侧遍历执行,帧末丢弃 | 仅 CPU 帧内 |
vertexAllocator | 顶点浮点数据(position、UV 坐标等) | 拼接成 GPU Vertex Buffer 上传 | CPU 写入 → GPU 上传 → CPU 释放 |
instanceAllocator | 实例数据(transform、color 等 per-instance 属性) | 拼接成 GPU Buffer 上传 | 同上 |
如果混在一个 allocator 里,会导致:
- GPU 上传时无法直接取连续内存块:顶点数据和 CPU 对象交错排列,无法整块上传
- 收缩策略互相干扰:顶点数据量和 CPU 对象量的增减模式完全不同
5.3.1 drawingAllocator:CPU 对象的 Arena
这是最"普通"的 arena 分配器,负责管线上所有一次性 CPU 对象的内存:
drawingAllocator (initBlock=16KB, maxBlock=2MB)
│
├── RenderTask (OpsRenderTask, RuntimeDrawTask, GenerateMipmapsTask...)
├── ResourceTask (TextureUploadTask, GPUBufferUploadTask, ShapeBufferUploadTask...)
├── AtlasUploadTask
├── DrawOp (RectDrawOp, AtlasTextOp, ShapeDrawOp...)
├── FragmentProcessor
├── GeometryProcessor
├── RectsVertexProvider (对象本身, 不含顶点数据)
└── ...其他帧内临时对象
使用方式(以 fillRTWithFP 为例):
auto allocator = &drawingBuffer->drawingAllocator;
auto provider = RectsVertexProvider::MakeFrom(allocator, bounds, AAType::None); // ← 对象在 drawing
auto drawOp = RectDrawOp::Make(context, std::move(provider), renderFlags); // ← 对象在 drawing
auto task = allocator->make<OpsRenderTask>(allocator, ...); // ← 对象在 drawing
drawingBuffer->renderTasks.emplace_back(std::move(task));
所有这些对象通过 PlacementPtr 管理——析构时只调 ~T(),不 free 内存,因为内存属于 allocator,帧末 reset() 批量回收。
5.3.2 vertexAllocator:Shared Vertex Buffer 拼接
这是最巧妙的设计。它不是简单的 arena,而是一个 GPU Vertex Buffer 的 CPU 侧暂存区,多个 DrawOp 的顶点数据被连续拼接到同一块内存中,最终一次上传成一个大的 GPU Buffer。
拼接流程:
Op1: RectDrawOp (4 个矩形, 需要 N 个 float)
Op2: AtlasTextOp (20 个字形, 需要 M 个 float)
Op3: RectDrawOp (1 个矩形, 需要 K 个 float)
vertexAllocator 内存布局
┌────────────────────────────────────────────┐
Block 0 │ [Op1 顶点 N×4B] [Op2 顶点 M×4B] [Op3 K×4B] │
└────────────────────────────────────────────┘
↑ ↑ ↑
offset=0 offset=N*4 offset=(N+M)*4
VertexBufferView VertexBufferView VertexBufferView
{proxy, 0, N*4} {proxy, N*4, M*4} {proxy, (N+M)*4, K*4}
│ │ │
└─────── 共享同一个 sharedVertexBuffer ────┘
关键代码路径:
// ProxyProvider::createVertexBufferProxy()
auto vertexAllocator = context->drawingManager()->vertexAllocator();
// 1. 记住当前 block 的写入位置
auto lastBlock = vertexAllocator->currentBlock(); // {blockPtr, currentOffset}
// 2. 在 vertexAllocator 中分配顶点数据空间
auto vertices = reinterpret_cast<float*>(vertexAllocator->allocate(byteSize));
// 3. 检查是否跨越到了新的 Block
auto offset = lastBlock.second;
auto currentBlock = vertexAllocator->currentBlock();
if (lastBlock.first != nullptr && lastBlock.first != currentBlock.first) {
// Block 跨越!把上一个 Block 的数据先上传
auto data = Data::MakeWithoutCopy(lastBlock.first, lastBlock.second);
uploadSharedVertexBuffer(std::move(data)); // 生成一个 GPUBufferUploadTask
offset = 0; // 新 Block 从头开始
}
// 4. 异步填充顶点数据(可选)
if (asyncEnabled) {
auto task = make_shared<VertexProviderTask>(std::move(provider), vertices);
Task::Run(task); // 后台线程运行 provider->getVertices(vertices)
} else {
provider->getVertices(vertices); // 同步填充
}
// 5. 返回一个 View,引用 sharedVertexBuffer + 偏移量
return make_shared<VertexBufferView>(sharedVertexBuffer, offset, byteSize);
Flush 时上传:
// DrawingManager::flush() 调用链
context->proxyProvider()->flushSharedVertexBuffer();
→ 取 vertexAllocator->currentBlock() 的最后一块数据
→ uploadSharedVertexBuffer(data)
→ 创建 GPUBufferUploadTask(sharedVertexBuffer, BufferType::Vertex, dataSource)
→ addResourceTask(task)
→ sharedVertexBuffer = nullptr // 重置,下帧新建
一句话总结:一帧内所有 Op 的顶点数据被拼接在 vertexAllocator 的连续内存中,每个 Op 拿到一个 VertexBufferView{proxy, offset, size} 来定位自己在大 Buffer 中的位置,最终整块上传为一个 GPU Vertex Buffer。
跨 Block 处理:
如果一帧内顶点数据量超过当前 Block 容量,allocator 会分配新 Block。此时上一个 Block 被立即上传为一个独立的 GPU Buffer,新 Block 开始拼接后续数据:
Block 0 装满 → 立即 uploadSharedVertexBuffer() → GPU Buffer A
Block 1 继续拼 → flush 时上传 → GPU Buffer B
Op1~Op5 的 VertexBufferView → 指向 GPU Buffer A (各自的 offset)
Op6~Op8 的 VertexBufferView → 指向 GPU Buffer B (各自的 offset)
5.3.3 instanceAllocator:实例数据拼接
与 vertexAllocator 逻辑完全一致,只是存的是 per-instance 数据(如 ShapeInstancedDrawOp 中的变换矩阵、颜色等):
// ProxyProvider::createInstanceBufferProxy()
auto allocator = context->drawingManager()->instanceAllocator();
auto destination = allocator->allocate(dataSize);
provider->getData(destination);
return make_shared<VertexBufferView>(sharedInstanceBuffer, offset, dataSize);
与 vertexAllocator 的区别:
| 维度 | vertexAllocator | instanceAllocator |
|---|---|---|
| 数据类型 | 顶点位置、UV 坐标 | 变换矩阵、颜色等 per-instance 属性 |
| 填充方式 | 支持异步填充(VertexProviderTask) | 目前同步填充 |
5.3.4 三 Allocator 协作流程
一帧的绘制流程:
1. 录制阶段
drawTextBlob() / drawRect() / drawShape() ...
│
├─→ drawingAllocator.make<DrawOp>() // CPU 对象
├─→ vertexAllocator.allocate(顶点数据) // 顶点拼接
└─→ instanceAllocator.allocate(实例数据) // 实例拼接
2. Flush 阶段
DrawingManager::flush()
│
├─→ flushSharedVertexBuffer() // 上传最后一块顶点数据
├─→ flushSharedInstanceBuffer() // 上传最后一块实例数据
└─→ 执行 RenderTask 列表
3. Reset 阶段
DrawingBuffer::reset()
│
├─→ drawingAllocator.clear(maxReuseSize) // 复用或释放
├─→ vertexAllocator.clear(maxReuseSize) // 复用或释放
└─→ instanceAllocator.clear(maxReuseSize) // 复用或释放
5.4 SlidingWindowTracker 智能收缩策略
要解决的问题
图形渲染的内存使用有一个典型特征:帧间波动大。
帧 1: 简单场景 → 用了 30KB 顶点
帧 2: 弹幕爆发 → 用了 500KB 顶点
帧 3: 弹幕继续 → 用了 480KB 顶点
帧 4: 场景恢复 → 用了 25KB 顶点
帧 5~100: 一直 25KB
两个极端策略都有问题:
| 策略 | 做法 | 问题 |
|---|---|---|
| 不收缩 | 分配 500KB 后永远保留 | 帧 5~100 白白占着 475KB 内存 |
| 每帧释放 | 每帧 free 所有 block,下帧重新 malloc | 帧 2→3 需要重新 malloc 500KB,白白浪费 |
需要一个自适应策略:近期用过多少就保留多少,超出部分逐步释放。
Tracker 实现
class SlidingWindowTracker {
size_t windowSize; // 窗口大小(通常 10)
std::deque<size_t> values; // 滑动窗口内的历史值
void addValue(size_t value) {
values.push_back(value);
if (values.size() > windowSize) {
values.pop_front(); // 踢掉最老的
}
}
size_t getMaxValue(); // 窗口内的峰值
size_t getAverageValue(); // 窗口内的均值
};
它本身不做任何分配/释放决策——它只是一个传感器,真正的决策在消费方。
在 DrawingBuffer 中的应用
// 采样时机:encode() 之后
void DrawingBuffer::encode() {
// ... 执行所有 tasks,编码渲染命令 ...
vertexMaxValueTracker.addValue(vertexAllocator.size());
instanceMaxValueTracker.addValue(instanceAllocator.size());
drawingMaxValueTracker.addValue(drawingAllocator.size());
}
// 决策时机:reset() 时
void DrawingBuffer::reset() {
vertexAllocator.clear(vertexMaxValueTracker.getMaxValue());
instanceAllocator.clear(instanceMaxValueTracker.getMaxValue());
drawingAllocator.clear(drawingMaxValueTracker.getMaxValue());
}
clear(maxReuseSize) 的行为:
void BlockAllocator::clear(size_t maxReuseSize) {
size_t totalBlockSize = 0;
size_t reusedBlockCount = 0;
for (auto& block : blocks) {
if (totalBlockSize < maxReuseSize) {
block.offset = 0; // 保留这个 block,重置写入位置
totalBlockSize += block.size;
reusedBlockCount++;
} else {
free(block.data); // 超出预算的 block → free 掉
}
}
blocks.resize(reusedBlockCount); // 截断 blocks 数组
}
完整示例
vertexAllocator 的 blocks
─────────────────────────
帧 1: 用 30KB
blocks: [16KB][32KB] ← 总共 48KB,使用 30KB
tracker.addValue(30KB) → values: [30KB]
帧 2: 弹幕爆发,用 500KB
blocks: [16KB][32KB][64KB][128KB][256KB][512KB] ← 指数增长分配
tracker.addValue(500KB) → values: [30KB, 500KB]
帧 3: reset()
maxReuseSize = tracker.getMaxValue() = 500KB
clear(500KB): 保留所有 block(因为都需要)
帧 3: 又用 480KB → 不需要新分配,直接复用已有 blocks ✓
帧 4~13: 每帧只用 25KB
tracker.addValue(25KB) × 10 帧
→ values 逐渐变为: [25KB × 10]
→ getMaxValue() = 25KB
帧 13: reset()
maxReuseSize = 25KB
clear(25KB):
保留 [16KB] ← 16KB < 25KB ✓
保留 [32KB] ← 48KB > 25KB → free 后续所有!
→ blocks: [16KB][32KB] ← 只保留 48KB,释放了 960KB
效果:弹幕爆发时分配的大内存,在弹幕结束后 10 帧内仍然保留(以备再次爆发),10 帧后如果确认不需要了,逐步 free。
六、跨线程资源回收(ReturnQueue)
6.1 设计背景
GPU 资源的创建和销毁必须在 GPU 线程(持有 GL context / Metal device 的线程)上进行,但资源的使用和引用释放可以发生在任意线程:
业务线程 A: shared_ptr<Texture> 引用计数 → 0 → 需要通知 GPU 线程回收
业务线程 B: shared_ptr<GPUBuffer> 引用计数 → 0 → 需要通知 GPU 线程回收
后台线程: Task 完成后释放 shared_ptr → 需要通知 GPU 线程回收
│
↓
GPU 线程: 在安全的时机统一执行 delete / glDeleteTextures / onRelease()
经典方案的问题:
传统做法是加锁——任何线程释放时锁住一个 mutex,操作待回收列表。但这有两个问题:
| 问题 | 描述 |
|---|---|
| 竞争热点 | 多个线程同时释放资源时,mutex 成为瓶颈 |
| 死锁风险 | GPU 线程可能正在 flush 中持有其他锁,再 lock 回收列表的锁容易死锁 |
所以 TGFX 采用了无锁方案:用 moodycamel::ConcurrentQueue 做跨线程的消息传递。
6.2 ReturnQueue 无锁队列设计
核心设计:shared_ptr 自定义 deleter + 无锁入队
// src/core/utils/ReturnQueue.h
class ReturnQueue {
public:
static std::shared_ptr<ReturnQueue> Make();
// 将 ReturnNode 包装成 shared_ptr,引用归零时自动入队
std::shared_ptr<ReturnNode> makeShared(ReturnNode* node);
// 尝试出队,无阻塞
ReturnNode* dequeue() {
ReturnNode* node = nullptr;
return queue.try_dequeue(node) ? node : nullptr;
}
private:
std::weak_ptr<ReturnQueue> weakThis; // 安全的自引用
moodycamel::ConcurrentQueue<ReturnNode*> queue; // 无锁并发队列
static void NotifyReferenceReachedZero(ReturnNode* node);
};
class ReturnNode {
std::shared_ptr<ReturnQueue> unreferencedQueue; // 指向归属的队列
};
6.2.1 moodycamel::ConcurrentQueue 无锁队列方案
ReturnQueue 底层使用 moodycamel::ConcurrentQueue(v1.0.0,单文件 header-only,BSD + Boost 双许可)作为无锁队列实现。
选型理由:
| 考量维度 | moodycamel::ConcurrentQueue | std::mutex + std::queue |
|---|---|---|
| 入队性能 | O(1) 无锁 | O(1) 但有锁开销 |
| 出队性能 | O(1) 无锁 | O(1) 但有锁开销 |
| 多生产者支持 | 原生支持,生产者间零竞争 | 需要锁竞争 |
| 头文件依赖 | 单头文件 | 标准库 |
| 内存开销 | 稍高(预分配块) | 低 |
6.2.1.1 核心架构:分解式多队列
ConcurrentQueue 不是一个全局队列,而是每个生产者拥有独立的 SPMC 子队列:
ConcurrentQueue<T>
│
┌───────────────┼───────────────┐
│ │ │
SubQueue(P1) SubQueue(P2) SubQueue(P3)
│ │ │
[Block]→[Block] [Block]→[Block] [Block]→[Block]
│ │ │
┌─────┴─────┐ ┌────┴─────┐ ┌────┴─────┐
│ 32 slots │ │ 32 slots│ │ 32 slots│ ← BLOCK_SIZE 默认 32
└───────────┘ └──────────┘ └──────────┘
- enqueue 只操作当前线程对应的子队列 → 生产者之间零竞争
- dequeue 遍历所有子队列寻找非空的 → 消费者之间有竞争,但通过乐观计数大幅降低
核心思想:将 MPMC 问题分解为多个 SPMC 问题。
6.2.1.2 两种生产者模式
| 模式 | 触发方式 | 块索引结构 | 块回收 |
|---|---|---|---|
| 显式生产者 | 手动创建 ProducerToken | 循环数组(单写多读,release/acquire) | 块留在环形链表中复用,不归还 |
| 隐式生产者 | 直接 enqueue(item),库按线程 ID 自动匹配 | 无锁哈希表 | 块用完可释放回全局 free list |
tgfx 的 ReturnQueue 使用隐式模式——因为"任意线程释放资源"的场景无法预先绑定 token。
隐式模式的线程 → 生产者映射是一个无锁开放地址哈希表(基于 Jeff Preshing 的方案),使用 MurmurHash3 对线程 ID 散列,半满时 spinlock 扩容。线程退出时通过 thread_local 析构回调标记槽位可复用。
6.2.1.3 入队算法(生产者端,单线程写)
1. currentTailIndex = tailIndex.load(relaxed) // 只有本线程写,relaxed 安全
2. 需要新 Block?→ 复用空块 或从三级块池申请
3. placement new 写入元素
4. tailIndex.store(newTailIndex, release) // 发布:消费者 acquire 后保证看到数据
关键:先写数据,再 release 发布 tailIndex,保证消费者看到 tailIndex 变化时数据一定已经写好。
6.2.1.4 出队算法(消费者端,乐观计数 + overcommit 修正)
这是整个库最精妙的部分。核心原子变量:
tailIndex— 生产者写,消费者读headIndex— 消费者竞争fetch_adddequeueOptimisticCount— 消费者乐观占位dequeueOvercommit— 过度提交修正
1. 快速检查:optimisticCount - overcommit < tailIndex?
└─ No → 队列可能为空,返回 false
2. myCount = dequeueOptimisticCount.fetch_add(1, relaxed) // "我预定了一个位置"
3. 重新检查 tailIndex(acquire):myCount - overcommit < tailIndex?
└─ No → 预定无效,overcommit.fetch_add(1, release) 修正
4. index = headIndex.fetch_add(1, acq_rel) // "我真正拿到了一个位置"
5. 根据 index 查块索引 → 取出元素 → 标记 empty
为什么要两步? 直接 headIndex.fetch_add 可能把 headIndex 推过 tailIndex。乐观计数器作为第一道闸门,先预定名额,确认有效后再真正递增 headIndex。无效的预定通过 overcommit 计数补偿,保持最终一致性。
相比 CAS 循环,fetch_add 是单条指令,必定成功,无需重试。
6.2.1.5 三级块池
请求 Block → ① 初始块池(预分配,fetch_add 零竞争)
→ ② 全局 Free List(CAS 无锁栈 + 引用计数防 ABA)
→ ③ malloc(最后手段)
try_enqueue 只尝试前两级,失败即返回 false;enqueue 则会在三级都失败时 malloc。
Free List 使用引用计数(而非 tagged pointer)解决 ABA 问题:try_get 期间增加引用计数,确保节点的 next 指针不被并发修改。
6.2.1.6 memory_order 使用策略
| 操作 | memory_order | 原因 |
|---|---|---|
| tailIndex.store (enqueue) | release | 确保元素数据对消费者可见 |
| tailIndex.load (dequeue) | acquire / relaxed | 快速检查用 relaxed,确认后用 acquire |
| headIndex.fetch_add (dequeue) | acq_rel | 既要看到最新块数据,也要让后续消费者看到 head 变化 |
| dequeueOptimisticCount | relaxed | 近似计数,不需要严格同步 |
| dequeueOvercommit | release(写) / relaxed(读) | 写入后需对 optimisticCount 的读可见 |
原则:生产端 release 发布,消费端 acquire 获取,中间路径尽量 relaxed。
6.2.1.7 tgfx 实际使用的接口子集
| 库特性 | 是否使用 | 说明 |
|---|---|---|
| 隐式 enqueue(无 token) | ✅ | ReturnQueue 的主要使用方式 |
| try_dequeue(单个) | ✅ | ReturnQueue 的核心操作 |
| size_approx | ✅ | 近似大小查询 |
| 显式 ProducerToken / ConsumerToken | ❌ | "任意线程释放"场景无法预先绑定 |
| bulk 批量操作 | ❌ | 未使用 |
| 自定义 Traits | ❌ | 使用默认配置 |
tgfx 只用了最简单的接口子集,但受益于整个无锁架构——多线程 enqueue 零竞争,单线程 dequeue 无锁。
6.2.2 引用计数归零时的回调机制
关键路径:入队
// 创建资源时:用自定义 deleter 包装 shared_ptr
std::shared_ptr<ReturnNode> ReturnQueue::makeShared(ReturnNode* node) {
auto reference = std::shared_ptr<ReturnNode>(node, NotifyReferenceReachedZero);
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 引用计数归零时调这个,而非 delete
reference->unreferencedQueue = weakThis.lock();
return reference;
}
// 引用计数归零时(任意线程调用)
void ReturnQueue::NotifyReferenceReachedZero(ReturnNode* node) {
// ⚠️ 关键:先 move 出 queue 指针,再入队
auto unreferencedQueue = std::move(node->unreferencedQueue);
DEBUG_ASSERT(unreferencedQueue != nullptr);
unreferencedQueue->queue.enqueue(node); // 无锁入队
}
为什么要先 std::move 再 enqueue?
这是一个精妙的竞态防护。如果不这样做,会出现 Use-After-Free:
时间线 ──────────────────────────────────────────────────────────────────►
线程 A (释放资源):
│
├─ enqueue(node) ← node 进入队列
│ │
│ │ ← 此时 node 已在队列中,可能被其他线程取走
│ │
└─ 访问 node->unreferencedQueue ← 💥 UAF (use-after-free)
线程 B (GPU 线程):
│
├─ dequeue(node) ← 取出 node
│
└─ delete node ← node 被销毁
通过先 move 到局部变量,enqueue 之后即使 node 立即被另一个线程 dequeue + delete,也不会有问题——因为我们已经不再访问 node 的任何成员了:
线程 A (释放资源):
│
├─ auto queue = std::move(node->unreferencedQueue) ← 局部变量持有 queue
│
├─ queue->enqueue(node) ← node 进入队列,之后不再访问 node
│
└─ (安全,不再访问 node)
关键路径:出队
ReturnNode* dequeue() {
ReturnNode* node = nullptr;
return queue.try_dequeue(node) ? node : nullptr; // 无锁出队,非阻塞
}
6.2.3 ResourceCache 中的处理
// src/gpu/ResourceCache.cpp
void ResourceCache::processUnreferencedResources() {
// 在 Context 线程上批量处理返回的资源
while (auto resource = static_cast<Resource*>(returnQueue->dequeue())) {
DEBUG_ASSERT(resource->isPurgeable());
RemoveFromList(nonpurgeableResources, resource);
if (!resource->scratchKey.empty() || resource->hasExternalReferences()) {
// 有复用价值的资源加入 purgeable 链表
AddToList(purgeableResources, resource);
purgeableBytes += resource->memoryUsage();
resource->lastUsedTime = currentFrameTime;
} else {
// 无复用价值的资源直接删除
removeResource(resource);
}
}
}
6.3 与 Skia GrResourceCache 回收方案的对比
Skia Ganesh 后端的 GrResourceCache 是业界成熟的 GPU 资源管理方案,TGFX 在此基础之上进行了针对性的简化和优化。
| 维度 | TGFX ResourceCache | Skia GrResourceCache |
|---|---|---|
| 跨线程回收 | moodycamel 无锁队列 + 批量处理 | SkMessageBus 有锁消息 + 惰性消费 |
| 引用计数 | 单引用(shared_ptr) | 双引用(fRefCnt + fCommandBufferUsageCnt) |
| 可清除容器 | 双向链表(O(1) 插入删除) | 最小堆(按 timestamp 排序) |
| 清理触发 | 帧开始显式调用 | 每次 insertResource 隐式触发 |
| UniqueKey 降级 | ✅ 过期时降级为 Scratch 资源 | ❌ 不支持 |
| 预算类型 | 2 种(budgeted / external) | 3 种(含 wrapped 外部资源) |
| 帧过期淘汰 | ✅ 超过 N 帧未使用 | ❌ 无帧概念 |
简要分析:
- TGFX 设计取向:简化优先、帧驱动、无锁优先、最大复用
- Skia 设计取向:精确控制、事件驱动、灵活策略、通用性强
TGFX 针对移动端帧渲染场景做了精简:用无锁队列替代消息总线提升跨线程性能,用帧驱动替代事件驱动使清理时机可预测,用 UniqueKey 降级机制最大化资源复用。
七、总结
7.1 缓存体系全景
| 缓存层级 | 主要职责 | 关键数据结构 | 淘汰策略 |
|---|---|---|---|
| ResourceCache | GPU 资源生命周期管理 | 双链表 + 双 Key 映射 | LRU + 帧过期(120帧)+ 容量限制(512MB)+ Scratch 2帧快速过期 |
| GlobalCache | 全局静态资源(渲染管线/渐变/索引/Uniform) | LRU 链表 + 三缓冲 | Program LRU 128个上限;Gradient LRU 32个上限;其余永驻 |
| AtlasStrikeCache | 字形数据 | LRU 链表 + 哈希映射 | 4MB / 2048 个限制 |
| Atlas | 字形纹理 | Page/Plot 多级结构 | MRU Plot 淘汰 |
| BlockAllocator | 小对象内存池 | 倍增块链表 | 滑动窗口自适应 |
| ReturnQueue | 跨线程资源回收 | 无锁并发队列 | 无(中转) |
7.2 设计亮点
双 Key 查找机制:ScratchKey 支持同类型资源复用,UniqueKey 支持精确匹配,兼顾灵活性和效率
双重引用计数:
shared_ptr控制"当前谁在用",UniqueKey 的UniqueDomain::useCount控制"将来谁可能用",两层配合实现帧间安全的资源保护与降级UniqueKey 降级策略:有 ScratchKey 的 UniqueKey 资源过期时不直接删除,而是降级为纯 Scratch 资源(移除身份、保留规格),最大化资源复用
无锁跨线程回收:基于 moodycamel::ConcurrentQueue 的 ReturnQueue 设计,避免锁竞争
自适应预分配:SlidingWindowTracker 追踪历史峰值,智能调整内存块复用策略(应用于 DrawingBuffer、Uniform Buffer 等)
零开销智能指针:PlacementPtr 只管理对象生命周期,不涉及内存释放,配合 BlockAllocator 实现高效内存管理
分层图集系统:AtlasStrikeCache 缓存字形元数据,Atlas 管理 GPU 纹理,职责清晰
Uniform Buffer 三缓冲:GlobalCache 的三缓冲机制消除 CPU/GPU 同步等待,提升渲染吞吐量
ResourceCache 与 GlobalCache 分层管理:按更新频率和复用模式分层 — ResourceCache 管理"内容敏感"的动态资源,GlobalCache 管理"结构性"的渲染基础设施
附录:核心代码索引
| 模块 | 头文件 | 实现文件 |
|---|---|---|
| ResourceCache | src/gpu/ResourceCache.h | src/gpu/ResourceCache.cpp |
| ResourceKey | src/gpu/resources/ResourceKey.h | src/gpu/resources/ResourceKey.cpp |
| Resource | src/gpu/resources/Resource.h | - |
| UniqueDomain | src/gpu/UniqueDomain.h | src/gpu/UniqueDomain.cpp |
| Program | src/gpu/Program.h | - |
| ProgramInfo | src/gpu/ProgramInfo.h | src/gpu/ProgramInfo.cpp |
| BlockAllocator | src/core/utils/BlockAllocator.h | src/core/utils/BlockAllocator.cpp |
| PlacementPtr | src/core/utils/PlacementPtr.h | - |
| PlacementArray | src/core/utils/PlacementArray.h | - |
| SlidingWindowTracker | src/core/utils/SlidingWindowTracker.h | src/core/utils/SlidingWindowTracker.cpp |
| ReturnQueue | src/core/utils/ReturnQueue.h | src/core/utils/ReturnQueue.cpp |
| DrawingBuffer | src/gpu/DrawingBuffer.h | src/gpu/DrawingBuffer.cpp |
| GlobalCache | src/gpu/GlobalCache.h | src/gpu/GlobalCache.cpp |
| Context | include/tgfx/gpu/Context.h | src/gpu/Context.cpp |
| Atlas | src/core/Atlas.h | src/core/Atlas.cpp |
| AtlasManager | src/core/AtlasManager.h | src/core/AtlasManager.cpp |
| AtlasStrikeCache | src/core/AtlasStrikeCache.h | src/core/AtlasStrikeCache.cpp |
| AtlasTypes | src/core/AtlasTypes.h | src/core/AtlasTypes.cpp |
