首页
/ Windows Terminal 渲染层剖析:AtlasEngine 的 Direct3D 文字渲染管线与字形图集缓存

Windows Terminal 渲染层剖析:AtlasEngine 的 Direct3D 文字渲染管线与字形图集缓存

2026-09-06 15:04:03作者:平淮齐Percy

本文基于 Windows Terminal 仓库中 AtlasEngine 架构文档,系统讲解该渲染引擎在 conhost/Terminal 渲染体系中的定位、BackendD3D/BackendD2D 双后端设计,以及以字形图集(glyph atlas)为核心的 GPU 文字绘制管线。读完本文,你可以理解 Windows Terminal 是如何把文本缓冲区高效地变成一帧 Direct3D 渲染结果的,并掌握其字形缓存、缓存满重建、连字拆分等关键机制在源码中的落点。

AtlasEngine 在渲染架构中的位置

架构总览给出的核心关系如下:上层 Renderer(位于 base/renderer.cpp)负责把文本缓冲区拆解成一批"GDI 风格的图元"("把画笔换成 X 色"、"画字符串 Y"……),这些调用经过抽象基类 RenderEngineBasebase/RenderEngineBase.cpp)分发到具体引擎:

graph TD
    Renderer["Renderer (base/renderer.cpp)<br>breaks the text buffer down into GDI-oriented graphics primitives"]
    RenderEngineBase["RenderEngineBase (base/RenderEngineBase.cpp)"]
    GdiEngine["GdiEngine (gdi/...)"]
    subgraph AtlasEngine["AtlasEngine (atlas/...)"]
        AE["AtlasEngine.cpp<br>Implements IRenderEngine text rendering API<br>breaks GDI graphics primitives down into DWRITE_GLYPH_RUNs"]
        API["AtlasEngine.api.cpp<br>Implements the parts run inside the console lock"]
        R["AtlasEngine.r.cpp<br>Implements the parts run outside of the console lock"]
        B["Backend.cpp<br>Implements common functionality/helpers"]
        B2D["BackendD2D.cpp<br>Pure Direct2D text renderer (low latency / older GPUs / RDP)"]
        B3D["BackendD3D.cpp<br>Custom performant text renderer with its own glyph cache"]
    end
    RenderEngineBase -.-> GdiEngine
    Renderer --> AtlasEngine
    AE <--> API
    AE <--> R
    R --> B2D
    R --> B3D
    B2D -.-> B
    B3D -.-> B

从源码结构看,AtlasEngineIRenderEngine 接口的唯一 GPU 实现,该接口定义了 StartPaint()EndPaint()Invalidate()/InvalidateCursor()/InvalidateSelection() 等一系列失效(invalidation)接口,以及 PaintBackground()PaintBufferLine()PaintSelection()PaintCursor() 等绘制接口。旧的 GdiEngine(gdi/)同样继承自 RenderEngineBase,作为 GDI 路径的对照实现存在。

README 同时指出了一个已知的设计损耗:先把文本缓冲区拆成 GDI 风格图元、再把这些图元重新组装回 DirectWrite 图元,这一来一回既浪费又容易出 bug。文档明确提出,如果能把 TextBuffer 和渲染设置直接交给 AtlasEngine,让它自主处理,架构会更干净——这可以视为该模块的已知改进方向。

文件职责划分

atlas/ 目录下的实现按"是否持有控制台锁"和"前端/后端"两条轴线切分:

文件 职责
AtlasEngine.cpp 实现 IRenderEngine 的文字渲染 API,把 GDI 图元拆分成 DWRITE_GLYPH_RUN
AtlasEngine.api.cpp 运行在控制台锁内部的部分,包含大量 IRenderEngine setter(如 SetGraphicsAPI
AtlasEngine.r.cpp 运行在控制台锁外部的部分(swapchain 重建、Present 等)
Backend.cpp / Backend.h 两个后端共用的工具函数(颜色换算、clamp、彩色字形枚举等)
BackendD2D.cpp 纯 Direct2D 文字渲染后端,用于低延迟场景(远程桌面、老显卡/无 GPU)
BackendD3D.cpp 自定义的高性能文字渲染后端,带自有字形缓存

AtlasEngine.h 中可以看到 AtlasEngine 只持有两个核心成员:std::unique_ptr<IBackend> _b(具体后端)和 RenderingPayload _p(每帧渲染数据),另有一个 ApiState _api 结构集中存放锁内状态(着色结果、字形索引缓冲、失效区域等)。

后端选择:GraphicsAPI 与 BackendD2D

后端的选择由 TargetSettings::graphicsAPI 控制,定义在 common.h

enum class GraphicsAPI
{
    Automatic,
    Direct2D,
    Direct3D11,
};

AtlasEngine.r.cpp_recreateBackend() 中,当 Automatic 模式探测失败(如 D3D11 设备不可用)时会回退到 Direct2D,最终通过 std::make_unique<BackendD2D>()std::make_unique<BackendD3D>(_p) 实例化对应后端。因此 BackendD2D 的"低延迟纯 D2D"定位与 README 中"for low latency remote desktop and older/no GPUs"的描述一致,它同时也充当 D3D 路径不可用时的安全兜底。

BackendD3D 渲染主管线

README 指出,BackendD3D 的渲染入口是 IBackend::Render,按固定顺序依次调用各阶段函数。对照 BackendD3D.cpp 中的实现,单帧流程为:

void BackendD3D::Render(RenderingPayload& p)
{
    if (_generation != p.s.generation())
    {
        _handleSettingsUpdate(p);   // 1. 设置代际变化时的资源更新
    }
    // ...
    _drawBackground(p);            // 2. 背景
    _drawCursorBackground(p);       // 3. 文字背后的光标
    _drawText(p);                   // 4. 文字主体
    _flushQuads(p);                 // 5. 把暂存的 quad 实例提交绘制
    if (_customPixelShader)
    {
        _executeCustomShader(p);    // 6. 可选的自定义像素着色器
    }
}

下面按 README 的章节逐段展开。

_handleSettingsUpdate:按代际增量更新资源

渲染设置采用"代计数(generation)"机制,Render 只在设置代际变化时才进入 _handleSettingsUpdate,内部按变化类型精细地重建资源:

graph TD
    Render --> _handleSettingsUpdate
    _handleSettingsUpdate -->|font changes| _updateFontDependents --> _d2dRenderTargetUpdateFontSettings
    _handleSettingsUpdate -->|misc changes| _recreateCustomShader
    _handleSettingsUpdate --->|misc changes| _recreateCustomRenderTargetView
    _handleSettingsUpdate ---->|size changes| _recreateBackgroundColorBitmap
    _handleSettingsUpdate -----> _recreateConstBuffer
    _handleSettingsUpdate ------> _setupDeviceContextState
  • 字体变化fontGeneration):走 _updateFontDependents,重算波浪下划线几何(_curlyLineHalfHeight)、DirectWrite 渲染参数(gamma、ClearType 增强对比度)、连字越界阈值(_ligatureOverhangTriggerLeft/Right),并置位 _fontChangedResetGlyphAtlas 延迟重置字形图集。
  • 杂项变化miscGeneration):重建自定义着色器与自定义 render target view。
  • 视口尺寸变化viewportCellCount):重建背景位图、常量缓冲,并恢复设备上下文状态。

_drawBackground 与光标两阶段绘制

背景绘制很简单:_drawBackground 调用 _uploadBackgroundBitmap 把按视口单元数预生成好的背景纹理上传 GPU。

README 把光标绘制描述为 _drawCursorPart1 / _drawCursorPart2 两段:Part1 在 _drawText 之前绘制位于文字下方的光标,Part2 在 _drawText 之后绘制反色光标,两段之间通过 _cursorRects 传递光标矩形数据。当前源码中对应 _drawCursorBackground_drawCursorForeground,后者带有 slow path 分支以处理跨宽字符、两侧背景色不同的空框光标(源码注释说明这种情形最多产生 6 条线,因此 _cursorRects 固定容量为 6)。这种"先画背景层、文字盖上去后再画前景层"的两段式设计,是保证光标与文字正确遮挡关系的关键。

_drawText:行 → 字体面 → 字形三级遍历

_drawText 是整个引擎的核心,其结构如下图所示(继承自 README):

graph TD
    Render --> _drawText
    _drawText --> foreachRow(("for each row"))
    foreachRow --> foreachFont(("for each font face"))
    foreachFont --> foreachGlyph(("for each glyph"))
    foreachGlyph --> _glyphAtlasMap[("font/glyph-pair lookup in glyph cache hashmap")]
    _glyphAtlasMap --> drawGlyph
    drawGlyph --> _appendQuad["_appendQuad: stages the glyph for later drawing"]
    _glyphAtlasMap --> _appendQuad
    subgraph drawGlyph["if glyph is missing"]
        _drawGlyph["_drawGlyph (defers to _drawSoftFontGlyph for soft fonts)"]
        _drawGlyph -.->|if glyph cache is full| _drawGlyphPrepareRetry
        _drawGlyphPrepareRetry --> _flushQuads["_flushQuads: draws current state into the render target"]
        _flushQuads --> _recreateInstanceBuffers["_recreateInstanceBuffers: allocates a GPU buffer for glyph instances"]
        _drawGlyphPrepareRetry --> _resetGlyphAtlas["_resetGlyphAtlas: clears the glyph texture"]
        _resetGlyphAtlas --> _resizeGlyphAtlas["_resizeGlyphAtlas: resizes the glyph texture if it's still small"]
        _drawGlyph -.->|DECDHL glyph| _splitDoubleHeightGlyph["_splitDoubleHeightGlyph: split into top/bottom halves to emulate clip rects"]
    end
    foreachGlyph -.-> _drawTextOverlapSplit["_drawTextOverlapSplit: splits overly wide glyphs to support fg color changes within the ligature"]
    foreachRow -.->|if gridlines exist| _drawGridlineRow["_drawGridlineRow: draws underlines, etc."]

几个关键机制:

  1. 命中缓存直接追加 quad。每个字体面维护一个字形哈希表,_glyphAtlasMap(fontFace, glyphIndex) 查找,命中则把该字形作为一次"quad 实例"追加到待绘队列(_appendQuad),真正的绘制延后到 _flushQuads 一次性批处理提交。
  2. 缓存未命中时走 _drawGlyph。首次见到的字形会被栅格化进字形图集纹理;软字体(DECALN 风格的位图字体)则委托给 _drawSoftFontGlyph 处理。
  3. 图集写满的自愈流程。当图集空间耗尽,_drawGlyphPrepareRetry_flushQuads 把当前已暂存的状态画进渲染目标,再 _recreateInstanceBuffers 重新分配 GPU 实例缓冲,然后 _resetGlyphAtlas 清空字形纹理,必要时 _resizeGlyphAtlas 扩大纹理尺寸——保证长时间运行、字形不断出现的会话(如浏览大量 Unicode 文本)不会因图集耗尽而崩溃。
  4. DECDHL 双高字符。半高渲染模式下,双高字形被 _splitDoubleHeightGlyph 拆成上/下半部分分别绘制,用以模拟裁剪矩形效果。
  5. 连字重叠拆分_drawTextOverlapSplit实现)把过宽的字形切分成小块,以支持连字(ligature)内部不同字符具有不同前景色的场景;触发阈值 ±halfCellWidth_updateFontDependents 中计算,若字体禁用了 liga 特性则直接关闭。
  6. 网格线。行内存在下划线/删除线/波浪线等网格线时,_drawGridlineRow(源码中为 _drawGridlines)负责绘制,其样式由 ShadingType 枚举(DottedLineDashedLineCurlyLineSolidLine 等,见 BackendD3D.h)区分。

_drawSelection 与 _executeCustomShader

选区绘制 _drawSelection 紧随文字之后执行,在 BackendD3D.cpp 中声明。管线最后一步是条件执行的 _executeCustomShader实现):当用户配置了自定义像素着色器时,引擎把当前帧渲染目标作为纹理喂给由 custom_shader_ps.hlsl / custom_shader_vs.hlsl 编译出的着色器,通过 CustomConstBuffer(含 timescaleresolutionbackground,见 BackendD3D.h)实现后处理效果——这正是仓库 samples/PixelShaders 中那些"复古""呼吸"等终端特效的底层支撑。

字形图集缓存的数据结构

BackendD3D 的"自定义高性能文字渲染器"特性,主要体现在三个精心设计的结构上(均在 BackendD3D.h):

  • QuadInstance:每帧提交给 GPU 的四边形实例,字段为 shadingTyperenditionScalepositioni16x2 有符号坐标,源码注释说明该类型是性能与功耗权衡的结果)、sizetexcoordcolor。成员刻意紧凑对齐(alignas(u16/u32)),注释还特别要求不要在成员初始化列表中赋值,以避免大批量分配时的零初始化开销。
  • AtlasGlyphEntry:图集缓存条目,记录字形索引、图集纹理坐标(texcoord)、尺寸和 overlapSplit 拆分计数,配合 AtlasGlyphEntryHashTrait 的扁平化哈希实现 O(1) 查找。
  • AtlasFontFaceEntry:按 IDWriteFontFace2 指针哈希组织,内部持有对应 4 种 LineRendition(单宽、双高、单高、双宽)各自的 til::linear_flat_set 字形集合——同一个字形在不同行渲染模式下需要独立的图集纹理块。

图集纹理上的矩形分配由 stb_rect_pack 完成(_rectPackerstbrp_nodeatlas/stb_rect_pack.cpp),保证字形纹理块紧凑排布。颜色处理方面,Backend.h 提供了 colorFromU32 / u32ColorPremultiply 等 constexpr 工具,配合 PSConstBuffer 中的 gammaRatios(ClearType 四通道 gamma)与 enhancedContrast 在像素着色器端做文字反锯齿补偿。

面向调试的后端宏

Backend.h 顶部定义了一组仅供开发/基准测试的宏开关,对理解渲染管线很有帮助:

  • ATLAS_DEBUG_SHADER_HOT_RELOAD:debug 构建默认开启,.hlsl 文件在磁盘上变更时热重载;
  • ATLAS_DEBUG_RENDER_DELAY:每帧前插入人工延迟(毫秒);
  • ATLAS_DEBUG_SHOW_DIRTY:显示每次 IDXGISwapChain2::Present1() 提交的 dirty rect;
  • ATLAS_DEBUG_DUMP_RENDER_TARGET:每帧把 swap chain 内容 dump 成 PNG(建议搭配 250ms 延迟);
  • ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION:强制整屏失效,用于对 DirectWrite 文字整形代码做基准测试;
  • ATLAS_DEBUG_DISABLE_FRAME_LATENCY_WAITABLE_OBJECT:禁用帧延迟等待对象,让渲染可以跑过屏幕刷新率做压测。

小结

AtlasEngine 是 Windows Terminal 的 GPU 渲染核心:AtlasEngine 前端把上层 GDI 风格图元整理为 DWRITE_GLYPH_RUN 并维护锁内/锁外两套状态,BackendD3D 后端则用"字体面级字形哈希表 + stb_rect_pack 图集 + quad 实例批处理"的组合把每帧文字绘制压缩为极少的 DrawCall,并以两阶段光标绘制、连字拆分、双高字形拆分处理终端特有的边缘情况;BackendD2D 作为纯 Direct2D 后备保证无 GPU 环境下的可用性。同时,README 也坦承当前"先拆 GDI 图元再重组 DirectWrite 图元"的接口路径存在浪费,是后续把 TextBuffer 直接交给引擎的优化方向。

延伸阅读路径IRenderEngine 接口AtlasEngine 头文件BackendD3D 实现着色器公共定义像素着色器样例

登录后查看全文
热门项目推荐
相关项目推荐