ReactOS 窗口系统分析(31):TextOutW 文本输出全链路 — 从用户函数到显示缓冲区的旅程

ReactOS 窗口系统分析(31):TextOutW 文本输出全链路 --- 从用户函数到显示缓冲区的旅程

目录

  • [1. 概述](#1. 概述)
  • [2. 设计动机](#2. 设计动机)
  • [3. 流程图](#3. 流程图)
  • [4. 数据流图](#4. 数据流图)
  • [5. 核心函数代码级展开](#5. 核心函数代码级展开)
  • [6. 文本→字形映射(重点)](#6. 文本→字形映射(重点))
  • [7. 字形→字库映射(重点)](#7. 字形→字库映射(重点))
  • [8. 文本→显示输出(重点)](#8. 文本→显示输出(重点))
  • [9. 关键设计思想](#9. 关键设计思想)
  • [10. 实现技术清单](#10. 实现技术清单)
  • [11. 调用链 mermaid 图](#11. 调用链 mermaid 图)
  • [12. 与 _30 / GDI 引擎 / 显示驱动的关系](#12. 与 _30 / GDI 引擎 / 显示驱动的关系)
  • [13. 源码索引](#13. 源码索引)
  • [14. 总结](#14. 总结)

1. 概述

本册是"窗口系统三大主线"的 GDI 文本渲染深化篇 。第 30 册《USER 与 GDI 双子系统》从分工 的视角给出了 TextOutW 的骨架调用链:DrawTextW(user32) → TextOutW(gdi32) → NtGdiExtTextOutW → GreExtTextOutW → IntExtTextOutW。本册则把镜头对准这条链的内部,回答三个"怎么":

  1. 文本怎么变成字形LPWSTR 的每个 WCHAR(含 UTF-16 surrogate 对)如何通过 FT_Get_Char_Index 查 cmap 表得到字形索引(glyph index);
  2. 字形怎么变成位图 :字形索引如何经 FT_Load_Glyph 加载轮廓、FT_Glyph_To_Bitmap 渲染成 8BPP 覆盖度掩码,并被 FONT_CACHE_ENTRY 全局缓存;
  3. 位图怎么变成屏幕像素 :字形位图如何被 EngCreateBitmap 包装成 SURFOBJ,再作为掩码 交给 IntEngMaskBlt → EngMaskBitBlt → AlphaBltMask,用 DC 的文本画刷 eboText 着色到目标表面------这就是"文本 = 形状掩码 × 颜色画刷"的 GDI 文本模型。

1.1 全链路 ASCII 架构图

复制代码
应用程序(App.exe)
   │  TextOutW(hdc, x, y, L"Hello", 5)
   ▼
┌──────────────────────────── gdi32.dll(用户态)────────────────────────────┐
│  TextOutW (objects/text.c L64)                                              │
│    └─ ExtTextOutW (L489)                                                    │
│         ├─ META DC 分支 → 元文件记录流(HANDLE_METADC)                      │
│         ├─ LPK 语言包分支 → LpkExtTextOut(LoadLPK(LPK_ETO))               │
│         ├─ GDI Batch 批处理(GdiBCTextOut,≤580 字符)→ TEB 批缓冲           │
│         │      │(同线程后续 NtGdiFlush 或下一条命令满时一次性送内核)        │
│         └─ 直连:NtGdiExtTextOutW(syscall → win32k.sys)                    │
└───────────────────────────────┬─────────────────────────────────────────────┘
                                ▼ 系统调用(win32u.spec: NtGdiExtTextOutW)
┌──────────────────────────── win32k.sys(内核态)────────────────────────────┐
│  ntgdi/freetype.c                                                           │
│  NtGdiExtTextOutW (L7342)                                                   │
│    └─ SEH 探测 + 拷贝用户态 String/Dx/Rect → 内核缓冲(Dx 在前,4 对齐)     │
│    └─ GreExtTextOutW (L7300)                                                │
│         └─ DC_LockDc → IntExtTextOutW (L6729)                               │
│              ├─ PATH_IsPathOpen ? PATH_ExtTextOut(路径分支,GGO_NATIVE)    │
│              ├─ DC_vPrepareDCsForBlit(更新画刷、锁设备信号量)              │
│              ├─ TA_UPDATECP / TA_BASELINE/TOP/BOTTOM 对齐处理                │
│              ├─ ETO_OPAQUE 背景填充(eboBackground 画刷)                    │
│              ├─ RealizeFontInit → FONTOBJ / PFONTGDI → FT_Face              │
│              ├─ TextIntUpdateSize(选 cmap + 设字号)                        │
│              ├─ FontLink_Chain_Init(字体链接回退链)                        │
│              ├─ lfEscapement + 世界变换 → FT_Set_Transform                   │
│              ├─ IntGetTextDisposition(TA_CENTER/TA_RIGHT 预测量宽)         │
│              ├─ EXLATEOBJ 颜色翻译(gpalRGB ↔ psurf->ppal)                  │
│              ├─ 主渲染循环(逐字符)                                        │
│              │    ├─ surrogate 对 → UTF-32                                     │
│              │    ├─ FontLink_Chain_FindGlyph → 字形索引                    │
│              │    ├─ IntGetRealGlyph → FT_Load_Glyph + 渲染 → FT_BitmapGlyph│
│              │    ├─ FT_Get_Kerning(字距)                                 │
│              │    ├─ EngCreateBitmap(8BPP 掩码表面)                        │
│              │    └─ IntEngMaskBlt(掩码 + eboText 画刷 → psoDest)          │
│              ├─ 下划线 / 删除线绘制                                          │
│              └─ DC_vFinishBlit                                              │
│  ▲                                                                          │
│  │ IntEngMaskBlt(eng/bitblt.c L1104)                                       │
│  │   └─ EngMaskBitBlt → AlphaBltMask(8BPP 掩码,alpha 混合)                │
│  │         └─ DIB_PutPixel 逐像素写入 psoDest->pvScan0                       │
├───┴──────────────────────────────────────────────────────────────────────────┤
│  eng/(GDI 引擎)→ SURFOBJ 抽象:STYPE_BITMAP(内存 DIB)/ STYPE_DEVICE      │
│  display 驱动(framebuf/vga):设备表面 dhsurf 由驱动管理,引擎回退软件渲染   │
└──────────────────────────────────────────────────────────────────────────────┘

1.2 一句话主线

一次 TextOutW 调用 = 把一串 UTF-16 字符逐一映射成字形索引 → 字形位图 → 用文本前景色画刷,透过字形掩码刷到目标表面的像素上。

链上的每个环节都在 ReactOS 源码中有一一对应的函数,本册将按"用户态 → 内核入口 → 渲染主流程 → 引擎掩码合成"的顺序逐层展开,所有行号均以实际源码为准(d:\reactos\)。


2. 设计动机

为什么一条"画几个字"的 API 会被实现得如此复杂?动机可以归纳为四点。

2.1 GDI Batch:减少系统调用次数

Windows 的 GDI 有一个著名的优化:批处理(Batch) 。像 TextOutWPatBltSelectObject 这类高频小操作,用户态 gdi32 并不立即进内核,而是先写进线程 TEB 里的一块 GdiTebBatch 缓冲 ,攒到一定量后一次性 NtGdiFlush。代价是:命令在用户态排队、在内核态延迟执行 ------于是执行时 DC 的属性可能已经变了,所以批命令必须快照 当时的 DC 属性(颜色、字体、对齐、视口原点等),把快照一起打包进批命令结构体。这就是本册第 5.2 节要展开的 GDIBSTEXTOUT 结构里那一长串"快照字段"的由来。

2.2 SEH 探测:内核不能盲信用户态指针

NtGdiExtTextOutW 的字符串、Dx 数组、RECT 都来自用户态。内核态直接解引用用户态指针有两个风险:指针可能无效(野指针)、可能跨越有效边界。ReactOS 的做法是 ProbeForRead + RtlCopyMemory 包在 _SEH2_TRY/_SEH2_EXCEPT 里,先把数据完整拷贝到内核缓冲,之后的整个渲染流程只碰内核缓冲------既安全(异常被捕获)又高效(后续循环零探测开销)。

2.3 FreeType 统一字体引擎

ReactOS 没有像 Windows 那样依赖字体驱动(DrvTextOut/DrvQueryFont),而是把 FreeType 2 直接编译进 win32k.syssdk/lib/3rdparty/freetype),由 ntgdi/freetype.c 统一完成:字体文件解析(FT_New_Memory_Face)、字符表选择(FT_Set_Charmap)、字符→字形映射(FT_Get_Char_Index)、轮廓加载(FT_Load_Glyph)、光栅化(FT_Glyph_To_Bitmap)、字距(FT_Get_Kerning)。这样:

  • 字体数据是只读共享内存SHARED_MEM + SHARED_FACE),多进程共享同一份 FT_Face
  • 字形位图进 全局 LRU 缓存g_FontCacheListHead,上限 MAX_FONT_CACHE=256),同字号同变换的重复字符不重复光栅化;
  • 输出阶段不依赖驱动:引擎用 IntEngMaskBlt 软件合成到 DIB 表面,驱动只需提供标准位块传输能力。

2.4 "文本 = 形状掩码 × 颜色画刷"的绘制模型

GDI 的哲学是一切绘制都是位块传输(BLT) 。文本也不例外:字形光栅化结果只是一张覆盖度掩码 (8BPP:0=全透明,255=全不透明),本身没有颜色;颜色来自 DC 的文本画刷DC->eboText,前景色 crForegroundClr)。IntEngMaskBlt 把"掩码 + 画刷"合成到目标表面,一个字一个字的合成,就形成了整行文字。这个模型的好处:前景色、背景色、裁剪、目标表面格式(调色板/16bpp/32bpp)全部解耦,由引擎的 DIB 逐像素函数统一处理。


3. 流程图

3.1 TextOutW 全流程(mermaid flowchart)

#mermaid-svg-XwGnlFZVrGWIVTGS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XwGnlFZVrGWIVTGS .error-icon{fill:#552222;}#mermaid-svg-XwGnlFZVrGWIVTGS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XwGnlFZVrGWIVTGS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XwGnlFZVrGWIVTGS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XwGnlFZVrGWIVTGS .marker.cross{stroke:#333333;}#mermaid-svg-XwGnlFZVrGWIVTGS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XwGnlFZVrGWIVTGS p{margin:0;}#mermaid-svg-XwGnlFZVrGWIVTGS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster-label text{fill:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster-label span{color:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster-label span p{background-color:transparent;}#mermaid-svg-XwGnlFZVrGWIVTGS .label text,#mermaid-svg-XwGnlFZVrGWIVTGS span{fill:#333;color:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS .node rect,#mermaid-svg-XwGnlFZVrGWIVTGS .node circle,#mermaid-svg-XwGnlFZVrGWIVTGS .node ellipse,#mermaid-svg-XwGnlFZVrGWIVTGS .node polygon,#mermaid-svg-XwGnlFZVrGWIVTGS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XwGnlFZVrGWIVTGS .rough-node .label text,#mermaid-svg-XwGnlFZVrGWIVTGS .node .label text,#mermaid-svg-XwGnlFZVrGWIVTGS .image-shape .label,#mermaid-svg-XwGnlFZVrGWIVTGS .icon-shape .label{text-anchor:middle;}#mermaid-svg-XwGnlFZVrGWIVTGS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XwGnlFZVrGWIVTGS .rough-node .label,#mermaid-svg-XwGnlFZVrGWIVTGS .node .label,#mermaid-svg-XwGnlFZVrGWIVTGS .image-shape .label,#mermaid-svg-XwGnlFZVrGWIVTGS .icon-shape .label{text-align:center;}#mermaid-svg-XwGnlFZVrGWIVTGS .node.clickable{cursor:pointer;}#mermaid-svg-XwGnlFZVrGWIVTGS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XwGnlFZVrGWIVTGS .arrowheadPath{fill:#333333;}#mermaid-svg-XwGnlFZVrGWIVTGS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XwGnlFZVrGWIVTGS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XwGnlFZVrGWIVTGS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwGnlFZVrGWIVTGS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XwGnlFZVrGWIVTGS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwGnlFZVrGWIVTGS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster text{fill:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS .cluster span{color:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XwGnlFZVrGWIVTGS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XwGnlFZVrGWIVTGS rect.text{fill:none;stroke-width:0;}#mermaid-svg-XwGnlFZVrGWIVTGS .icon-shape,#mermaid-svg-XwGnlFZVrGWIVTGS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwGnlFZVrGWIVTGS .icon-shape p,#mermaid-svg-XwGnlFZVrGWIVTGS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XwGnlFZVrGWIVTGS .icon-shape .label rect,#mermaid-svg-XwGnlFZVrGWIVTGS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwGnlFZVrGWIVTGS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XwGnlFZVrGWIVTGS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XwGnlFZVrGWIVTGS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





lprc 且 cwc==0 且 ETO_OPAQUE
cwc ≤ 580 字符
缓冲不足/其他












应用调用 TextOutW hdc,x,y,lpString,cch
TextOutW gdi32/text.c L64

直接转发
ExtTextOutW gdi32/text.c L489

参数整理
元文件 DC?
HANDLE_METADC

序列化进元文件记录流
返回
需要 LPK 语言包?

无 ETO_GLYPH_INDEX / ETO_IGNORELANGUAGE
LoadLPK LPK_ETO

LpkExtTextOut
返回
DC 属性检查

pdcattr 有效 且 非 DIBSECTION 且 无 TA_UPDATECP
NtGdiExtTextOutW freetype.c L7342

syscall 进内核
三种批处理情形
GdiBCExtTextOut

仅背景矩形
写 TEB 批缓冲
返回 TRUE
GdiBCTextOut

快照颜色/字体/对齐/视口原点

Dx 在前 String 后 4 对齐
SEH 探测拷贝
StringSize = Count*2

DxSize = Count*4 或 *8 ETO_PDY
超 512 字节栈缓冲?
ExAllocatePool PagedPool GDITAG_TEXT
用栈上 LocalBuffer
_SEH2_TRY

ProbeForRead + RtlCopyMemory

String 与 Dx 分别拷贝
Rect 指针?
SEH 拷贝 RECT
GreExtTextOutW L7300

DC_LockDc
IntExtTextOutW L6729
PATH 打开?
PATH_ExtTextOut

GGO_NATIVE 轮廓进路径
DC_vPrepareDCsForBlit

更新画刷/锁设备
无表面? 内存 DC
返回 TRUE
起点: TA_UPDATECP?

ptlCurrent : XStart,YStart

IntLPtoDP + ptlDCOrig
ETO_OPAQUE 或 jBkMode==OPAQUE

→ IntEngFillBox 背景
RealizeFontInit

→ FONTOBJ/PFONTGDI/FT_Face
TextIntUpdateSize

选 cmap + IntRequestFontSize
FontLink_Chain_Init

字体链接回退链
lfEscapement 矩阵 + 世界变换

FT_Set_Transform
ETO_OPAQUE 或

TA_CENTER/TA_RIGHT?
IntGetTextDisposition 预测量宽

按对齐调整起点 + 背景填充
EXLATEOBJ 颜色翻译初始化
主渲染循环 逐字符
surrogate 对 → UTF-32
FontLink_Chain_FindGlyph

→ 字形索引
IntGetRealGlyph

FT_Load_Glyph + 渲染

(查缓存)
FT_Get_Kerning 字距
EngCreateBitmap 8BPP 掩码
ETO_CLIPPED 裁剪
IntEngMaskBlt

掩码 + eboText 画刷 → psoDest
advance 推进笔位置
还有字符?
下划线/删除线绘制

IntEngFillBox/Polygon
DC_vFinishBlit 收尾
返回 bResult

3.2 IntExtTextOutW 内部九步

IntExtTextOutW 是内核侧真正的"干活"函数(freetype.c L6727-7295),内部可按逻辑划分九步:

步骤 阶段 源码位置 关键动作
1 准备 L6774-6798 参数校验、路径分支、DC_vPrepareDCsForBlit、无表面短路
2 对齐 L6800-6825 TA_UPDATECPptlCurrentIntLPtoDP 逻辑→设备;GM_ADVANCED 取世界矩阵
3 背景 L6827-6853 ETO_OPAQUE / jBkMode==OPAQUEIntEngFillBox(eboBackground)
4 字体 L6855-6887 RealizeFontInitFontGDI->SharedFace->FaceTextIntUpdateSizeFontLink_Chain_Init
5 变换 L6889-6928 lfEscapement 矩阵、世界变换 FT_Set_Transform、纵对齐 TA_BASELINE/TOP/BOTTOM
6 测量 L6930-6981 use_kerning=FT_HAS_KERNINGIntGetTextDispositionTA_CENTER/TA_RIGHT 平移起点;背景填充
7 主循环 L6992-7160 逐字符:surrogate→字形→位图→字距→EngCreateBitmapIntEngMaskBlt→advance
8 下划线/删除线 L7165-7256 lfUnderline/lfStrikeOutIntEngFillBox/IntEngFillPolygon(eboText 画刷)
9 收尾 L7258-7295 LockWindowUpdate 边界累积、解锁、DC_vFinishBlitTEXTOBJ_UnlockText

4. 数据流图

4.1 文本数据流(mermaid)

#mermaid-svg-SeUqCTX7EXH5QJry{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SeUqCTX7EXH5QJry .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SeUqCTX7EXH5QJry .error-icon{fill:#552222;}#mermaid-svg-SeUqCTX7EXH5QJry .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SeUqCTX7EXH5QJry .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SeUqCTX7EXH5QJry .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SeUqCTX7EXH5QJry .marker.cross{stroke:#333333;}#mermaid-svg-SeUqCTX7EXH5QJry svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SeUqCTX7EXH5QJry p{margin:0;}#mermaid-svg-SeUqCTX7EXH5QJry .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SeUqCTX7EXH5QJry .cluster-label text{fill:#333;}#mermaid-svg-SeUqCTX7EXH5QJry .cluster-label span{color:#333;}#mermaid-svg-SeUqCTX7EXH5QJry .cluster-label span p{background-color:transparent;}#mermaid-svg-SeUqCTX7EXH5QJry .label text,#mermaid-svg-SeUqCTX7EXH5QJry span{fill:#333;color:#333;}#mermaid-svg-SeUqCTX7EXH5QJry .node rect,#mermaid-svg-SeUqCTX7EXH5QJry .node circle,#mermaid-svg-SeUqCTX7EXH5QJry .node ellipse,#mermaid-svg-SeUqCTX7EXH5QJry .node polygon,#mermaid-svg-SeUqCTX7EXH5QJry .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SeUqCTX7EXH5QJry .rough-node .label text,#mermaid-svg-SeUqCTX7EXH5QJry .node .label text,#mermaid-svg-SeUqCTX7EXH5QJry .image-shape .label,#mermaid-svg-SeUqCTX7EXH5QJry .icon-shape .label{text-anchor:middle;}#mermaid-svg-SeUqCTX7EXH5QJry .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SeUqCTX7EXH5QJry .rough-node .label,#mermaid-svg-SeUqCTX7EXH5QJry .node .label,#mermaid-svg-SeUqCTX7EXH5QJry .image-shape .label,#mermaid-svg-SeUqCTX7EXH5QJry .icon-shape .label{text-align:center;}#mermaid-svg-SeUqCTX7EXH5QJry .node.clickable{cursor:pointer;}#mermaid-svg-SeUqCTX7EXH5QJry .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SeUqCTX7EXH5QJry .arrowheadPath{fill:#333333;}#mermaid-svg-SeUqCTX7EXH5QJry .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SeUqCTX7EXH5QJry .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SeUqCTX7EXH5QJry .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SeUqCTX7EXH5QJry .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SeUqCTX7EXH5QJry .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SeUqCTX7EXH5QJry .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SeUqCTX7EXH5QJry .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SeUqCTX7EXH5QJry .cluster text{fill:#333;}#mermaid-svg-SeUqCTX7EXH5QJry .cluster span{color:#333;}#mermaid-svg-SeUqCTX7EXH5QJry div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SeUqCTX7EXH5QJry .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SeUqCTX7EXH5QJry rect.text{fill:none;stroke-width:0;}#mermaid-svg-SeUqCTX7EXH5QJry .icon-shape,#mermaid-svg-SeUqCTX7EXH5QJry .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SeUqCTX7EXH5QJry .icon-shape p,#mermaid-svg-SeUqCTX7EXH5QJry .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SeUqCTX7EXH5QJry .icon-shape .label rect,#mermaid-svg-SeUqCTX7EXH5QJry .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SeUqCTX7EXH5QJry .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SeUqCTX7EXH5QJry .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SeUqCTX7EXH5QJry :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户态
直连路径
批 flush


LPWSTR 字符串

L'Hello 你好'

(UTF-16,可能含 surrogate 对)
Dx\[\] 字距数组

(可选,ETO_PDY 时为 (dx,dy) 对)
RECT 背景/裁剪矩形

(可选)
gdi32 批缓冲 GDIBSTEXTOUT

Dx 在前 String 后 4 字节对齐
NtGdiExtTextOutW

SEH ProbeForRead+RtlCopyMemory

→ 内核缓冲 SafeString/SafeDx
IntExtTextOutW 主循环

i = 0..Count
ch0 = *String++

(WCHAR,16 位)
IS_HIGH_SURROGATE?
ch1 = *String++

IS_LOW_SURROGATE →

Utf32FromSurrogatePair → UTF-32
UTF-32 码点 ch0
FontLink_Chain_FindGlyph

→ get_glyph_index → FT_Get_Char_Index

→ 字形索引 glyph_index
IntGetRealGlyph

FT_Load_Glyph → FT_Glyph_To_Bitmap

→ FT_BitmapGlyph(8BPP 覆盖度)
EngCreateBitmap

glyphSize + BMF_8BPP + 位图 buffer

→ HBITMAP
EngLockSurface → psoGlyph

(掩码 SURFOBJ)
IntEngMaskBlt

psoDest(目标) psoGlyph(掩码)

dc->eboText(画刷) + EXLATEOBJ×2
EngMaskBitBlt → AlphaBltMask

逐像素:掩码值×前景色 + 背景混合
psoDest->pvScan0

目标表面像素(显示缓冲区/DIB)
屏幕/窗口显示

4.2 属性数据流(mermaid)

渲染错误: Mermaid 渲染失败: Lexical error on line 2. Unrecognized text. ...R subgraph DC 属性(pdcattr,ntgdihdl.h ----------------------^

4.3 字库映射数据流(mermaid)

#mermaid-svg-8CKOzqaePYjxUsEp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8CKOzqaePYjxUsEp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8CKOzqaePYjxUsEp .error-icon{fill:#552222;}#mermaid-svg-8CKOzqaePYjxUsEp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8CKOzqaePYjxUsEp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8CKOzqaePYjxUsEp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8CKOzqaePYjxUsEp .marker.cross{stroke:#333333;}#mermaid-svg-8CKOzqaePYjxUsEp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8CKOzqaePYjxUsEp p{margin:0;}#mermaid-svg-8CKOzqaePYjxUsEp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8CKOzqaePYjxUsEp .cluster-label text{fill:#333;}#mermaid-svg-8CKOzqaePYjxUsEp .cluster-label span{color:#333;}#mermaid-svg-8CKOzqaePYjxUsEp .cluster-label span p{background-color:transparent;}#mermaid-svg-8CKOzqaePYjxUsEp .label text,#mermaid-svg-8CKOzqaePYjxUsEp span{fill:#333;color:#333;}#mermaid-svg-8CKOzqaePYjxUsEp .node rect,#mermaid-svg-8CKOzqaePYjxUsEp .node circle,#mermaid-svg-8CKOzqaePYjxUsEp .node ellipse,#mermaid-svg-8CKOzqaePYjxUsEp .node polygon,#mermaid-svg-8CKOzqaePYjxUsEp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8CKOzqaePYjxUsEp .rough-node .label text,#mermaid-svg-8CKOzqaePYjxUsEp .node .label text,#mermaid-svg-8CKOzqaePYjxUsEp .image-shape .label,#mermaid-svg-8CKOzqaePYjxUsEp .icon-shape .label{text-anchor:middle;}#mermaid-svg-8CKOzqaePYjxUsEp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8CKOzqaePYjxUsEp .rough-node .label,#mermaid-svg-8CKOzqaePYjxUsEp .node .label,#mermaid-svg-8CKOzqaePYjxUsEp .image-shape .label,#mermaid-svg-8CKOzqaePYjxUsEp .icon-shape .label{text-align:center;}#mermaid-svg-8CKOzqaePYjxUsEp .node.clickable{cursor:pointer;}#mermaid-svg-8CKOzqaePYjxUsEp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8CKOzqaePYjxUsEp .arrowheadPath{fill:#333333;}#mermaid-svg-8CKOzqaePYjxUsEp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8CKOzqaePYjxUsEp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8CKOzqaePYjxUsEp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8CKOzqaePYjxUsEp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8CKOzqaePYjxUsEp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8CKOzqaePYjxUsEp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8CKOzqaePYjxUsEp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8CKOzqaePYjxUsEp .cluster text{fill:#333;}#mermaid-svg-8CKOzqaePYjxUsEp .cluster span{color:#333;}#mermaid-svg-8CKOzqaePYjxUsEp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8CKOzqaePYjxUsEp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8CKOzqaePYjxUsEp rect.text{fill:none;stroke-width:0;}#mermaid-svg-8CKOzqaePYjxUsEp .icon-shape,#mermaid-svg-8CKOzqaePYjxUsEp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8CKOzqaePYjxUsEp .icon-shape p,#mermaid-svg-8CKOzqaePYjxUsEp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8CKOzqaePYjxUsEp .icon-shape .label rect,#mermaid-svg-8CKOzqaePYjxUsEp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8CKOzqaePYjxUsEp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8CKOzqaePYjxUsEp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8CKOzqaePYjxUsEp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LOGFONTW

lfFaceName='SimSun', lfHeight=-12,

lfWeight, lfCharSet, lfItalic...
NtGdiCreateFontIndirectW

→ GreCreateFontIndirectW

(font.c L30)
HfontCreate(font.c L1223)

LFONT 对象 + 句柄
SelectObject → DC_hSelectFont

→ pdcattr->hlfntNew
RealizeFontInit → TextIntRealizeFont

(freetype.c L6050)
SubstituteFontRecurse

(字体替换:找不到 → 替代字体)
FindBestFontFromList

PrivateFontListHead + g_FontListHead

(最佳匹配 FONTOBJ)
FONTGDI->SharedFace

PSHARED_FACE{FT_Face Face}
字体文件

\\SystemRoot\\Fonts\\*.ttf/ttc/otf/fon/fnt

(IntLoadSystemFonts)
FT_New_Memory_Face

(IntGdiLoadFontByIndexFromMemory L1834)

→ FT_Face + FT_Library
TextIntUpdateSize(L4269)

FT_Set_Charmap(选 Unicode cmap)
IntRequestFontSize(L4016)

lfHeight → FT_Request_Size
FT_Get_Char_Index(face, ch)

cmap 查表 → 字形索引
FT_Load_Glyph → 轮廓

→ FT_Glyph_To_Bitmap → 位图
FONT_CACHE_ENTRY 全局缓存

(g_FontCacheListHead,256 条 LRU)
FontLink_Chain_Init

(默认字体无此字形 → 链接链)
s_szzDefFontLink 默认链接

tahoma/msgothic/mingliu/simsun/gulim
FontLink_Chain_FindGlyph

逐链查 get_glyph_index


5. 核心函数代码级展开

5.1 用户态:TextOutW → ExtTextOutW(gdi32)

文件:d:\reactos\win32ss\gdi\gdi32\objects\text.c

TextOutW (L64-74)是所有文本输出的最简入口------9 个参数折叠成 ExtTextOutW 的 8 个参数,fuOptions=0lprc=NULLlpDx=NULL

c 复制代码
BOOL WINAPI TextOutW(HDC hdc, INT nXStart, INT nYStart,
                     LPCWSTR lpString, INT cchString)
{
    return ExtTextOutW(hdc, nXStart, nYStart, 0, NULL, lpString, cchString, NULL);
}

TextOutA (L17-58)则先做 ANSI→Unicode 转换:RtlAnsiStringToUnicodeString(L40),再调 TextOutW,用 RtlFreeUnicodeString 释放临时缓冲(L56)。注意 cchString > MAXUSHORT 时截断为 MAXUSHORT(L33-34)------因为内核接口的 Count 是 16 位范围(见 NtGdiExtTextOutWCount > 0xFFFF 校验)。

ExtTextOutW(L489-654)是用户态真正的"大脑",依次做五件事:

① 参数自洽检查(L503-517)

c 复制代码
if ( !lpDx && fuOptions & ETO_PDY )   // PDY 需要成对的 dx,dy
    return FALSE;
if ( lprc && !(fuOptions & (ETO_CLIPPED|ETO_OPAQUE)) )
    lprc = NULL;                       // 无标志的矩形没有意义,丢掉
else if (!lprc)
    fuOptions &= ~(ETO_CLIPPED|ETO_OPAQUE);  // 无矩形,清标志继续

② 元文件 DC 分支(L519-532)HANDLE_METADC(BOOL, ExtTextOut, FALSE, ...) 宏检测 DC 类型,命中元文件驱动就把整个调用序列化进 EMF 记录流,不真正画像素。

③ LPK 语言包分支(L536-546) :当 fuOptions 不含 ETO_GLYPH_INDEX | ETO_IGNORELANGUAGE 时,尝试 LoadLPK(LPK_ETO) 加载语言包 DLL,命中则调 LpkExtTextOut------这是 Windows 的"复杂文种排版(阿拉伯文/泰文等)"挂钩点,ReactOS 里 LPK 通常是空的,实际走不到。bBypassETOWMF 静态变量防止递归。

④ GDI Batch 批处理(L548-643)------本册重点。先取 DC 属性快照源:

c 复制代码
pdcattr = GdiGetDcAttr(hdc);
if ( pdcattr &&
     !(pdcattr->ulDirty_ & DC_DIBSECTION) &&   // DIBSECTION DC 不走批
     !(pdcattr->lTextAlign & TA_UPDATECP))      // UPDATECP 笔位置语义不批

然后分两种批命令:

  • GdiBCExtTextOut (L554-577):lprc && !cwc && ETO_OPAQUE 的纯背景矩形情形。GdiAllocBatchCommand(hdc, GdiBCExtTextOut) 分配 GDIBSEXTTEXTOUT,快照 ulBackgroundClrptlViewportOrg,返回 TRUE。内核侧执行时(gdibatch.c L357-400)只调 IntExtTextOutW 填充背景。

  • GdiBCTextOut (L579-643):cwc <= ((GDIBATCHBUFSIZE - sizeof(GDIBSTEXTOUT)) / sizeof(WCHAR))------即 580 个字符上限 。分配 GDIBSTEXTOUT

c 复制代码
pgO = GdiAllocBatchCommand(hdc, GdiBCTextOut);
if (pgO)
{
    USHORT cjSize = 0;
    ULONG DxSize = 0;
    if (cwc > 2) cjSize = (cwc * sizeof(WCHAR)) - sizeof(pgO->String);
    if (lpDx)
    {
        DxSize = (cwc * sizeof(INT)) * (fuOptions & ETO_PDY ? 2 : 1);
        cjSize += DxSize;
        // The structure buffer holds 4 bytes. Store Dx data then string.
        // Result one wchar -> Buf[ Dx ]Str[wC], [4][2][X] one extra unused wchar
        // to assure alignment of 4.
    }
    if ((pTeb->GdiTebBatch.Offset + cjSize ) <= GDIBATCHBUFSIZE)
    {
        pdcattr->ulDirty_ |= DC_MODE_DIRTY|DC_FONTTEXT_DIRTY;
        pgO->cbCount = cwc;  pgO->x = x;  pgO->y = y;
        pgO->Options = fuOptions;  pgO->iCS_CP = 0;
        if (lprc) pgO->Rect = *lprc;
        else      pgO->Options |= GDIBS_NORECT;  // 通知内核 lprc 为空
        /* Snapshot attributes ------ 批处理延迟执行,必须快照! */
        pgO->crForegroundClr = pdcattr->crForegroundClr;
        pgO->crBackgroundClr = pdcattr->crBackgroundClr;
        pgO->ulForegroundClr = pdcattr->ulForegroundClr;
        pgO->ulBackgroundClr = pdcattr->ulBackgroundClr;
        pgO->lBkMode         = pdcattr->lBkMode == OPAQUE ? OPAQUE : TRANSPARENT;
        pgO->hlfntNew        = pdcattr->hlfntNew;
        pgO->flTextAlign     = pdcattr->flTextAlign;
        pgO->ptlViewportOrg  = pdcattr->ptlViewportOrg;
        pgO->Size = DxSize; // of lpDx then string after.
        /* Dx 放在 String 前面,保证 String 4 字节对齐 */
        if (lpDx) RtlCopyMemory( &pgO->Buffer, lpDx, DxSize);
        if (cwc)  RtlCopyMemory( &pgO->String[DxSize/sizeof(WCHAR)], lpString, cwc * sizeof(WCHAR));
        pTeb->GdiTebBatch.Offset += cjSize;
        ((PGDIBATCHHDR)pgO)->Size += cjSize;
        return TRUE;
    }
    // 缓冲不够:回退 Offset/Count,走直连
    pTeb->GdiTebBatch.Offset -= sizeof(GDIBSTEXTOUT);
    pTeb->GdiBatchCount--;
}

关键点:快照了 8 个 DC 属性字段 (前后景颜色×2、BkMode、字体、文本对齐、视口原点)。为什么?因为批命令可能在本函数返回后很久才被 flush(下一条批命令把缓冲写满、或显式 NtGdiFlush),执行时 pdcattr 已被中间代码改掉,必须用快照还原后再调 IntExtTextOutW(见 5.2 内核侧)。

⑤ 直连路径(L645-653) :批处理条件不满足(字符太多、TA_UPDATECP、DIBSECTION DC、缓冲满)就 return NtGdiExtTextOutW(hdc, x, y, fuOptions, lprc, lpString, cwc, lpDx, 0)------真正进内核。

批命令结构体d:\reactos\win32ss\include\ntgdityp.h L509-541):

c 复制代码
typedef struct _GDIBSTEXTOUT
{
  GDIBATCHHDR gbHdr;             // 命令头:Cmd + Size
  COLORREF crForegroundClr;      // ← 快照
  COLORREF crBackgroundClr;      // ← 快照
  LONG lBkMode;                  // ← 快照(OPAQUE/TRANSPARENT)
  ULONG ulForegroundClr;         // ← 快照
  ULONG ulBackgroundClr;         // ← 快照
  int x;  int y;
  UINT Options;
  RECT Rect;
  DWORD iCS_CP;
  UINT cbCount;
  UINT Size;                     // DxSize(lpDx 字节数)
  HANDLE hlfntNew;               // ← 快照
  FLONG flTextAlign;             // ← 快照
  POINTL ptlViewportOrg;         // ← 快照
  union { WCHAR String[2]; ULONG Buffer[1]; };  // Dx 在前、String 在后
} GDIBSTEXTOUT;

typedef struct _GDIBSEXTTEXTOUT
{
  GDIBATCHHDR gbHdr;
  UINT Count;  UINT Options;  RECT Rect;
  POINTL ptlViewportOrg;
  ULONG ulBackgroundClr;
} GDIBSEXTTEXTOUT;

GDIBS_NORECT 0x80000000 复用 Options 高位标记"lprc 为空"(L507)。

5.2 批处理的内核执行:GdiFlushUserBatch

文件:d:\reactos\win32ss\gdi\ntgdi\gdibatch.c

用户态某次调用写满批缓冲或显式 GdiFlush()gdi32/misc/misc.c L44-46 → NtGdiFlush())时,内核 NtGdiFlushUserBatch(L485-533)遍历 TEB 缓冲里的每条命令:

c 复制代码
NTSTATUS APIENTRY NtGdiFlushUserBatch(VOID)
{
  PTEB pTeb = NtCurrentTeb();
  ULONG GdiBatchCount = pTeb->GdiBatchCount;
  if( (GdiBatchCount > 0) && (GdiBatchCount <= (GDIBATCHBUFSIZE/4)))
  {
    HDC hDC = (HDC) pTeb->GdiTebBatch.HDC;
    if (hDC || GdiBatchCount)
    {
      PCHAR pHdr = (PCHAR)&pTeb->GdiTebBatch.Buffer[0];
      PDC pDC = NULL;
      if (GDI_HANDLE_GET_TYPE(hDC) == GDILoObjType_LO_DC_TYPE && GreIsHandleValid(hDC))
          pDC = DC_LockDc(hDC);          // 全程只锁一次 DC
      for (; GdiBatchCount > 0; GdiBatchCount--)
      {
          ULONG Size;
          Size = GdiFlushUserBatch(pDC, (PGDIBATCHHDR) pHdr);  // 逐条执行
          if (!Size) break;
          pHdr += Size;
      }
      ...
    }
  }
}

GdiFlushUserBatchcase GdiBCTextOut:(L280-355)------用快照还原 DC 属性再执行

c 复制代码
case GdiBCTextOut:
{
    PGDIBSTEXTOUT pgO;
    ...
    if (dc->pdcattr->crForegroundClr != pgO->crForegroundClr) { ...保存旧值,设新值... }
    if (dc->pdcattr->hlfntNew != pgO->hlfntNew)
    {
        hlfntNew = dc->pdcattr->hlfntNew;
        dc->pdcattr->hlfntNew = pgO->hlfntNew;
        dc->pdcattr->ulDirty_ &= ~SLOW_WIDTHS;
        flags |= DIRTY_CHARSET;
    }
    if ( dc->pdcattr->ptlViewportOrg.x != pgO->ptlViewportOrg.x || ... )
        ...  // 视口原点快照还原 + 标记变换脏
    dc->pdcattr->jBkMode = pgO->lBkMode;   // 注意:jBkMode 是 BYTE,lBkMode 是 LONG
    dc->pdcattr->lBkMode = pgO->lBkMode;
    lprc = (pgO->Options & GDIBS_NORECT) ? NULL : &pgO->Rect;
    pgO->Options &= ~GDIBS_NORECT;

    IntExtTextOutW( dc, pgO->x, pgO->y, pgO->Options, lprc,
                    (LPCWSTR)&pgO->String[pgO->Size/sizeof(WCHAR)],  // String 在 Dx 之后
                    pgO->cbCount,
                    pgO->Size ? (const INT *)&pgO->Buffer : NULL,    // Dx 在前
                    pgO->iCS_CP );
    // 执行完恢复 DC 属性(save 旧值)
    ...
}

这里能看到用户态"快照"与内核态"还原"的完整闭环:批处理之所以能延迟执行,正是因为命令里携带了执行时刻所需的全部 DC 属性case GdiBCExtTextOut:(L357-400)同理,只还原背景色与视口原点后调 IntExtTextOutW 填背景。

5.3 内核入口:NtGdiExtTextOutW(SEH 探测拷贝)

文件:d:\reactos\win32ss\gdi\ntgdi\freetype.c L7340-7459。这是系统调用边界(win32u.spec L154:@ stdcall NtGdiExtTextOutW(ptr long long long ptr wstr long ptr long)),职责是把用户态数据安全地搬进内核

c 复制代码
#define STACK_TEXT_BUFFER_SIZE 512

BOOL APIENTRY NtGdiExtTextOutW(HDC hDC, INT XStart, INT YStart, UINT fuOptions,
    LPCRECT UnsafeRect, PCWCH UnsafeString, UINT Count, const INT *UnsafeDx,
    DWORD dwCodePage)
{
    BYTE LocalBuffer[STACK_TEXT_BUFFER_SIZE];
    PVOID Buffer = LocalBuffer;      // 优先用栈上 512 字节
    LPCWSTR SafeString = NULL;
    PINT SafeDx = NULL;
    ULONG BufSize, StringSize, DxSize = 0;

    if ((Count > 0xFFFF) || (Count > 0 && UnsafeString == NULL))  // 与 gdi32 上限一致
    {
        EngSetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }

    if (Count > 0)
    {
        BufSize = StringSize = Count * sizeof(WCHAR);
        if (UnsafeDx)
        {
            /* ETO_PDY → (dx,dy) 成对,双倍 */
            DxSize = (Count * sizeof(INT)) * ((fuOptions & ETO_PDY) ? 2 : 1);
            BufSize += DxSize;
        }
        if (BufSize > sizeof(LocalBuffer))      // 栈不够 → 池
            Buffer = ExAllocatePoolWithTag(PagedPool, BufSize, GDITAG_TEXT);
        if (!Buffer) return FALSE;

        _SEH2_TRY
        {
            /* 与 gdi32 批处理相同的布局:Dx 在前,String 后移保证 4 对齐 */
            SafeString = (LPCWSTR)(((ULONG_PTR)Buffer) + DxSize);

            ProbeForRead(UnsafeString, StringSize, 1);
            RtlCopyMemory((PVOID)SafeString, UnsafeString, StringSize);

            if (UnsafeDx)
            {
                SafeDx = Buffer;
                ProbeForRead(UnsafeDx, DxSize, 1);
                RtlCopyMemory(SafeDx, UnsafeDx, DxSize);
            }
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
        {
            Status = _SEH2_GetExceptionCode();  // 用户态坏指针 → 捕获
        }
        _SEH2_END
        if (!NT_SUCCESS(Status)) goto cleanup;
    }

    if (UnsafeRect)                              // RECT 单独 SEH 拷贝
    {
        _SEH2_TRY
        {
            ProbeForRead(UnsafeRect, sizeof(RECT), 1);
            SafeRect = *UnsafeRect;
        }
        _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); }
        _SEH2_END
        if (!NT_SUCCESS(Status)) goto cleanup;
    }

    Result = GreExtTextOutW(hDC, XStart, YStart, fuOptions,
                            &SafeRect, SafeString, Count, SafeDx, dwCodePage);

cleanup:
    if (Buffer != LocalBuffer) ExFreePoolWithTag(Buffer, GDITAG_TEXT);
    return Result;
}

设计要点:

  • 栈优先 :≤512 字节(约 128 个 WCHAR + Dx)走 LocalBuffer,零分配开销;大文本才 ExAllocatePoolWithTag(PagedPool, ..., GDITAG_TEXT)
  • 一次拷贝,全程安全 :此后 SafeString/SafeDx 都是内核地址,IntExtTextOutW 的逐字符循环不再有任何探测成本。
  • 布局与 gdi32 批命令一致Dx 在前、String 在后、4 字节对齐------两个入口(批处理/直连)在内核汇合到同一个 IntExtTextOutW,统一解析。

5.4 GreExtTextOutW:DC 锁 + 转发

文件:d:\reactos\win32ss\gdi\ntgdi\freetype.c L7298-7336:

c 复制代码
BOOL APIENTRY GreExtTextOutW(HDC hDC, INT XStart, INT YStart, UINT fuOptions,
    PRECTL lprc, PCWCH String, INT Count, const INT *Dx, DWORD dwCodePage)
{
    DC *dc;
    dc = DC_LockDc(hDC);                       // GDI 对象句柄表锁(dc.h L218)
    if (!dc) { EngSetLastError(ERROR_INVALID_HANDLE); return FALSE; }

    bResult = IntExtTextOutW( dc, XStart, YStart, fuOptions, lprc,
                              String, Count, Dx, dwCodePage );
    DC_UnlockDc(dc);                           // dc.h L236
    return bResult;
}

DC_LockDcdc.h L218-234)本质是 GDIOBJ_LockObject(hdc, GDIObjType_DC_TYPE) 加断言------DC 是 GDI 句柄表对象,锁保证并发安全;GreTextOutWntgdi/text.c L59-68)则是内核 USER(ntuser)画非客户区字符时的入口,同样转发到 GreExtTextOutW

5.5 IntExtTextOutW 主流程

文件:d:\reactos\win32ss\gdi\ntgdi\freetype.c L6727-7295。函数头注释即点明设计意图:

c 复制代码
/*
 * FIXME:
 * Call EngTextOut, which does the real work (calling DrvTextOut where
 * appropriate)
 */

即:理论上应走 EngTextOut → DrvTextOut 让显示驱动接管,但 ReactOS 当前在 IntExtTextOutW 内部用 FreeType + 引擎掩码合成完成了全部工作(驱动接管留作 FIXME)。

准备阶段(L6774-6798)

c 复制代码
if (Count > 0xFFFF || (Count > 0 && String == NULL))  // 参数校验
{ EngSetLastError(ERROR_INVALID_PARAMETER); return FALSE; }

if (PATH_IsPathOpen(dc->dclevel))                      // DC 处于 BeginPath 中
{
    return PATH_ExtTextOut(dc, XStart, YStart, fuOptions, lprc, String, Count, Dx);
}
DC_vPrepareDCsForBlit(dc, NULL, NULL, NULL);           // 更新画刷 + 锁设备信号量

if (!dc->dclevel.pSurface)                             // 内存 DC 无表面
{ bResult = TRUE; goto Cleanup; }

PATH_ExtTextOutpath.c L2416-...)是路径分支:逐字符调 ftGdiGetGlyphOutline(dc, str[idx], GGO_NATIVE|GGO_GLYPH_INDEX, ...) 把字形轮廓(TTPOLYGONHEADER/TTPOLYCURVE 多边形序列)追加进路径,供 EndPath+StrokePath/FillPath 使用------文本在这里退化为矢量路径。

DC_vPrepareDCsForBlitdclife.c L505-609)按 DIRTY_FILL|DC_BRUSH_DIRTYDIRTY_LINE|DC_PEN_DIRTYDIRTY_TEXT 依次更新填充/线/文本画刷(DC_vUpdateTextBrush),再按设备锁地址排序加锁、MouseSafetyOnDrawStart 通知驱动鼠标保护。

对齐阶段(L6800-6825)

c 复制代码
pdcattr = dc->pdcattr;
if (pdcattr->flTextAlign & TA_UPDATECP)
{ Start.x = pdcattr->ptlCurrent.x; Start.y = pdcattr->ptlCurrent.y; }  // 用上次笔位
else { Start.x = XStart; Start.y = YStart; }

IntLPtoDP(dc, &Start, 1);                            // 逻辑坐标 → 设备坐标
RealXStart64 = ((LONGLONG)Start.x + dc->ptlDCOrig.x) << 6;   // 26.6 定点,下同
RealYStart64 = ((LONGLONG)Start.y + dc->ptlDCOrig.y) << 6;

psurf = dc->dclevel.pSurface;
psoDest = &psurf->SurfObj;                            // 目标 SURFOBJ

if (pdcattr->iGraphicsMode == GM_ADVANCED)
    pmxWorldToDevice = DC_pmxWorldToDevice(dc);       // 高级模式:世界变换
else
    pmxWorldToDevice = (PMATRIX)&gmxWorldToDeviceDefault;

坐标全程用 26.6 定点数<< 6,即 1/64 像素精度),ptlDCOrig 是窗口 DC 的原点偏移(窗口 DC 的 (0,0) 不在屏幕原点)。

背景阶段(L6827-6853)

c 复制代码
if (pdcattr->ulDirty_ & DIRTY_BACKGROUND)
    DC_vUpdateBackgroundBrush(dc);                    // eboBackground 就绪

if (lprc && (fuOptions & (ETO_CLIPPED | ETO_OPAQUE)))
{ IntLPtoDP(dc, (POINT*)lprc, 2); lprc->left += dc->ptlDCOrig.x; ... }  // 矩形转设备

if (lprc && (fuOptions & ETO_OPAQUE))
{
    IntEngFillBox(dc, lprc->left, lprc->top,
                  lprc->right - lprc->left, lprc->bottom - lprc->top,
                  &dc->eboBackground.BrushObject);    // 背景矩形填色
    fuOptions &= ~ETO_OPAQUE;                          // 已填,清除标志
}
else if (pdcattr->jBkMode == OPAQUE)
    fuOptions |= ETO_OPAQUE;                           // OPAQUE 模式补背景

字体阶段(L6855-6887)------文本→字库的关键一跳:

c 复制代码
TextObj = RealizeFontInit(pdcattr->hlfntNew);         // 字体句柄 → TEXTOBJ(font.c L433)
if (TextObj == NULL) { bResult = FALSE; goto Cleanup; }

FontObj = TextObj->Font;                               // FONTOBJ(内核字体对象)
FontGDI = ObjToGDI(FontObj, FONT);                     // 转成 PFONTGDI
ASSERT(FontGDI);

IntLockFreeType();                                     // 全局 FreeType 锁
Cache.Hashed.Face = face = FontGDI->SharedFace->Face;  // ★ 共享 FT_Face

plf = &TextObj->logfont.elfEnumLogfontEx.elfLogFont;   // 原始 LOGFONTW
Cache.Hashed.lfHeight = plf->lfHeight;
Cache.Hashed.lfWidth  = plf->lfWidth;
Cache.Hashed.Aspect.Emu.Bold   = EMUBOLD_NEEDED(FontGDI->OriginalWeight, plf->lfWeight);
Cache.Hashed.Aspect.Emu.Italic = (plf->lfItalic && !FontGDI->OriginalItalic);

if (IntIsFontRenderingEnabled())
    Cache.Hashed.Aspect.RenderMode = (BYTE)IntGetFontRenderMode(plf);  // 抗锯齿模式
else
    Cache.Hashed.Aspect.RenderMode = (BYTE)FT_RENDER_MODE_MONO;        // 关抗锯齿 → 单色

if (!TextIntUpdateSize(TextObj, FontGDI, FALSE))       // 选 cmap + 设字号
{ IntUnLockFreeType(); bResult = FALSE; goto Cleanup; }

FontLink_Chain_Init(&Chain, TextObj, face);            // 字体链接链初始化

RealizeFontInitfont.c L431-450)= TEXTOBJ_LockText(hFont) + 若未初始化则 TextIntRealizeFont(懒初始化:找字体、做替换、填 FONTGDI 请求属性)。

FONT_CACHE_HASHEDfont.h L40-51)是字形缓存的查找键GlyphIndex + Face + lfHeight + lfWidth + Aspect(EmuBold/Italic + RenderMode) + matTransform,打包后 IntGetHash(L3644-3656:dwHash *= 3; dwHash ^= *pdw++ 的乘异或散列)得到 dwHash

变换阶段(L6889-6928)

c 复制代码
if (FT_IS_SCALABLE(face) && plf->lfEscapement != 0)
    IntEscapeMatrix(&Cache.Hashed.matTransform, plf->lfEscapement);   // 旋转矩阵
else
    Cache.Hashed.matTransform = identityMat;

IntMatrixFromMx(&mat, pmxWorldToDevice);              // 世界变换 → 16.16 FT_Matrix
FT_Matrix_Multiply(&mat, &Cache.Hashed.matTransform);
FT_Set_Transform(face, &Cache.Hashed.matTransform, NULL);  // ★ 注入 FreeType

bNoTransform = ((mat.xy == 0) && (mat.yx == 0) &&
                (mat.xx == (1 << 16)) && (mat.yy == (1 << 16)));   // 无变换快速路径

vecAscent64.x = 0;  vecAscent64.y = (FontGDI->tmAscent << 6);     // 上升部
FT_Vector_Transform(&vecAscent64, &Cache.Hashed.matTransform);
vecDescent64.x = 0; vecDescent64.y = -(FontGDI->tmDescent << 6);  // 下降部
FT_Vector_Transform(&vecDescent64, &Cache.Hashed.matTransform);

#define VALIGN_MASK  (TA_TOP | TA_BASELINE | TA_BOTTOM)
if ((pdcattr->flTextAlign & VALIGN_MASK) == TA_BASELINE) NOTHING;  // 基线即起点
else if ((pdcattr->flTextAlign & VALIGN_MASK) == TA_BOTTOM) { ... 调整... }
else /* TA_TOP */ { RealXStart64 -= vecAscent64.x; RealYStart64 += vecAscent64.y; }

IntEscapeMatrix(L1037-1047):lfEscapement 是 1/10 度,INT_TO_FIXED(lfEscapement)/10 转 16.16 角度,FT_Vector_Unit 求单位向量构造旋转矩阵。IntMatrixFromMx(L1049-1076)把 MATRIXefM11FLOATOBJ)乘 0x00010000 转 16.16,并注释 Y 方向镜像([ M11 -M12; -M21 M22 ])。纵对齐用 ascent/descent 向量平移起点。

测量阶段(L6930-6981)

c 复制代码
use_kerning = FT_HAS_KERNING(face);          // 字体有无 kern 表

if ((fuOptions & ETO_OPAQUE) || (pdcattr->flTextAlign & (TA_CENTER | TA_RIGHT)))
{
    if (!IntGetTextDisposition(&DeltaX64, &DeltaY64, String, Count, Dx, &Cache,
                               fuOptions, bNoTransform, &Chain))
    { IntUnLockFreeType(); bResult = FALSE; goto Cleanup; }

    if ((pdcattr->flTextAlign & TA_CENTER) == TA_CENTER)
    { RealXStart64 -= DeltaX64 / 2; RealYStart64 -= DeltaY64 / 2; }   // 居中
    else if ((pdcattr->flTextAlign & TA_RIGHT) == TA_RIGHT)
    { RealXStart64 -= DeltaX64;      RealYStart64 -= DeltaY64; }      // 右对齐

    if (fuOptions & ETO_OPAQUE)                  // 按文字实际宽度填背景
    {
        INT X0 = (RealXStart64 + vecAscent64.x + 32) >> 6;
        INT Y0 = (RealYStart64 - vecAscent64.y + 32) >> 6;
        INT DX = (DeltaX64 >> 6);
        if (无旋转) IntEngFillBox(dc, X0, Y0, DX, CY, &dc->eboBackground.BrushObject);
        else       IntEngFillPolygon(dc, Points, 4, &dc->eboBackground.BrushObject);
    }
}

IntGetTextDisposition(L6643-6725)是"预测量宽":与主循环同构地走一遍 FontLink_Chain_FindGlyph → IntGetRealGlyph,累加 advance >> 10(或 Dx),得到整串文字的 26.6 宽高------供居中/右对齐平移起点,以及 ETO_OPAQUE 背景精确填宽。

颜色翻译初始化(L6983-6987)

c 复制代码
EXLATEOBJ_vInitialize(&exloRGB2Dst, &gpalRGB, psurf->ppal, 0, 0, 0);  // RGB → 目标调色板
EXLATEOBJ_vInitialize(&exloDst2RGB, psurf->ppal, &gpalRGB, 0, 0, 0);  // 目标调色板 → RGB

if (pdcattr->ulDirty_ & DIRTY_TEXT)
    DC_vUpdateTextBrush(dc);                     // eboText 前景色就绪(dcobjs.c L106-122)

EXLATEOBJeng/xlateobj.c L491+)在 ppalDst==ppalSrc 时置 XO_TRIVIAL 用恒等翻译(L517-521);否则按位掩码/移位或查表翻译。文本绘制需要双向翻译:exloRGB2Dst 把画刷 RGB 颜色翻译成目标表面颜色空间(比如 8bpp 调色板设备的索引),exloDst2RGBAlphaBltMask 读回背景做混合。

主渲染循环(L6992-7160)------本册的核心,逐字符展开:

c 复制代码
X64 = RealXStart64;  Y64 = RealYStart64;
previous = 0;  DoBreak = FALSE;  bResult = TRUE;
for (i = 0; i < Count; ++i)
{
    ch0 = *String++;
    if (IS_HIGH_SURROGATE(ch0))                     // ① UTF-16 → UTF-32
    {
        ++i;
        if (i >= Count) break;
        ch1 = *String++;
        if (IS_LOW_SURROGATE(ch1))
            ch0 = Utf32FromSurrogatePair(ch0, ch1);
    }

    glyph_index = FontLink_Chain_FindGlyph(&Chain, &Cache, &face, ch0,
                                           (fuOptions & ETO_GLYPH_INDEX));
    Cache.Hashed.GlyphIndex = glyph_index;          // ② 字符 → 字形索引

    realglyph = IntGetRealGlyph(&Cache);            // ③ 字形 → 位图(带缓存)
    if (!realglyph) { bResult = FALSE; break; }

    if (use_kerning && previous && glyph_index && NULL == Dx)   // ④ 字距
    {
        FT_Get_Kerning(face, previous, glyph_index, 0, &delta);
        X64 += delta.x;  Y64 -= delta.y;
    }

    glyphSize.cx = realglyph->bitmap.width;
    glyphSize.cy = realglyph->bitmap.rows;

    /* CORE-11787:空格/不换行空格无位图时补宽度(TA_UPDATECP 下) */
    if ((pdcattr->flTextAlign & TA_UPDATECP) && glyphSize.cx == 0 &&
        (ch0 == L' ' || ch0 == nbsp))
    {
        IntUnLockFreeType();
        TextIntGetTextExtentPoint(dc, TextObj, L" ", 1, 0, NULL, NULL, &spaceWidth, 0);
        IntLockFreeType();
        glyphSize.cx = spaceWidth.cx;  realglyph->left = 0;
    }

    MaskRect.right = realglyph->bitmap.width;
    MaskRect.bottom = realglyph->bitmap.rows;

    DestRect.left  = ((X64 + 32) >> 6) + realglyph->left;    // ⑤ 目标矩形
    DestRect.right = DestRect.left + glyphSize.cx;
    DestRect.top   = ((Y64 + 32) >> 6) - realglyph->top;
    DestRect.bottom = DestRect.top + glyphSize.cy;

    if ((glyphSize.cx != 0) && (glyphSize.cy != 0))
    {
        /* ⑥ 字形位图包装成引擎表面(掩码) */
        hbmGlyph = EngCreateBitmap(glyphSize, realglyph->bitmap.pitch,
                                   BMF_8BPP, BMF_TOPDOWN,
                                   realglyph->bitmap.buffer);   // ★ 零拷贝,直接引用 FT 缓冲
        if (!hbmGlyph) { ... bResult = FALSE; break; }

        psoGlyph = EngLockSurface((HSURF)hbmGlyph);             // 锁成 SURFOBJ
        if (!psoGlyph) { EngDeleteSurface(...); ... break; }

        /* ETO_CLIPPED 裁剪:矩形右/下边界收紧 */
        if (lprc && (fuOptions & ETO_CLIPPED))
        {
            if (DestRect.right >= lprc->right)  { DestRect.right = lprc->right;  DoBreak = TRUE; }
            if (DestRect.bottom >= lprc->bottom)  DestRect.bottom = lprc->bottom;
        }

        /* ⑦ 掩码 + 文本画刷 → 目标表面("文本怎么变成图形"的答案) */
        if (!IntEngMaskBlt(psoDest, psoGlyph,
                           (CLIPOBJ *)&dc->co,
                           &exloRGB2Dst.xlo, &exloDst2RGB.xlo,
                           &DestRect, (PPOINTL)&MaskRect,
                           &dc->eboText.BrushObject, &g_PointZero))
        { DPRINT1("Failed to MaskBlt a glyph!\n"); }

        EngUnlockSurface(psoGlyph);
        EngDeleteSurface((HSURF)hbmGlyph);
    }

    if (DoBreak) break;

    /* ⑧ advance 推进笔位置 */
    if (NULL == Dx)
    { X64 += realglyph->root.advance.x >> 10;  Y64 -= realglyph->root.advance.y >> 10; }
    else if (fuOptions & ETO_PDY)
    { vec.x = (Dx[2 * i + 0] << 6); vec.y = (Dx[2 * i + 1] << 6); ...; X64 += vec.x; Y64 -= vec.y; }
    else
    { vec.x = (Dx[i] << 6); vec.y = 0; ...; X64 += vec.x; Y64 -= vec.y; }

    previous = glyph_index;
}
/* CORE-19721:String==NULL 时不更新 TA_UPDATECP 笔位 */
if ((pdcattr->flTextAlign & TA_UPDATECP) && String)
    pdcattr->ptlCurrent.x = DestRect.right - dc->ptlDCOrig.x;

要点解读:

  • advance 为什么 >> 10FT_BitmapGlyph->root.advance 是 26.6 定点(1/64 像素),但 FreeType 的 glyph advance 字段是 16.16 语义的 advanceroot.advance.x 是 26.6)。实际 >> 10 是把 16.16 缩回 26.6(advance = 26.6 << 10),保证后续 >> 6 得到整像素------这是 FreeType API 的经典陷阱:FT_GlyphSlot->advance 是 16.16,而 FT_BitmapGlyph->root.advance 保持同一单位,ReactOS 这里统一按 16.16 处理再 >>10 回 26.6。
  • EngCreateBitmap 零拷贝pvBits = realglyph->bitmap.buffer 直接引用 FreeType 光栅化缓冲,不做一次像素拷贝;表面用完 EngDeleteSurface 只释放表面包装、不释放 FT 缓冲(FT 缓冲由 glyph 对象持有)。
  • 掩码格式 BMF_8BPP :FT 灰度渲染(FT_PIXEL_MODE_GRAY)每像素 1 字节覆盖度,正好映射 BMF_8BPPIntEngMaskBlt 据此走 AlphaBltMask 做抗锯齿混合。

下划线/删除线(L7165-7256)

c 复制代码
if (plf->lfUnderline || plf->lfStrikeOut)
{
    /* 用 face->underline_position / underline_thickness 换算像素 */
    underline_position = face->underline_position * face->size->metrics.y_ppem / face->units_per_EM;
    thickness = face->underline_thickness * face->size->metrics.y_ppem / face->units_per_EM;
    if (thickness <= 0) thickness = 1;

    if (plf->lfUnderline)
    {
        vecA64.y = (-underline_position - thickness / 2) << 6;
        vecB64.y = vecA64.y + (thickness << 6);
        FT_Vector_Transform(...);
        if (无旋转) IntEngFillBox(dc, X0, Y0, DX, CY, &dc->eboText.BrushObject);
        else        IntEngFillPolygon(dc, Points, 4, &dc->eboText.BrushObject);
    }
    if (plf->lfStrikeOut)   // 删除线:位置 = -tmAscent/3
    { ...同构代码,使用 eboText 画刷... }
}

下划线与删除线都用文本画刷 eboText (前景色)绘制,宽度由 DeltaX64(最终笔位 - 起点)决定------即使字符串里全是空格/空字形也能正确画线。

收尾(L7258-7295)DC_ACCUM_APP | DC_ACCUM_WMGR 时把文字外接矩形累加进 IntUpdateBoundsRect(供 LockWindowUpdate 汇总刷新区域);然后 IntUnLockFreeType()EXLATEOBJ_vCleanup ×2、DC_vFinishBlit(dc, NULL)dclife.c L612+,释放设备锁 + MouseSafetyOnDrawEnd)、TEXTOBJ_UnlockText(TextObj)

5.6 IntGetRealGlyph:字符 → 字形位图(带缓存)

文件:d:\reactos\win32ss\gdi\ntgdi\freetype.c L4987-5024:

c 复制代码
IntGetRealGlyph(IN OUT PFONT_CACHE_ENTRY Cache)
{
    Cache->dwHash = IntGetHash(&Cache->Hashed, sizeof(Cache->Hashed) / sizeof(DWORD));

    realglyph = IntFindGlyphCache(Cache);          // ① 查全局字形缓存
    if (realglyph) return realglyph;

    error = FT_Load_Glyph(Cache->Hashed.Face, Cache->Hashed.GlyphIndex, FT_LOAD_DEFAULT);  // ② 加载
    if (error) { ...; return NULL; }

    glyph = Cache->Hashed.Face->glyph;
    if (Cache->Hashed.Aspect.Emu.Bold)
        FT_GlyphSlot_Embolden(glyph);              // ③ 仿真粗体(合成粗体)
    if (Cache->Hashed.Aspect.Emu.Italic)
        FT_GlyphSlot_Oblique(glyph);               //    仿真斜体(倾斜)

    realglyph = IntGetBitmapGlyphWithCache(Cache, glyph);   // ④ 渲染 + 入缓存
    if (!realglyph) DPRINT1("Failed to render glyph! ...");
    return realglyph;
}

IntFindGlyphCache (L3658-3693)是精确键匹配的哈希链表遍历(dwHash + GlyphIndex + Face + lfHeight + lfWidth + AspectValue + matTransform 全等),命中即 LRU 提升RemoveEntryList + InsertHeadList)。

IntGetBitmapGlyphWithCache(L3695-3815)完成"渲染 + 缓存":

c 复制代码
error = FT_Get_Glyph(GlyphSlot, &GlyphCopy);                       // 复制轮廓 glyph
error = FT_Glyph_To_Bitmap(&GlyphCopy, Cache->Hashed.Aspect.RenderMode, 0, 1);  // ★ 光栅化

/* 空位图兜底:某些字体(如 Droid Sans Fallback)在某些模式下出零宽位图,
   依次重试 LIGHT → NORMAL → MONO(L3729-3778) */
if (BitmapGlyph->bitmap.width == 0 && BitmapGlyph->bitmap.rows == 0) { ...重试... }

NewEntry = ExAllocatePoolWithTag(PagedPool, sizeof(FONT_CACHE_ENTRY), TAG_FONT);
FT_Bitmap_New(&AlignedBitmap);
FT_Bitmap_Convert_ReactOS_Hack(GlyphSlot->library, &BitmapGlyph->bitmap,
                               &AlignedBitmap, 4, TRUE);   // 转换到 4 字节对齐位图
FT_Bitmap_Done(GlyphSlot->library, &BitmapGlyph->bitmap);
BitmapGlyph->bitmap = AlignedBitmap;

NewEntry->BitmapGlyph = BitmapGlyph;
NewEntry->dwHash = Cache->dwHash;
NewEntry->Hashed = Cache->Hashed;
InsertHeadList(&g_FontCacheListHead, &NewEntry->ListEntry);
if (++g_FontCacheNumEntries > MAX_FONT_CACHE)             // 256 上限(L237)
{
    NewEntry = CONTAINING_RECORD(g_FontCacheListHead.Blink, FONT_CACHE_ENTRY, ListEntry);
    RemoveCachedEntry(NewEntry);                          // 超限 → 淘汰表尾(LRU)
}

return BitmapGlyph;

缓存淘汰:MAX_FONT_CACHE = 256(L237),表头是最近使用(LRU),表尾最久未用,RemoveCachedEntry(L460-470)调 FT_Done_Glyph 释放位图并摘链。RemoveCacheEntries(Face)(L472-492)在字体卸载时按 Hashed.Face 批量清掉该字体的缓存项------保证缓存不悬挂已卸载的 FT_Face

文件:d:\reactos\win32ss\gdi\ntgdi\freetype.c L4438-4493。作用:默认字体查不到字形时,沿字体链接链逐个 fallback 字体查找

c 复制代码
FontLink_Chain_FindGlyph(_Inout_ PFONTLINK_CHAIN pChain, _Out_ PFONT_CACHE_ENTRY pCache,
                         _Inout_ FT_Face *pFace, _In_ UINT code, _In_ BOOL fCodeAsIndex)
{
    PFONTLINK pFontLink; PLIST_ENTRY Entry, Head;
    UINT index; FT_Face face;

    // 先试默认字体
    index = get_glyph_index_flagged(pChain->pDefFace, code, fCodeAsIndex);
    if (index)
    {
        pCache->Hashed.Face = *pFace = pChain->pDefFace;
        return index;                            // 默认字体命中
    }

    if (!FontLink_Chain_IsPopulated(pChain))     // 链未构建 → 构建
    {
        FontLink_Chain_Populate(pChain);
        FontLink_Chain_Dump(pChain);
    }

    Head = &pChain->FontLinkList;
    for (Entry = Head->Flink; Entry != Head; Entry = Entry->Flink)
    {
        pFontLink = CONTAINING_RECORD(Entry, FONTLINK, ListEntry);
        if (!FontLink_PrepareFontInfo(pFontLink)) continue;   // 链接字体未加载/被忽略
        face = pFontLink->SharedFace->Face;
        index = get_glyph_index(face, code);
        if (!index) continue;                    // 该链接字体也无此字形 → 下一个
        pCache->Hashed.Face = *pFace = face;     // ★ 找到:切换 FT_Face
        if (IntNeedRequestFontSize(pChain, face, Entry))
            IntRequestFontSizeEx(face, &pChain->LogFont);     // 按 LOGFONT 重新设字号
        FT_Set_Transform(face, &pCache->Hashed.matTransform, NULL);
        return index;
    }

    // 全部失败:用默认字体画默认字形(U+30FB KATAKANA MIDDLE DOT)
    code = s_chFontLinkDefaultChar;
    index = get_glyph_index(*pFace, code);
    pCache->Hashed.Face = *pFace = pChain->pDefFace;
    return index;
}

配套函数:

  • FontLink_Chain_Init (L1176-1185):RtlZeroMemory 后记下 pBaseTextObj(TEXTOBJ)与 pDefFace(默认 FT_Face)。
  • FontLink_Chain_Populate (L1302-1370):取 LOGFONT 为基底,按 lfFaceName 查注册表 FontLink 条目(FontLink_Chain_FindEntry),逐个 FontLink_Create 生成 FONTLINK 节点插入链表;默认链接数据 s_szzDefFontLink(L1188-1194):
c 复制代码
static const WCHAR s_szzDefFontLink[] =
    L"tahoma.ttf,Tahoma\0"
    L"msgothic.ttc,MS UI Gothic\0"
    L"mingliu.ttc,PMingLiU\0"
    L"simsun.ttc,SimSun\0"
    L"gulim.ttc,Gulim\0"
    L"\0";

(固定宽度字体的默认链 s_szzDefFixedFontLink 用 Courier New/MS Gothic/MingLiU/NSimSun/GulimChe,L1196-1202。)

  • FontLink_Create (L1223-1261):解析 "<文件名>,<字体名>",把名字拷进 lfFaceNameSubstituteFontRecurse 做一次替换,去重后 ExAllocatePoolZeroFONTLINK
  • FontLink_PrepareFontInfo (L349-397):惰性解析链接字体的 SharedFace------先在私有字体表 Win32Process->PrivateFontListHead 找,再在系统字体表 g_FontListHead 找(FindBestFontFromList),找不到置 bIgnore=TRUE;还要用 MatchFontName 校验家族名。
  • FontLink_Chain_FindEntry :按 lfFaceNameg_FontLinkEntries(注册表读入的 FONTLINK_ENTRY 表)取 pszzFontLink 字符串(格式 "<文件>,<名字>\0..." 的 ZZZ 串)。

FONTLINK / FONTLINK_CHAIN 结构(freetype.c L33-55):

c 复制代码
typedef struct _FONTLINK
{
    LIST_ENTRY ListEntry;      // 链上节点
    BOOL bIgnore;
    LOGFONTW LogFont;
    PSHARED_FACE SharedFace;   // 解析出的共享 FT_Face 宿主
} FONTLINK, *PFONTLINK;

typedef struct _FONTLINK_CHAIN
{
    LIST_ENTRY FontLinkList;   // FONTLINK 链表
    LOGFONTW LogFont;
    PCZZWSTR pszzFontLink;
    PTEXTOBJ pBaseTextObj;
    FT_Face pDefFace;          // 默认字体
} FONTLINK_CHAIN, *PFONTLINK_CHAIN;

#define FONTLINK_DEFAULT_CHAR 0x30FB   // U+30FB(片假名中点),兜底字形
static DWORD s_chFontLinkDefaultChar = FONTLINK_DEFAULT_CHAR;

5.8 IntEngMaskBlt:掩码绘制(字形位图当掩码、画刷着色)

文件:d:\reactos\win32ss\gdi\eng\bitblt.c L1104-1240。这是"文本怎么变成图形显示输出"的直接答案:

c 复制代码
BOOL APIENTRY IntEngMaskBlt(
    _Inout_ SURFOBJ *psoDest,      // 目标表面(DC 的表面)
    _In_ SURFOBJ *psoMask,         // 掩码表面(字形位图)
    _In_ CLIPOBJ *pco,             // 裁剪(dc->co)
    _In_ XLATEOBJ *pxloDest,       // RGB→目标颜色翻译
    _In_ XLATEOBJ *pxloSource,     // 目标→RGB 翻译
    _In_ RECTL *prclDest,          // 目标矩形(DestRect)
    _In_ POINTL *pptlMask,         // 掩码源点(MaskRect 起点)
    _In_ BRUSHOBJ *pbo,            // 文本画刷(dc->eboText.BrushObject)
    _In_ POINTL *pptlBrushOrg)     // 画刷原点(g_PointZero)
{
    if (psoMask->iBitmapFormat == BMF_1BPP)      // 1BPP 掩码 → ROP4 位操作
        return IntEngBitBlt(psoDest, NULL, psoMask, pco, pxloDest, prclDest,
                            NULL, pptlMask, pbo, pptlBrushOrg, ROP4_MASKPAINT);

    ASSERT(psoMask->iBitmapFormat == BMF_8BPP);  // 字形掩码是 8BPP 覆盖度

    if (pco != NULL)                              // 与裁剪边界求交
    {
        if (!RECTL_bIntersectRect(&rcDest, prclDest, &pco->rclBounds))
            return TRUE;                          // 完全被裁剪掉
        ptMask.x += rcDest.left - prclDest->left; // 掩码点随裁剪平移
        ptMask.y += rcDest.top - prclDest->top;
    }

    if (psoDest->iType != STYPE_BITMAP)           // 设备托管表面
    {
        /* 建临时 DIB 表面:拷贝目标 → 掩码合成 → 拷回 */
        psurfTemp = SURFACE_AllocSurface(STYPE_BITMAP, ..., psoDest->iBitmapFormat, ...);
        EngCopyBits(&psurfTemp->SurfObj, psoDest, NULL, NULL, &rcTemp, (PPOINTL)&rcDest);
        EngMaskBitBlt(&psurfTemp->SurfObj, psoMask, NULL, pxloDest, pxloSource, ...);
        EngCopyBits(psoDest, &psurfTemp->SurfObj, pco, NULL, &rcDest, (PPOINTL)&rcTemp);
        GDIOBJ_vDeleteObject(&psurfTemp->BaseObject);
    }
    else
        ret = EngMaskBitBlt(psoDest, psoMask, pco, pxloDest, pxloSource,
                            &rcDest, &ptMask, pbo, pptlBrushOrg);   // 直接合成
    return ret;
}

EngMaskBitBlt (L894-...)在 DC_TRIVIAL 裁剪下按掩码格式分流(L1010-1018):

c 复制代码
case DC_TRIVIAL:
    if (psoMask->iBitmapFormat == BMF_8BPP)
        Ret = AlphaBltMask(psoOutput, NULL, psoInput, DestColorTranslation,
                           SourceColorTranslation, &OutputRect, NULL, &InputPoint,
                           pbo, &AdjustedBrushOrigin);       // 8BPP → alpha 混合
    else
        Ret = BltMask(psoOutput, NULL, psoInput, DestColorTranslation,
                      &OutputRect, NULL, &InputPoint, pbo, &AdjustedBrushOrigin,
                      ROP4_MASK);                            // 1BPP → ROP4

AlphaBltMask (L823-890)------抗锯齿文本的像素级核心:每个掩码字节是覆盖度(alpha),0xFF 直接写画刷色,中间值按 alpha 与背景混合:

c 复制代码
AlphaBltMask(SURFOBJ* psoDest, SURFOBJ* psoSource, SURFOBJ* psoMask,
             XLATEOBJ* pxloRGB2Dest, XLATEOBJ* pxloBrush,
             RECTL* prclDest, POINTL* pptlMask, BRUSHOBJ* pbo, POINTL* pptlBrush)
{
    BrushColor = XLATEOBJ_iXlate(pxloBrush, pbo ? pbo->iSolidColor : 0);
    r = GetRValue(BrushColor); g = GetGValue(BrushColor); b = GetBValue(BrushColor);

    tMask = (PBYTE)psoMask->pvScan0 + (pptlMask->y * psoMask->lDelta) + pptlMask->x;
    for (j = 0; j < dy; j++)
    {
        lMask = tMask;
        for (i = 0; i < dx; i++)
        {
            if (*lMask > 0)
            {
                if (*lMask == 0xff)
                {
                    DIB_PutPixel(psoDest, prclDest->left + i, prclDest->top + j,
                                 pbo ? pbo->iSolidColor : 0);          // 完全不透明 → 画刷色
                }
                else
                {
                    Background = DIB_GetSource(psoDest, prclDest->left + i, prclDest->top + j,
                                               pxloBrush);              // 读目标背景
                    NewColor = RGB((*lMask * (r - GetRValue(Background)) >> 8) + GetRValue(Background),
                                   (*lMask * (g - GetGValue(Background)) >> 8) + GetGValue(Background),
                                   (*lMask * (b - GetBValue(Background)) >> 8) + GetBValue(Background));
                    Background = XLATEOBJ_iXlate(pxloRGB2Dest, NewColor);  // 翻译回目标空间
                    DIB_PutPixel(psoDest, prclDest->left + i, prclDest->top + j, Background);
                }
            }
            lMask++;
        }
        tMask += psoMask->lDelta;
    }
}

BltMask (L47-...)是 1BPP 掩码版本:按掩码位选 fgndRop/bkgndRop 逐像素走 ROP4(L136-166),配合 DibFunctionsForBitmapFormat[...].DIB_GetPixel/DIB_PutPixel 函数表适配目标表面格式。

至此,"文本怎么变成图形"的三段式答案完整了:① 字形光栅化 → 8BPP 覆盖度掩码;② 掩码 + 前景色画刷 → AlphaBltMask 混合;③ DIB_PutPixel 写进 psoDest->pvScan0 。目标表面是 DIB 时直接写内存;是设备表面时引擎建临时 DIB 中转再 EngCopyBits 回写。


6. 文本→字形映射(重点)

本节回答第一个"怎么":一串 WCHAR 是怎么变成一串字形索引(glyph index)的。映射分三级:编码归一 → cmap 查表 → 字形索引。

6.1 编码归一:UTF-16 → UTF-32

TextOutW 的字符串是 UTF-16(LPWSTR,每个元素 16 位)。但 FreeType 的 FT_Get_Char_Index 接收的是 32 位 Unicode 码点FT_ULong charcode),因此必须处理 surrogate pair。ReactOS 在 utils.h 定义(L38-50):

c 复制代码
#define HIGH_SURROGATE_MIN 0xD800U
#define HIGH_SURROGATE_MAX 0xDBFFU
#define LOW_SURROGATE_MIN  0xDC00U
#define LOW_SURROGATE_MAX  0xDFFFU

#define IS_HIGH_SURROGATE(ch0) (HIGH_SURROGATE_MIN <= (ch0) && (ch0) <= HIGH_SURROGATE_MAX)
#define IS_LOW_SURROGATE(ch1)  (LOW_SURROGATE_MIN  <= (ch1) && (ch1) <=  LOW_SURROGATE_MAX)

static inline DWORD
Utf32FromSurrogatePair(_In_ DWORD ch0, _In_ DWORD ch1)
{
    return ((ch0 - HIGH_SURROGATE_MIN) << 10) + (ch1 - LOW_SURROGATE_MIN) + 0x10000;
}

IntExtTextOutW 主循环里(L6999-7009)、IntGetTextDisposition(L6667-6677)、TextIntGetTextExtentPoint(L5086-5096)三处都用了同一套 surrogate 处理模式:

c 复制代码
ch0 = *String++;
if (IS_HIGH_SURROGATE(ch0))
{
    ++i;
    if (i >= Count) break;          // 高位代理是最后一个 → 放弃
    ch1 = *String++;
    if (IS_LOW_SURROGATE(ch1))
        ch0 = Utf32FromSurrogatePair(ch0, ch1);   // 组成合法代理对 → UTF-32
    // 若 ch1 不是低位代理,则 ch0 保持高位代理值(无效码点,画默认字形)
}

注意语义细节:孤立的代理项不会导致崩溃,只是被当作无效码点交给字形查找(通常查不到 → 默认字形),这符合 Windows 的容错行为。

6.2 cmap 查表:FT_Get_Char_Index

字形索引的获取集中在 get_glyph_index 系列(freetype.c L4358-4391):

c 复制代码
static inline FT_UInt FASTCALL
get_glyph_index_symbol(FT_Face ft_face, UINT glyph)
{
    FT_UInt ret;
    if (glyph < 0x100) glyph += 0xf000;      // Symbol 字体:U+00XX → U+f0XX 私用区
    /* 兼容老的非 Unicode "残缺" TTF:字形可能在 U+00XX */
    if (!(ret = FT_Get_Char_Index(ft_face, glyph)))
        ret = FT_Get_Char_Index(ft_face, glyph - 0xf000);
    return ret;
}

static inline FT_UInt FASTCALL
get_glyph_index(FT_Face ft_face, UINT glyph)
{
    FT_UInt ret;
    if (face_has_symbol_charmap(ft_face))     // 有 MS Symbol cmap?
    {
        ret = get_glyph_index_symbol(ft_face, glyph);
        if (ret != 0) return ret;
    }
    return FT_Get_Char_Index(ft_face, glyph); // ★ 常规:查当前 cmap
}

static inline FT_UInt FASTCALL
get_glyph_index_flagged(FT_Face face, FT_ULong code, BOOL fCodeAsIndex)
{
    return (fCodeAsIndex ? code : get_glyph_index(face, code));
    // ETO_GLYPH_INDEX / GGO_GLYPH_INDEX 时:调用者已传字形索引,跳过查表
}

face_has_symbol_charmap(L2700-2713)检查 platform_id == TT_PLATFORM_MICROSOFT && encoding == FT_ENCODING_MS_SYMBOL。Symbol 字体(如 Marlett、Wingdings)把字形放在 U+F000 私用区,码点映射有专门算法:0x00XX → 0xF0XX,查不到再试回 -0xf000

FT_Get_Char_Index 本身(freetype.h L3714)是 FreeType 的公共 API:用当前字符表(charmap)把字符码点翻译成字形索引,返回 0 表示"无此字形"。查表在 FreeType 内部按 cmap 格式(format 0/4/6/12/13/14...)分派,对调用者透明。

fCodeAsIndex 路径值得注意:ETO_GLYPH_INDEX 标志下应用传入的已经是字形索引 (不是码点),此时 IntExtTextOutW 直接把它当索引用,跳过查表------FontLink_Chain_FindGlyph 的第一个参数 fCodeAsIndex 即来自 (fuOptions & ETO_GLYPH_INDEX)(L7011-7012)。

6.3 字形加载与渲染:FT_Load_Glyph → FT_Glyph_To_Bitmap

IntGetRealGlyph(见 5.6)拿到字形索引后:

  1. FT_Load_Glyph(face, glyph_index, FT_LOAD_DEFAULT)(L5003):按当前字号(IntRequestFontSize 设好的 face->size)与变换(FT_Set_Transform 注入的矩阵)把字形轮廓加载进 face->glyph 槽(FT_GlyphSlot)。矢量字体得到 FT_GLYPH_FORMAT_OUTLINE(轮廓 + advance),点阵字体(.fon/.fnt)得到位图。
  2. 需要时 FT_GlyphSlot_Embolden(L5013)/ FT_GlyphSlot_Oblique(L5016)仿真粗体/斜体------当 LOGFONT 请求的 lfWeight > 字体原始字重lfItalic=1 而字体本身非斜体 时(Cache.Hashed.Aspect.Emu.Bold/Italic 由 L6873-6874 计算)。
  3. IntGetBitmapGlyphWithCacheFT_Get_Glyph(复制 glyph)→ FT_Glyph_To_Bitmap(&GlyphCopy, RenderMode, 0, 1)(L3708-3715)把轮廓光栅化 成位图。RenderMode 来自 IntGetFontRenderMode(plf)(L6876-6878):抗锯齿开启时为 FT_RENDER_MODE_NORMAL(8BPP 灰度)或更柔和的模式,关闭时为 FT_RENDER_MODE_MONO(1BPP 单色)。空位图兜底重试逻辑(L3729-3778)再依次试 LIGHT → NORMAL → MONO
  4. FT_Bitmap_Convert_ReactOS_Hack(..., 4, TRUE)(L3790-3791)把位图转为 4 字节行对齐,保证 EngCreateBitmap 要求的 pitch 对齐。

6.4 字形缓存:FONT_CACHE_ENTRY

FONT_CACHE_HASHED(font.h L40-51)是缓存键,FONT_CACHE_ENTRY(L55-61)是缓存项:

c 复制代码
typedef struct _FONT_CACHE_HASHED
{
    INT GlyphIndex;         // 字形索引
    FT_Face Face;           // 所在字体(★ 字体链接切换后可能是链接字体)
    LONG lfHeight;          // 字号
    LONG lfWidth;
    union { DWORD AspectValue; FONT_ASPECT Aspect; };  // EmuBold/EmuItalic + RenderMode
    FT_Matrix matTransform; // 变换矩阵(旋转/世界变换)
} FONT_CACHE_HASHED;

typedef struct _FONT_CACHE_ENTRY
{
    LIST_ENTRY ListEntry;
    FT_BitmapGlyph BitmapGlyph;   // 渲染结果
    DWORD dwHash;
    FONT_CACHE_HASHED Hashed;
} FONT_CACHE_ENTRY;

C_ASSERT(FIELD_OFFSET(FONT_CACHE_ENTRY, Hashed) % sizeof(DWORD) == 0)C_ASSERT(sizeof(FONT_CACHE_HASHED) % sizeof(DWORD) == 0)(L63-64)保证散列可以直接按 DWORD 数组读。IntFindGlyphCache 全字段比较(含 memcmp(matTransform)),命中即 LRU 提升;IntGetBitmapGlyphWithCache 未命中则光栅化并入表,超 256 项淘汰表尾。键里带了 Face,所以字体链接切换字体后各自缓存,互不串扰。

6.5 与字形测量 API 的关系

同一个"字符 → 字形索引 → 字形度量"管线还被测量类 API 复用:

  • NtGdiGetGlyphOutline(font.c L805-886):SEH 拷贝 pgm/pvBuf 后调 ftGdiGetGlyphOutline(freetype.c L4498-...),内部 get_glyph_index_flagged(ft_face, wch, (iFormat & GGO_GLYPH_INDEX))(L4590)拿索引,FT_Load_Glyph 后把 FT_Outline 转成 TTPOLYGONHEADER/TTPOLYCURVE 原生轮廓格式(get_native_glyph_outline,L3818-...)。
  • NtGdiGetCharWidthW(font.c L1318-...):get_glyph_index_flagged(face, Safepwc[i-FirstChar], (fl & GCW_INDICES))(L7666-7668)逐字符取宽。
  • NtGdiGetCharABCWidthsW(L1376):同样查索引后累加 ABC 宽度(L7562/7564)。
  • NtGdiGetGlyphIndicesW(L1440)→ NtGdiGetGlyphIndicesWInternal(L1454):批量查表,DefChar = get_glyph_index(Face, DefChar)(L7760)把默认字符也转成索引。

可见 get_glyph_index + FT_Get_Char_Index 是全部字形度量的公共底座,文本输出与度量共享同一套 cmap 语义。


7. 字形→字库映射(重点)

本节回答第二个"怎么":一个 LOGFONTW(逻辑字体)是怎么落到某个具体字体文件、最终产生 FT_Face 供字形渲染的 。链路:LOGFONT → 字体对象 → 字体文件 → FT_Face → FT_Set_Charmap → glyph 位图,中间还夹着字体替换字体链接两级回退。

7.1 LOGFONT → 字体对象(HFONT)

用户 CreateFontIndirectW(gdi32)→ NtGdiCreateFontIndirectW → 内核 GreCreateFontIndirectW(font.c L29-51):把 LOGFONTW 拷进 ENUMLOGFONTEXDVW(清空 FullName/Style/Script/DesignVector)后调 HfontCreate

HfontCreate(font.c L1223-1272)是通用的字体对象工厂:LFONT_AllocFontWithHandle() 从 GDI 句柄表分配 LFONT(含 logfont 字段保存 ENUMLOGFONTEXDVW 副本),拷贝 LOGFONT,初始化 ExInitializePushLock,并把 lfOrientation 修正为 lfEscapement(L1250-1256,GM_ADVANCED 未设时的兼容行为)。NtGdiHfontCreate(L1277-1313)先用 SEH ProbeForRead 拷贝用户态 ENUMLOGFONTEXDVW 再调用。返回的 HFONT 通过 SelectObject → DC_hSelectFont 进 DC 的 pdcattr->hlfntNew

7.2 字体对象 → 字体文件(懒实现 + 共享内存)

HFONT 只是句柄壳,真正的字体解析在 RealizeFontInit(懒初始化) 时发生:TEXTOBJ_LockText 后若 !(pTextObj->fl & TEXTOBJECT_INIT) 就调 TextIntRealizeFont(font.c L433-450)。

TextIntRealizeFont(freetype.c L6050-6160)的核心流程:

c 复制代码
pLogFont = &TextObj->logfont.elfEnumLogfontEx.elfLogFont;

/* 字体替换:请求的字体名找不到时换成替代字体 */
SubstitutedLogFont = *pLogFont;
SubstituteFontRecurse(&SubstitutedLogFont);

MatchPenalty = 0xFFFFFFFF;
TextObj->Font = NULL;
Win32Process = PsGetCurrentProcessWin32Process();

IntLockFreeType();
IntLockProcessPrivateFonts(Win32Process);
FindBestFontFromList(&TextObj->Font, &MatchPenalty, &SubstitutedLogFont,
                     &Win32Process->PrivateFontListHead);   // ① 进程私有字体
IntUnLockProcessPrivateFonts(Win32Process);
FindBestFontFromList(&TextObj->Font, &MatchPenalty, &SubstitutedLogFont,
                     &g_FontListHead);                      // ② 系统字体表

if (NULL == TextObj->Font)
    Status = STATUS_NOT_FOUND;                              // 一个字体都没有
else
{
    PFONTGDI FontGdi = ObjToGDI(TextObj->Font, FONT);
    PSHARED_FACE SharedFace = FontGdi->SharedFace;          // ★ 共享 FT_Face
    ...
    TextObj->Font->iUniq = 1;                               // 允许缓存
    IntFontType(FontGdi);
    FontGdi->RequestUnderline = pLogFont->lfUnderline ? 0xFF : 0;
    FontGdi->RequestStrikeOut = pLogFont->lfStrikeOut ? 0xFF : 0;
    FontGdi->RequestItalic    = pLogFont->lfItalic    ? 0xFF : 0;
    FontGdi->RequestWeight    = (pLogFont->lfWeight != FW_DONTCARE)
                              ? pLogFont->lfWeight : FW_NORMAL;
    TextObj->fl |= TEXTOBJECT_INIT;                         // 标记已实现
}

FindBestFontFromList 按 LOGFONT 各字段(家族名、字重、斜体、charset、点尺寸)计算匹配代价 ,选代价最小的 FONTOBJFontGdi->SharedFacePSHARED_FACE(engobjects.h L131-137):

c 复制代码
typedef struct _SHARED_FACE {
  FT_Face       Face;        // ★ FreeType 面对象(多进程共享)
  LONG          RefCount;
  PSHARED_MEM   Memory;      // 字体文件内存映射
  SHARED_FACE_CACHE EnglishUS;
  SHARED_FACE_CACHE UserLanguage;
} SHARED_FACE, *PSHARED_FACE;

PFONTGDI(engobjects.h L139-168)是字体对象的核心:FontObj(FONTOBJ 内嵌)、dhpdevSharedFace、请求属性(RequestUnderline/StrikeOut/Italic/Weight)、原始属性(OriginalItalic/Weight,用于判断是否需要仿真)、预计算度量(tmHeight/tmAscent/tmDescent/...)、Magic=FONTGDI_MAGIC 0x20110311

7.3 字体文件 → FT_Face

字体文件在系统初始化/AddFontResource 时被加载进内存并创建 FT_Face

  • IntLoadSystemFonts (freetype.c L1378-...):枚举 \SystemRoot\Fonts\ 目录,匹配 *.ttf / *.ttc / *.otf / *.otc / *.fon / *.fnt(L1389-1397),逐个 IntGdiAddFontResourceSingle
  • IntGdiAddFontResourceSingle (L1876-...):ZwCreateFile 打开字体文件 → 建 Section → 映射进系统空间(MmMapViewInSystemSpace),得到 SHARED_MEM(Buffer/Size/IsMapping/RefCount)。
  • IntGdiLoadFontByIndexFromMemory(L1822-1871):
c 复制代码
Error = FT_New_Memory_Face(g_FreeTypeLibrary,
                           pLoadFont->Memory->Buffer, pLoadFont->Memory->BufferSize,
                           ((FontIndex == -1) ? 0 : FontIndex), &Face);
pLoadFont->IsTrueType = FT_IS_SFNT(Face);
num_faces = Face->num_faces;
SharedFace = SharedFace_Create(Face, pLoadFont->Memory);   // 包一层共享结构

FT_New_Memory_Face(FreeType 公共 API,位于 freetype.h)用只读内存 创建面对象------字体文件整个映射在系统地址空间,所有进程的同一字体共享同一个 FT_Face,这就是 SHARED_FACE.RefCount 的意义。.ttc(TrueType Collection)每个字体索引一个 FT_Face,递归 IntGdiLoadFontByIndexFromMemory 遍历 num_faces(L1860-1867)。

7.4 选字符表:FT_Set_Charmap

TextIntUpdateSize(freetype.c L4269-4356)在每次文本输出/度量前确保两件事:① face 有正确的 charmap;② 字号正确。charmap 选择优先级(L4284-4339):

  1. FT_ENCODING_UNICODE(Unicode cmap);
  2. 没有则 platform_id == TT_PLATFORM_APPLE_UNICODE(Apple 旧式 Unicode);
  3. 再没有则 FT_ENCODING_MS_SYMBOL(Symbol 字体);
  4. 兜底 face->charmaps[0]

选定后 FT_Set_Charmap(face, found)(L4334)。IntSelectFaceCharmap(L7461-...)是同一个逻辑的简化版(face->charmap 已选则直接返回,否则找第一个 encoding != 0 的 cmap)。cmap 的选择直接决定第 6 章 FT_Get_Char_Index 查哪张表------Unicode 码点 vs Symbol 私用区的分派就在这一层定死。

字号设置 IntRequestFontSize(L4016-4145)读 OS/2 表与 hhea 表换算 tmAscent/tmDescent/tmHeight/tmInternalLeading,按 lfHeight 正负分两套算法(正数=点尺寸直接用,负数=像素高度),最后:

c 复制代码
req.type = FT_SIZE_REQUEST_TYPE_NOMINAL;
req.height = (EmHeight << 6);
return FT_Request_Size(face, &req);

FT_Request_Sizefreetype.h L2661)让 FreeType 按 26.6 定点尺寸计算 face->size->metricsx_scale/y_scale/x_ppem/y_ppem)------此后 FT_Load_Glyph 都按这个尺寸缩放轮廓。

7.5 字体替换(FontSubst)与字体链接(FontLink)的回退链

两条回退机制容易混淆,对比说明:

维度 字体替换 SubstituteFontRecurse 字体链接 FontLink_Chain_FindGlyph
触发时机 字体选择时TextIntRealizeFont L6084 / FontLink_Create L1249) 逐字符渲染时IntExtTextOutW L7011)
判定依据 lfFaceName 匹配替换表 g_FontSubstListHead(注册表 HKLM\...\FontSubstitutes 默认字体 FT_Get_Char_Index 返回 0(无此字形)
动作 整体换用另一字体的 FT_Face 同一行内按字符切换 fallback 字体的 FT_Face
数据来源 FONTSUBST_ENTRY{FontNames[2], CharSets[2]} FONTLINK_ENTRY / 默认 s_szzDefFontLink
失败兜底 保持原名继续找 s_chFontLinkDefaultChar(U+30FB)默认字形

SubstituteFontRecurse (L1140-...)最多递归 5 层:SubstituteFontByList(L1078-1137)遍历 g_FontSubstListHead,按 RequestedCharSetFONTSUBST_FROM_AND_TO 映射表匹配,命中就把 lfFaceName 换成替代名,直到找不到替换为止------典型例子是 Tahoma → MS Shell Dlg 这类注册表项。

FontLink 链 在本册 5.7 已展开:默认字体查不到字形 → 沿 s_szzDefFontLink(Tahoma→MS UI Gothic→PMingLiU→SimSun→Gulim)逐字体 get_glyph_index;命中则把 pCache->Hashed.Face 换成链接字体并 FT_Set_Transform 重新注入变换(L4480-4484)------注意字形缓存键含 Face,因此同一字符在不同字体上的位图是分开缓存的 。整条链都查不到才画 U+30FB 占位符。

7.6 一张图总结字库映射

复制代码
LOGFONTW{SimSun, -12, FW_NORMAL, GB2312_CHARSET}
   │  SubstituteFontRecurse(FontSubstitutes 表,可能改名)
   ▼
TextIntRealizeFont → FindBestFontFromList(私有表 + 系统表,代价最小匹配)
   ▼
FONTOBJ → PFONTGDI{SharedFace, RequestWeight, OriginalItalic, tmAscent...}
   │        SharedFace = PSHARED_FACE{Face, Memory=字体文件映射}
   ▼
FT_Face(来自 FT_New_Memory_Face,系统空间只读映射)
   │  TextIntUpdateSize:FT_Set_Charmap(Unicode/Apple/Symbol/第一个)
   │  IntRequestFontSize:FT_Request_Size(lfHeight → 26.6 尺寸)
   │  IntExtTextOutW:FT_Set_Transform(lfEscapement × 世界变换)
   ▼
FT_Get_Char_Index(face, ch) → glyph index ── 查不到 ──► FontLink 链逐字体重查
   ▼
FT_Load_Glyph → 轮廓 → FT_Glyph_To_Bitmap → 8BPP 位图 → 字形缓存

8. 文本→显示输出(重点)

本节回答第三个"怎么":字形位图最终是怎么变成目标表面像素、进而到达显示缓冲区的 。链路:EngCreateBitmap(包装)→ IntEngMaskBlt(掩码合成)→ 像素写入 → 显示缓冲。

8.1 目标表面:SURFOBJ 抽象与 DC 表面

一切绘制都发生在"表面"上。IntExtTextOutW 开头就取了目标表面(L6819-6820):

c 复制代码
psurf = dc->dclevel.pSurface;
psoDest = &psurf->SurfObj;      // SURFOBJ:引擎统一的表面描述

SURFOBJ(DDK 风格结构,eng/engobjects.h 一带定义)关键字段:iTypeSTYPE_BITMAP=内存 DIB / STYPE_DEVICE=设备表面)、iBitmapFormatBMF_1BPP/4BPP/8BPP/16BPP/24BPP/32BPP)、pvScan0(像素基址)、lDelta(行距)、sizlBitmap(宽高)、hdev/dhsurf(所属设备)。显示驱动通过 DrvEnablePDEV/DrvEnableSurface 创建设备表面,dhsurf 指向驱动私有句柄。

IntEngMaskBlt 在 L1090 判断 psoDest->iType != STYPE_BITMAP------设备表面(如直接写显存)不能由引擎直接逐像素操作 ,于是建临时 DIB 表面:EngCopyBits 把目标矩形拷进临时表面 → 在临时表面上做 EngMaskBitBltEngCopyBits 拷回(L1190-1220)。这保证了引擎的软件合成逻辑只面对内存 DIB,而设备表面的读写由 EngCopyBits 统一调度(可能走驱动的 DrvCopyBits)。framebuf 类驱动(win32ss/drivers/displays/framebuf/)通常把帧缓冲直接作为 DIB 表面,iType == STYPE_BITMAP,走直接路径,像素直写显存映射------这就是"文本到显示缓冲区"的最后一跳

8.2 掩码表面:EngCreateBitmap 零拷贝包装

主循环 L7073-7075:

c 复制代码
hbmGlyph = EngCreateBitmap(glyphSize, realglyph->bitmap.pitch,
                           BMF_8BPP, BMF_TOPDOWN,
                           realglyph->bitmap.buffer);   // pvBits 直接引用 FT 位图

EngCreateBitmapeng/surface.c L306-344)= SURFACE_AllocSurface(STYPE_BITMAP, cx, cy, iFormat, fl, lWidth, 0, pvBits) + 建句柄 + API_BITMAP 标记。传入 pvBits 时表面不分配自己的像素缓冲,直接包装外部内存 ------这里就是 FreeType 刚光栅化出的 8BPP 覆盖度位图,零拷贝。随后 EngLockSurface((HSURF)hbmGlyph)(L7083)把句柄换成可用的 SURFOBJ* psoGlyph,用完 EngUnlockSurface + EngDeleteSurface(L7126-7127)------删除只释放表面包装结构,不碰 FT 位图内存(由 FONT_CACHE_ENTRY 持有)。

8.3 掩码合成:IntEngMaskBlt 的参数语义

L7113-7121 的调用把所有参与者一次传齐:

c 复制代码
IntEngMaskBlt(psoDest,                       // ① 目标:DC 表面
              psoGlyph,                      // ② 掩码:字形 8BPP 位图
              (CLIPOBJ *)&dc->co,            // ③ 裁剪:DC 的 XCLIPOBJ(含可见区域/更新区域)
              &exloRGB2Dst.xlo,              // ④ RGB→目标色翻译(画刷色 → 目标颜色空间)
              &exloDst2RGB.xlo,              // ⑤ 目标→RGB 翻译(读背景做混合用)
              &DestRect,                     // ⑥ 目标矩形(设备坐标)
              (PPOINTL)&MaskRect,            // ⑦ 掩码源矩形起点
              &dc->eboText.BrushObject,      // ⑧ 文本画刷(前景色实体画刷)
              &g_PointZero)                  // ⑨ 画刷原点

其中 dc->co 是 DC 内嵌的 XCLIPOBJ(dc.h L130),其裁剪区域包含窗口可见区域(prgnVis)等合成结果------文本天然被窗口裁剪。dc->eboTextEBRUSHOBJ(扩展画刷对象),DC_vUpdateTextBrush(dcobjs.c L106-122)把它的纯色设为 pdcattr->crForegroundClr

8.4 像素合成:AlphaBltMask 与 BltMask

IntEngMaskBlt 按掩码格式分流(1BPP → IntEngBitBlt + ROP4_MASKPAINT;8BPP → EngMaskBitBlt)。8BPP 路径最终落进 AlphaBltMask(bitblt.c L823-890,5.8 已贴代码)------每个掩码字节当作 alpha 覆盖度

  • 掩码 == 0xFF:目标像素直接写画刷色(完全不透明);
  • 0 < 掩码 < 0xFF新色 = 掩码×(画刷色−背景色)/255 + 背景色,经 pxloRGB2Dest 翻译后写入(半透明抗锯齿);
  • 掩码 == 0:不动目标像素(全透明)。

1BPP 路径 BltMask(L47-...)则按掩码位选 fgndRop/bkgndRop 走 ROP4(如 ROP4_MASKPAINT = 前景用 DSTINVERT 类 ROP),逐像素 DIB_GetPixel/DIB_PutPixel。两个函数都通过 DibFunctionsForBitmapFormat[psoDest->iBitmapFormat] 函数表分派,天然支持 1/4/8/16/24/32bpp 目标。

8.5 文本像素流的完整拼图

把第 5、6、7 章的结论拼起来,一次 TextOutW("A") 的像素级旅程:

复制代码
"A"(U+0041)
  → FT_Get_Char_Index(SimSun, 0x41) = 36              [字符 → 字形索引]
  → IntGetRealGlyph: FT_Load_Glyph(face, 36)          [加载轮廓]
      → FT_Glyph_To_Bitmap(..., NORMAL)               [光栅化 8BPP 覆盖度]
      → 缓存进 g_FontCacheListHead
  → EngCreateBitmap(12×13, BMF_8BPP, buffer)          [包装掩码表面]
  → IntEngMaskBlt(psoDest, psoGlyph, &dc->co,         [掩码 + 画刷合成]
                  exlo×2, &DestRect, &MaskRect,
                  &dc->eboText, &g_PointZero)
      → EngMaskBitBlt → AlphaBltMask
          → 逐像素: DIB_PutPixel(psoDest, x, y,       [写目标表面像素]
                                 画刷色或混合色)
  → 目标 = 窗口 DC 表面(内存 DIB / 帧缓冲映射)
  → 显示驱动刷新 / 引擎直接落显存 → 屏幕

若目标是调色板设备(8bpp 调色板模式),AlphaBltMask 写像素前还会 XLATEOBJ_iXlate(pxloRGB2Dest, NewColor) 把 RGB 换成调色板索引------这就是 EXLATEOBJ 双向翻译存在的意义(L6983-6984 初始化,eng/xlateobj.c L491+)。


9. 关键设计思想

9.1 GDI Batch:以"延迟执行 + 属性快照"换系统调用

批处理把 TextOutW 的 syscall 从"每次调用一次"降为"每缓冲一次"。为此付出的代价是执行时刻与调用时刻解耦,所以:

  • 命令结构体里快照了 crForegroundClr/crBackgroundClr/ulForegroundClr/ulBackgroundClr/lBkMode/hlfntNew/flTextAlign/ptlViewportOrg 八个属性(text.c L619-626);
  • 内核 GdiFlushUserBatch 执行前用快照覆盖 当前 pdcattr,执行后恢复(gdibatch.c L280-355)------像事务一样隔离;
  • 部分语义天然不能批TA_UPDATECP(依赖前一次调用的笔位)与 DC_DIBSECTION(DIB 段需要精确同步)直接走直连 NtGdiExtTextOutW(text.c L549-552)。

9.2 DC 属性快照:批处理延迟执行的必然产物

"为何要快照"的完整答案:批命令在 TEB 里排队期间,应用可能又调了 SetTextColor/SelectObject/SetViewportOrgEx------如果不快照,等到 flush 时画出来的就是后来的 属性,语义错误。快照 + 还原让每个批命令自带执行上下文,等价于"在调用时刻拍照、在执行时刻重放"。

9.3 SEH 用户态内存安全:一次探测,全程安心

NtGdiExtTextOutW 在入口把所有用户态指针(String/Dx/Rect)ProbeForRead + 拷贝进内核缓冲(栈优先、池兜底)。好处:

  1. 安全 :坏指针被 _SEH2_EXCEPT 捕获,不会蓝屏;
  2. 性能:主循环里逐字符访问的都是内核地址,免去每字符探测;
  3. 一致性:拷贝后应用并发修改原字符串不影响本次输出。

9.4 字形缓存:以"键"换"算"

FONT_CACHE_ENTRY 的键 FONT_CACHE_HASHED{GlyphIndex, Face, lfHeight, lfWidth, AspectValue, matTransform} 精确刻画了"同一个字形位图"的全部决定因素。256 项 LRU + 字体卸载时按 Face 批量清除(RemoveCacheEntries L472-492),使缓存既快又不悬挂。散列用 pshpack1 打包保证按 DWORD 对齐读(font.h L26/63-64)。

9.5 掩码刷色模型:文本 = 形状 × 颜色

文本不携带颜色:字形只是形状掩码 ,颜色由 eboText 画刷提供,合成在引擎层完成。这套模型的三点收益:

  • 颜色与形状解耦 :换 SetTextColor 不必重新光栅化字形(缓存命中即刷新色);
  • 复用现成 BLT 管线AlphaBltMask/BltMask 同时服务位图掩码与字形掩码,文本绘制零新增 DDI;
  • 设备无关 :无论目标 1bpp 还是 32bpp、内存还是显存,掩码合成的语义一致,只差 DibFunctionsForBitmapFormat 分派。

9.6 颜色翻译:EXLATEOBJ 的"两端各翻一次"

调色板设备(8bpp)上文本画刷颜色与目标表面颜色空间不同,需要翻译。IntExtTextOutW 同时初始化两个方向(L6983-6984):exloRGB2Dst(画刷 RGB → 目标索引)与 exloDst2RGB(目标索引 → RGB,供 AlphaBltMask 读背景混合)。ppalDst==ppalSrc 时置 XO_TRIVIAL 恒等翻译(xlateobj.c L517-521),24/32bpp 真彩目标几乎零开销。

9.7 字体替换与链接:两级"找不到"兜底

  • 字体替换在"选字体"阶段兜底:名字不存在 → 换一个名字相近的字体(FontSubstitutes 注册表)。
  • 字体链接在"选字形"阶段兜底:字体存在但缺某个字形 → 沿链接链换字体画这个字(东亚字体混排的核心机制)。
  • 两层都失败 → U+30FB 占位符,保证永远能画出点东西,绝不因缺字形失败返回。

9.8 26.6 定点数:贯穿全程的精度语言

坐标、advance、字距全部以 <<6(1/64 像素)表示,渲染时 +32 >> 6 四舍五入回整数像素(L6958/7060/7198 等)。旋转文本(lfEscapement)时 26.6 精度保证逐字拼接不漂移;FreeType 的 advance 是 16.16,统一按 >>10 缩回 26.6 后再参与运算(L7135)。


10. 实现技术清单

技术点 位置 说明
FreeType 集成 ntgdi/freetype.c + sdk/lib/3rdparty/freetype 字体解析/光栅化/度量统一走 FreeType,全局锁 IntLockFreeType 保护
surrogate 处理 utils.h L38-50;freetype.c L6999-7009 UTF-16 代理对 → UTF-32 码点,孤立代理容错
kerning 字距 freetype.c L6930、L7023-7028、L6688-6693 FT_HAS_KERNING 检测 + FT_Get_Kerning,仅无 Dx 时生效
TA_* 文本对齐 freetype.c L6801-6810、L6913-6928、L6944-6953 TA_UPDATECP 笔位、TA_BASELINE/TOP/BOTTOM 纵对齐、TA_CENTER/RIGHT 横对齐(配合 IntGetTextDisposition 预测量)
ETO_* 选项 text.c L503-517;freetype.c L6830-6853、L7096-7111 ETO_OPAQUE 背景、ETO_CLIPPED 裁剪、ETO_PDY 成对 Dx、ETO_GLYPH_INDEX 直接索引
字形位图格式 freetype.c L7073-7075 BMF_8BPP 覆盖度掩码(抗锯齿)/ BMF_1BPP(MONO),FT_Bitmap_Convert_ReactOS_Hack 对齐
引擎表面抽象 eng/surface.c L306-344;SURFOBJ EngCreateBitmap/EngLockSurface/EngDeleteSurfaceiType/iBitmapFormat/pvScan0/lDelta
GDI Batch gdi32 GdiAllocBatchCommand;ntgdi GdiFlushUserBatch TEB 批缓冲、属性快照、580 字符上限
SEH 探测拷贝 freetype.c L7392-7438 ProbeForRead + RtlCopyMemory + _SEH2_TRY
字形缓存 font.h L40-64;freetype.c L3658-3815 哈希 + 全字段比对 + 256 项 LRU
字体替换 freetype.c L1078-1137、L1140-... SubstituteFontRecurse 递归 5 层,FontSubstitutes 表
字体链接 freetype.c L1176-1370、L4438-4493 FontLink_Chain_FindGlyph 逐字体回退
仿真粗体/斜体 freetype.c L5012-5016 FT_GlyphSlot_Embolden/Oblique
路径文本 path.c L2416-...;freetype.c L6781-6789 PATH_ExtTextOut + GGO_NATIVE 轮廓
颜色翻译 eng/xlateobj.c L491+ EXLATEOBJ 双向翻译,XO_TRIVIAL 快速路径
掩码合成 eng/bitblt.c L823-890、L894-...、L1104-1240 AlphaBltMask/BltMask/IntEngMaskBlt
元文件/语言包 text.c L519-546 HANDLE_METADCLoadLPK(LPK_ETO)

11. 调用链 mermaid 图

11.1 用户态批处理链

win32k.sys 线程 TEB 批缓冲 gdi32.dll 应用 win32k.sys 线程 TEB 批缓冲 gdi32.dll 应用 #mermaid-svg-SaVDRjgqKeCp56kf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SaVDRjgqKeCp56kf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SaVDRjgqKeCp56kf .error-icon{fill:#552222;}#mermaid-svg-SaVDRjgqKeCp56kf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SaVDRjgqKeCp56kf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SaVDRjgqKeCp56kf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SaVDRjgqKeCp56kf .marker.cross{stroke:#333333;}#mermaid-svg-SaVDRjgqKeCp56kf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SaVDRjgqKeCp56kf p{margin:0;}#mermaid-svg-SaVDRjgqKeCp56kf .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SaVDRjgqKeCp56kf text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-SaVDRjgqKeCp56kf .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SaVDRjgqKeCp56kf .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-SaVDRjgqKeCp56kf .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-SaVDRjgqKeCp56kf .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-SaVDRjgqKeCp56kf #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-SaVDRjgqKeCp56kf .sequenceNumber{fill:white;}#mermaid-svg-SaVDRjgqKeCp56kf #sequencenumber{fill:#333;}#mermaid-svg-SaVDRjgqKeCp56kf #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-SaVDRjgqKeCp56kf .messageText{fill:#333;stroke:none;}#mermaid-svg-SaVDRjgqKeCp56kf .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SaVDRjgqKeCp56kf .labelText,#mermaid-svg-SaVDRjgqKeCp56kf .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-SaVDRjgqKeCp56kf .loopText,#mermaid-svg-SaVDRjgqKeCp56kf .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-SaVDRjgqKeCp56kf .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SaVDRjgqKeCp56kf .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-SaVDRjgqKeCp56kf .noteText,#mermaid-svg-SaVDRjgqKeCp56kf .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-SaVDRjgqKeCp56kf .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SaVDRjgqKeCp56kf .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SaVDRjgqKeCp56kf .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SaVDRjgqKeCp56kf .actorPopupMenu{position:absolute;}#mermaid-svg-SaVDRjgqKeCp56kf .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-SaVDRjgqKeCp56kf .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SaVDRjgqKeCp56kf .actor-man circle,#mermaid-svg-SaVDRjgqKeCp56kf line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-SaVDRjgqKeCp56kf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命令排队,不立即进内核 alt 元文件 DC LPK 可用 可批处理(≤580 字符、无 TA_UPDATECP、非 DIB 段 DC) 直连 TextOutW(hdc, x, y, L"...", n) TextOutW → ExtTextOutW(参数整理) HANDLE_METADC → EMF 记录 LoadLPK(LPK_ETO) → LpkExtTextOut GdiAllocBatchCommand(GdiBCTextOut) 快照 8 个 DC 属性 + Dx/String 打包 返回 TRUE 继续其它 GDI 调用... NtGdiFlush(缓冲满/显式刷新) GdiFlushUserBatch 逐条还原快照 → IntExtTextOutW NtGdiExtTextOutW(syscall)

11.2 内核全链

#mermaid-svg-VyqUj7oVAz60Zo2G{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VyqUj7oVAz60Zo2G .error-icon{fill:#552222;}#mermaid-svg-VyqUj7oVAz60Zo2G .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VyqUj7oVAz60Zo2G .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VyqUj7oVAz60Zo2G .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VyqUj7oVAz60Zo2G .marker.cross{stroke:#333333;}#mermaid-svg-VyqUj7oVAz60Zo2G svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VyqUj7oVAz60Zo2G p{margin:0;}#mermaid-svg-VyqUj7oVAz60Zo2G .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster-label text{fill:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster-label span{color:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster-label span p{background-color:transparent;}#mermaid-svg-VyqUj7oVAz60Zo2G .label text,#mermaid-svg-VyqUj7oVAz60Zo2G span{fill:#333;color:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G .node rect,#mermaid-svg-VyqUj7oVAz60Zo2G .node circle,#mermaid-svg-VyqUj7oVAz60Zo2G .node ellipse,#mermaid-svg-VyqUj7oVAz60Zo2G .node polygon,#mermaid-svg-VyqUj7oVAz60Zo2G .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VyqUj7oVAz60Zo2G .rough-node .label text,#mermaid-svg-VyqUj7oVAz60Zo2G .node .label text,#mermaid-svg-VyqUj7oVAz60Zo2G .image-shape .label,#mermaid-svg-VyqUj7oVAz60Zo2G .icon-shape .label{text-anchor:middle;}#mermaid-svg-VyqUj7oVAz60Zo2G .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VyqUj7oVAz60Zo2G .rough-node .label,#mermaid-svg-VyqUj7oVAz60Zo2G .node .label,#mermaid-svg-VyqUj7oVAz60Zo2G .image-shape .label,#mermaid-svg-VyqUj7oVAz60Zo2G .icon-shape .label{text-align:center;}#mermaid-svg-VyqUj7oVAz60Zo2G .node.clickable{cursor:pointer;}#mermaid-svg-VyqUj7oVAz60Zo2G .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VyqUj7oVAz60Zo2G .arrowheadPath{fill:#333333;}#mermaid-svg-VyqUj7oVAz60Zo2G .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VyqUj7oVAz60Zo2G .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VyqUj7oVAz60Zo2G .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VyqUj7oVAz60Zo2G .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VyqUj7oVAz60Zo2G .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VyqUj7oVAz60Zo2G .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster text{fill:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G .cluster span{color:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VyqUj7oVAz60Zo2G .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VyqUj7oVAz60Zo2G rect.text{fill:none;stroke-width:0;}#mermaid-svg-VyqUj7oVAz60Zo2G .icon-shape,#mermaid-svg-VyqUj7oVAz60Zo2G .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VyqUj7oVAz60Zo2G .icon-shape p,#mermaid-svg-VyqUj7oVAz60Zo2G .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VyqUj7oVAz60Zo2G .icon-shape .label rect,#mermaid-svg-VyqUj7oVAz60Zo2G .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VyqUj7oVAz60Zo2G .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VyqUj7oVAz60Zo2G .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VyqUj7oVAz60Zo2G :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



NtGdiExtTextOutW freetype.c L7342

SEH 探测拷贝 String/Dx/Rect
GreExtTextOutW L7300

DC_LockDc
IntExtTextOutW L6729
PATH 打开?
PATH_ExtTextOut path.c L2416

ftGdiGetGlyphOutline GGO_NATIVE
DC_vPrepareDCsForBlit dclife.c L505
RealizeFontInit font.c L433

→ TEXTOBJ/FONTOBJ/PFONTGDI
TextIntUpdateSize L4269

FT_Set_Charmap + IntRequestFontSize
FontLink_Chain_Init L1176
FT_Set_Transform L6898

lfEscapement × 世界变换
IntGetTextDisposition L6643

TA_CENTER/TA_RIGHT 预测量
还有字符?
FontLink_Chain_FindGlyph L4438

→ glyph_index
IntGetRealGlyph L4988

FT_Load_Glyph + 渲染 + 缓存
FT_Get_Kerning L7025
EngCreateBitmap L7073

8BPP 掩码
IntEngMaskBlt bitblt.c L1104

掩码 + eboText 画刷
EngMaskBitBlt L894

→ AlphaBltMask L823 / BltMask L47
advance 推进 L7133
下划线/删除线 L7165

IntEngFillBox/Polygon
DC_vFinishBlit + TEXTOBJ_UnlockText

L7288

11.3 字体映射链

#mermaid-svg-9Csttc7I3b7wnr6Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9Csttc7I3b7wnr6Y .error-icon{fill:#552222;}#mermaid-svg-9Csttc7I3b7wnr6Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9Csttc7I3b7wnr6Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9Csttc7I3b7wnr6Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9Csttc7I3b7wnr6Y .marker.cross{stroke:#333333;}#mermaid-svg-9Csttc7I3b7wnr6Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9Csttc7I3b7wnr6Y p{margin:0;}#mermaid-svg-9Csttc7I3b7wnr6Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster-label text{fill:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster-label span{color:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster-label span p{background-color:transparent;}#mermaid-svg-9Csttc7I3b7wnr6Y .label text,#mermaid-svg-9Csttc7I3b7wnr6Y span{fill:#333;color:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y .node rect,#mermaid-svg-9Csttc7I3b7wnr6Y .node circle,#mermaid-svg-9Csttc7I3b7wnr6Y .node ellipse,#mermaid-svg-9Csttc7I3b7wnr6Y .node polygon,#mermaid-svg-9Csttc7I3b7wnr6Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9Csttc7I3b7wnr6Y .rough-node .label text,#mermaid-svg-9Csttc7I3b7wnr6Y .node .label text,#mermaid-svg-9Csttc7I3b7wnr6Y .image-shape .label,#mermaid-svg-9Csttc7I3b7wnr6Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-9Csttc7I3b7wnr6Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9Csttc7I3b7wnr6Y .rough-node .label,#mermaid-svg-9Csttc7I3b7wnr6Y .node .label,#mermaid-svg-9Csttc7I3b7wnr6Y .image-shape .label,#mermaid-svg-9Csttc7I3b7wnr6Y .icon-shape .label{text-align:center;}#mermaid-svg-9Csttc7I3b7wnr6Y .node.clickable{cursor:pointer;}#mermaid-svg-9Csttc7I3b7wnr6Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9Csttc7I3b7wnr6Y .arrowheadPath{fill:#333333;}#mermaid-svg-9Csttc7I3b7wnr6Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9Csttc7I3b7wnr6Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9Csttc7I3b7wnr6Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9Csttc7I3b7wnr6Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9Csttc7I3b7wnr6Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9Csttc7I3b7wnr6Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster text{fill:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y .cluster span{color:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9Csttc7I3b7wnr6Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9Csttc7I3b7wnr6Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-9Csttc7I3b7wnr6Y .icon-shape,#mermaid-svg-9Csttc7I3b7wnr6Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9Csttc7I3b7wnr6Y .icon-shape p,#mermaid-svg-9Csttc7I3b7wnr6Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9Csttc7I3b7wnr6Y .icon-shape .label rect,#mermaid-svg-9Csttc7I3b7wnr6Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9Csttc7I3b7wnr6Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9Csttc7I3b7wnr6Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9Csttc7I3b7wnr6Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 缺字形时
LOGFONTW
GreCreateFontIndirectW font.c L30
HfontCreate font.c L1223

LFONT + HFONT
SelectObject → hlfntNew
RealizeFontInit → TextIntRealizeFont L6050

SubstituteFontRecurse + FindBestFontFromList
PFONTGDI engobjects.h L139

SharedFace
PSHARED_FACE engobjects.h L131

FT_Face + 文件映射
TextIntUpdateSize L4269

FT_Set_Charmap 选 Unicode cmap
IntRequestFontSize L4016

FT_Request_Size 设字号
FT_Get_Char_Index

freetype.h L3714

码点 → 字形索引
FT_Load_Glyph

freetype.h L2793
FT_Glyph_To_Bitmap

ftglyph.h → 8BPP 位图
FONT_CACHE_ENTRY

font.h L55 全局缓存 256 LRU
FontLink_Chain_FindGlyph L4438

s_szzDefFontLink 回退链


12. 与 _30 / GDI 引擎 / 显示驱动的关系

12.1 与第 30 册(USER 与 GDI 双子系统)的关系

_30 给出的是分工骨架DrawTextW(user32,排版语义)→ TextOutW(gdi32,参数整理)→ NtGdiExtTextOutW(内核 GDI);本册是实现血肉。两册的对照:

层面 _30 的结论 本册的深化
用户态入口 TextOutW L63 → ExtTextOutW L491 → NtGdiExtTextOutW L645 补充 META/LPK/批处理三分支(text.c L503-646),快照 8 属性
内核入口 NtGdiExtTextOutW L7342 → GreExtTextOutW L7300 → IntExtTextOutW L6727 展开 SEH 探测拷贝(L7392-7438)与 580 字符批上限
引擎调度 "应调 EngTextOut→DrvTextOut(FIXME)" 说明 ReactOS 当前在 IntExtTextOutW 内用 IntEngMaskBlt 完成合成,DrvTextOut 留作 FIXME
文本模型 文本走 FreeType 逐字取字形(FT_BitmapGlyph 展开"文本=掩码×画刷"模型与 AlphaBltMask 像素算法

_30 第 7.2 节还提到内核 USER 的 GreTextOutWntgdi/text.c L59-68)画非客户区字符------它转发到 GreExtTextOutW与本册的 IntExtTextOutW 是同一实现:Marlett 字体符号(单选框/标题栏按钮)与普通文本共用整条管线,只是字体对象是 Marlett。

12.2 与 GDI 引擎(eng/)的关系

ntgdi(内核 GDI)负责语义与字体eng/(引擎)负责像素搬运

  • 表面抽象:EngCreateBitmap/EngLockSurface/EngDeleteSurface(eng/surface.c)------文本主循环用它包装字形位图;
  • 掩码合成:IntEngMaskBlt → EngMaskBitBlt → AlphaBltMask/BltMask(eng/bitblt.c)------文本绘制的像素级实现;
  • 颜色翻译:EXLATEOBJ(eng/xlateobj.c)------调色板设备的两端翻译;
  • 画刷:EBRUSHOBJ/BRUSHOBJ(eboText/eboBackground)------文本颜色载体。

文本是 GDI 中少数完全由引擎软件合成 的高级原语之一(另如 StretchBlt/AlphaBlend),不要求驱动实现 DrvTextOut;驱动只需做好表面与 EngCopyBits,即可正确显示文字。

12.3 与显示驱动的关系

  • framebuf/vga 类驱动 :帧缓冲作为 DIB 表面(STYPE_BITMAP),文本像素由 AlphaBltMask 直写显存映射,无需驱动参与;
  • 驱动实现 DrvTextOut 的路径 (未来/硬件加速场景):IntExtTextOutW 的 FIXME 注释(L6740-6744)指明应调 EngTextOut → DrvTextOut,驱动接管整行文本的光栅化与合成;ReactOS 当前显示驱动均未实现该 DDI,所以回退到引擎软件路径;
  • SynchronizeSurface/Synchronize 回调 (gdibatch.c L19-40):批 flush 前驱动可收到同步通知(NtGdiFlushSynchronizeDriver(GCAPS2_SYNCFLUSH) L474)。

13. 源码索引

13.1 用户态(gdi32.dll)

文件 函数/位置 说明
win32ss/gdi/gdi32/objects/text.c TextOutA L17-58 ANSI→Unicode 转换
TextOutW L64-74 折叠成 ExtTextOutW
ExtTextOutW L489-654 参数整理/元文件/LPK/批处理/直连
GetTextFaceW L660-677 字体名查询(NtGdiGetTextFaceW
win32ss/gdi/gdi32/gdi32.spec TextOutW L593、ExtTextOutW L223 导出表
win32ss/gdi/gdi32/misc/misc.c GdiFlush L44-46 显式 flush 批缓冲
win32ss/include/ntgdityp.h GDIBSTEXTOUT L509-531、GDIBSEXTTEXTOUT L533-541 批命令结构体(含快照字段)

13.2 内核(win32k.sys)

文件 函数/位置 说明
win32ss/gdi/ntgdi/freetype.c IntExtTextOutW L6727-7295 文本渲染主流程
IntGetTextDisposition L6643-6725 预测量宽(对齐/背景)
GreExtTextOutW L7298-7336 DC 锁 + 转发
NtGdiExtTextOutW L7340-7459 SEH 探测拷贝(系统调用)
IntSelectFaceCharmap L7461-... charmap 简化选择
IntGetRealGlyph L4987-5024 字形加载/仿真/渲染
IntFindGlyphCache L3658-3693、IntGetBitmapGlyphWithCache L3695-3815 字形缓存
get_glyph_index L4372-4385、get_glyph_index_symbol L4358-4370 码点→字形索引
TextIntUpdateSize L4269-4356 cmap 选择 + 字号
IntRequestFontSize L4016-4145 lfHeight→FT_Request_Size
FontLink_Chain_FindGlyph L4438-4493、_Init L1176-1185、_Populate L1302-1370 字体链接
SubstituteFontByList L1078-1137、SubstituteFontRecurse L1140-... 字体替换
TextIntRealizeFont L6050-6160 字体懒实现
IntLoadSystemFonts L1378-...、IntGdiLoadFontByIndexFromMemory L1822-1871 字体加载(FT_New_Memory_Face)
IntEscapeMatrix L1037-1047、IntMatrixFromMx L1049-1076 变换矩阵
TextIntGetTextExtentPoint L5028-... 文本度量(共享管线)
win32ss/gdi/ntgdi/font.c GreCreateFontIndirectW L29-51、HfontCreate L1223-1272、NtGdiHfontCreate L1277-1313 字体对象
RealizeFontInit L431-450 TEXTOBJ 懒实现
NtGdiGetGlyphOutline L805-886 字形轮廓 API
NtGdiGetCharWidthW L1318-... 字符宽度 API
win32ss/gdi/ntgdi/gdibatch.c GdiFlushUserBatch L67-...、NtGdiFlushUserBatch L485-533、NtGdiFlush L468-476 批处理执行
win32ss/gdi/ntgdi/dclife.c DC_vPrepareDCsForBlit L505-609、DC_vFinishBlit L612-... 绘制准备/收尾
win32ss/gdi/ntgdi/dcobjs.c DC_vUpdateTextBrush L106-122、DC_vUpdateBackgroundBrush L124-... 文本/背景画刷
win32ss/gdi/ntgdi/dc.h DC_LockDc L218-234、DC 结构 L95-137 DC 对象
win32ss/include/ntgdihdl.h DC_ATTR L291-351 DC 属性(文本属性)
win32ss/gdi/ntgdi/path.c PATH_ExtTextOut L2416-... 路径分支文本
win32ss/gdi/ntgdi/text.c GreTextOutW L59-68 内核 USER 文本入口
win32ss/gdi/ntgdi/font.h FONT_CACHE_HASHED L40-51、FONT_CACHE_ENTRY L55-61、FONTSUBST_ENTRY L76-81 缓存/替换结构
win32ss/gdi/ntgdi/utils.h surrogate 宏 L38-50 UTF-16 代理对

13.3 引擎与 FreeType

文件 函数/位置 说明
win32ss/gdi/eng/bitblt.c IntEngMaskBlt L1104-1240、EngMaskBitBlt L894-...、AlphaBltMask L823-890、BltMask L47-... 掩码合成
win32ss/gdi/eng/surface.c EngCreateBitmap L306-344、EngDeleteSurface L567、EngLockSurface L607 表面管理
win32ss/gdi/eng/xlateobj.c EXLATEOBJ_vInitialize L491-...、EXLATEOBJ_vCleanup L894、XLATEOBJ_iXlate L909 颜色翻译
win32ss/gdi/eng/engobjects.h FONTGDI L139-168、SHARED_FACE L131-137、SHARED_MEM L118-123 内核字体对象结构
sdk/lib/3rdparty/freetype/include/freetype/freetype.h FT_Request_Size L2661、FT_Load_Glyph L2793、FT_Set_Transform L3193、FT_Get_Kerning L3462、FT_Set_Charmap L3657、FT_Get_Char_Index L3714 FreeType 公共接口
sdk/lib/3rdparty/freetype/include/freetype/ftglyph.h FT_Get_Glyph L283、FT_Glyph_To_Bitmap(示例 L505-533)、FT_BitmapGlyphRec L167-174 字形位图接口
win32ss/win32u/win32u.spec NtGdiExtTextOutW L154 系统调用声明

14. 总结

本册沿着 TextOutW 的完整调用路径,把 ReactOS 的文本渲染实现从"黑盒"拆成了"透明管线"。全篇的核心结论可以浓缩为五句话:

  1. 一条链、两个入口 :gdi32 的 ExtTextOutW 要么把调用打包进 TEB 批缓冲(≤580 字符、快照 8 个 DC 属性、延迟执行),要么直连 NtGdiExtTextOutW------两条路在内核的 IntExtTextOutW 汇合,共用同一渲染主流程(text.c L489-654 ↔ freetype.c L6727-7295)。

  2. 一次 SEH、全程安心NtGdiExtTextOutW(L7340-7459)在系统调用边界用 ProbeForRead + RtlCopyMemory + _SEH2_TRY 把用户态 String/Dx/Rect 完整拷进内核缓冲(Dx 在前、String 后移保证 4 对齐),此后逐字符循环零探测成本。

  3. 两级映射、两级回退 :字符→字形靠 FT_Get_Char_Index 查 cmap(get_glyph_index,L4372-4385),字形→位图靠 FT_Load_Glyph + FT_Glyph_To_BitmapIntGetRealGlyph,L4987-5024)+ 256 项 LRU 缓存;字体找不到走 FontSubstitutes 替换 ,字形找不到走 FontLink 链接回退FontLink_Chain_FindGlyph,L4438-4493),最终兜底 U+30FB 占位符。

  4. 文本 = 形状掩码 × 颜色画刷 :字形位图被 EngCreateBitmap 零拷贝包装成 BMF_8BPP 掩码表面,IntEngMaskBlt → EngMaskBitBlt → AlphaBltMask/BltMask(eng/bitblt.c)用 dc->eboText 画刷逐像素着色/混合写入目标表面------这就是"文本怎么变成图形显示输出"的最终答案,也解释了为什么文本绘制不需要驱动实现 DrvTextOut

  5. 全链路可追溯 :从 TextOutW(gdi32/text.c L64)到 DIB_PutPixel(eng/bitblt.c),每个环节都有真实源码与精确行号对应,本册所有行号均可直接在 d:\reactos\ 源码树中复核。

作为"窗口系统三大主线"的 GDI 文本渲染深化篇,本册与 _30(USER/GDI 分工)、以及后续可能的 GDI 引擎(位块传输/表面管理)分册共同构成完整的 GDI 绘制知识链:上层看语义分工(USER/GDI),中层看文本管线(本册),下层看像素搬运(eng/)


本文基于 ReactOS master 源码撰写,所有函数行号以 d:\reactos\ 实际源码为准。核心文件:win32ss/gdi/gdi32/objects/text.cwin32ss/gdi/ntgdi/freetype.cwin32ss/gdi/ntgdi/font.cwin32ss/gdi/ntgdi/gdibatch.cwin32ss/gdi/eng/bitblt.cwin32ss/gdi/eng/surface.cwin32ss/gdi/eng/xlateobj.cwin32ss/gdi/ntgdi/font.hwin32ss/gdi/eng/engobjects.hsdk/lib/3rdparty/freetype/include/freetype/freetype.h

相关推荐
markvivv1 小时前
【译】适合在RTX 5090、DGX Spark或类似机器上可运行的最佳新模型是什么?
大数据·人工智能·spark
sunneo1 小时前
每周GitCode开源项目精选
人工智能
zhangfeng11331 小时前
HiDevLab vCANNLab(昇腾两个云端WebIDE)安装codebuddy
人工智能·ai编程·算子开发
leeyi1 小时前
Agent 间 Transfer 交接:用户在不同 Agent 间无缝切换(第93篇-E79)
人工智能·aigc·agent
很楠爱上1 小时前
从“AI 看合同”到可举证的合同决策链:CounterClause(对薄) 的架构设计与工程实践
人工智能·经验分享·python·学习·agent
Java后端的Ai之路1 小时前
02、Python普通工厂模式
开发语言·人工智能·python·设计模式·普通工厂模式
爱奥尼欧1 小时前
14.输出解析器-Pydantic与JSON
人工智能·学习·langchain·json
无忧.芙桃1 小时前
Linux系统之网络基础概念
网络
ReleaseU1 小时前
Claude Code 两天三版本、Cursor 把仓库搬进编辑器:AI 编程工具在卷什么?
人工智能·大模型