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。本册则把镜头对准这条链的内部,回答三个"怎么":
- 文本怎么变成字形 :
LPWSTR的每个WCHAR(含 UTF-16 surrogate 对)如何通过FT_Get_Char_Index查 cmap 表得到字形索引(glyph index); - 字形怎么变成位图 :字形索引如何经
FT_Load_Glyph加载轮廓、FT_Glyph_To_Bitmap渲染成 8BPP 覆盖度掩码,并被FONT_CACHE_ENTRY全局缓存; - 位图怎么变成屏幕像素 :字形位图如何被
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) 。像 TextOutW、PatBlt、SelectObject 这类高频小操作,用户态 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.sys (sdk/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_UPDATECP 取 ptlCurrent;IntLPtoDP 逻辑→设备;GM_ADVANCED 取世界矩阵 |
| 3 | 背景 | L6827-6853 | ETO_OPAQUE / jBkMode==OPAQUE → IntEngFillBox(eboBackground) |
| 4 | 字体 | L6855-6887 | RealizeFontInit、FontGDI->SharedFace->Face、TextIntUpdateSize、FontLink_Chain_Init |
| 5 | 变换 | L6889-6928 | lfEscapement 矩阵、世界变换 FT_Set_Transform、纵对齐 TA_BASELINE/TOP/BOTTOM |
| 6 | 测量 | L6930-6981 | use_kerning=FT_HAS_KERNING;IntGetTextDisposition;TA_CENTER/TA_RIGHT 平移起点;背景填充 |
| 7 | 主循环 | L6992-7160 | 逐字符:surrogate→字形→位图→字距→EngCreateBitmap→IntEngMaskBlt→advance |
| 8 | 下划线/删除线 | L7165-7256 | lfUnderline/lfStrikeOut → IntEngFillBox/IntEngFillPolygon(eboText 画刷) |
| 9 | 收尾 | L7258-7295 | LockWindowUpdate 边界累积、解锁、DC_vFinishBlit、TEXTOBJ_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=0、lprc=NULL、lpDx=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 位范围(见 NtGdiExtTextOutW 的 Count > 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,快照ulBackgroundClr与ptlViewportOrg,返回 TRUE。内核侧执行时(gdibatch.cL357-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;
}
...
}
}
}
GdiFlushUserBatch 的 case 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_LockDc(dc.h L218-234)本质是 GDIOBJ_LockObject(hdc, GDIObjType_DC_TYPE) 加断言------DC 是 GDI 句柄表对象,锁保证并发安全;GreTextOutW(ntgdi/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_ExtTextOut(path.c L2416-...)是路径分支:逐字符调 ftGdiGetGlyphOutline(dc, str[idx], GGO_NATIVE|GGO_GLYPH_INDEX, ...) 把字形轮廓(TTPOLYGONHEADER/TTPOLYCURVE 多边形序列)追加进路径,供 EndPath+StrokePath/FillPath 使用------文本在这里退化为矢量路径。
DC_vPrepareDCsForBlit(dclife.c L505-609)按 DIRTY_FILL|DC_BRUSH_DIRTY、DIRTY_LINE|DC_PEN_DIRTY、DIRTY_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); // 字体链接链初始化
RealizeFontInit(font.c L431-450)= TEXTOBJ_LockText(hFont) + 若未初始化则 TextIntRealizeFont(懒初始化:找字体、做替换、填 FONTGDI 请求属性)。
FONT_CACHE_HASHED(font.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)把 MATRIX(efM11 等 FLOATOBJ)乘 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)
EXLATEOBJ(eng/xlateobj.c L491+)在 ppalDst==ppalSrc 时置 XO_TRIVIAL 用恒等翻译(L517-521);否则按位掩码/移位或查表翻译。文本绘制需要双向翻译:exloRGB2Dst 把画刷 RGB 颜色翻译成目标表面颜色空间(比如 8bpp 调色板设备的索引),exloDst2RGB 供 AlphaBltMask 读回背景做混合。
主渲染循环(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 为什么
>> 10:FT_BitmapGlyph->root.advance是 26.6 定点(1/64 像素),但 FreeType 的 glyph advance 字段是 16.16 语义的advance(root.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_8BPP;IntEngMaskBlt据此走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。
5.7 FontLink_Chain_FindGlyph:字体链接回退
文件: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):解析
"<文件名>,<字体名>",把名字拷进lfFaceName后SubstituteFontRecurse做一次替换,去重后ExAllocatePoolZero建FONTLINK。 - FontLink_PrepareFontInfo (L349-397):惰性解析链接字体的
SharedFace------先在私有字体表Win32Process->PrivateFontListHead找,再在系统字体表g_FontListHead找(FindBestFontFromList),找不到置bIgnore=TRUE;还要用MatchFontName校验家族名。 - FontLink_Chain_FindEntry :按
lfFaceName从g_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)拿到字形索引后:
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)得到位图。- 需要时
FT_GlyphSlot_Embolden(L5013)/FT_GlyphSlot_Oblique(L5016)仿真粗体/斜体------当 LOGFONT 请求的lfWeight > 字体原始字重或lfItalic=1 而字体本身非斜体时(Cache.Hashed.Aspect.Emu.Bold/Italic由 L6873-6874 计算)。 IntGetBitmapGlyphWithCache内FT_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。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、点尺寸)计算匹配代价 ,选代价最小的 FONTOBJ。FontGdi->SharedFace 是 PSHARED_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 内嵌)、dhpdev、SharedFace、请求属性(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):
FT_ENCODING_UNICODE(Unicode cmap);- 没有则
platform_id == TT_PLATFORM_APPLE_UNICODE(Apple 旧式 Unicode); - 再没有则
FT_ENCODING_MS_SYMBOL(Symbol 字体); - 兜底
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_Size(freetype.h L2661)让 FreeType 按 26.6 定点尺寸计算 face->size->metrics(x_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,按 RequestedCharSet 与 FONTSUBST_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 一带定义)关键字段:iType(STYPE_BITMAP=内存 DIB / STYPE_DEVICE=设备表面)、iBitmapFormat(BMF_1BPP/4BPP/8BPP/16BPP/24BPP/32BPP)、pvScan0(像素基址)、lDelta(行距)、sizlBitmap(宽高)、hdev/dhsurf(所属设备)。显示驱动通过 DrvEnablePDEV/DrvEnableSurface 创建设备表面,dhsurf 指向驱动私有句柄。
IntEngMaskBlt 在 L1090 判断 psoDest->iType != STYPE_BITMAP------设备表面(如直接写显存)不能由引擎直接逐像素操作 ,于是建临时 DIB 表面:EngCopyBits 把目标矩形拷进临时表面 → 在临时表面上做 EngMaskBitBlt → EngCopyBits 拷回(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 位图
EngCreateBitmap(eng/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->eboText 是 EBRUSHOBJ(扩展画刷对象),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 + 拷贝进内核缓冲(栈优先、池兜底)。好处:
- 安全 :坏指针被
_SEH2_EXCEPT捕获,不会蓝屏; - 性能:主循环里逐字符访问的都是内核地址,免去每字符探测;
- 一致性:拷贝后应用并发修改原字符串不影响本次输出。
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/EngDeleteSurface;iType/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_METADC、LoadLPK(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 的 GreTextOutW(ntgdi/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 前驱动可收到同步通知(NtGdiFlush→SynchronizeDriver(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 的文本渲染实现从"黑盒"拆成了"透明管线"。全篇的核心结论可以浓缩为五句话:
-
一条链、两个入口 :gdi32 的
ExtTextOutW要么把调用打包进 TEB 批缓冲(≤580 字符、快照 8 个 DC 属性、延迟执行),要么直连NtGdiExtTextOutW------两条路在内核的IntExtTextOutW汇合,共用同一渲染主流程(text.cL489-654 ↔freetype.cL6727-7295)。 -
一次 SEH、全程安心 :
NtGdiExtTextOutW(L7340-7459)在系统调用边界用ProbeForRead + RtlCopyMemory + _SEH2_TRY把用户态 String/Dx/Rect 完整拷进内核缓冲(Dx 在前、String 后移保证 4 对齐),此后逐字符循环零探测成本。 -
两级映射、两级回退 :字符→字形靠
FT_Get_Char_Index查 cmap(get_glyph_index,L4372-4385),字形→位图靠FT_Load_Glyph + FT_Glyph_To_Bitmap(IntGetRealGlyph,L4987-5024)+ 256 项 LRU 缓存;字体找不到走 FontSubstitutes 替换 ,字形找不到走 FontLink 链接回退 (FontLink_Chain_FindGlyph,L4438-4493),最终兜底U+30FB占位符。 -
文本 = 形状掩码 × 颜色画刷 :字形位图被
EngCreateBitmap零拷贝包装成BMF_8BPP掩码表面,IntEngMaskBlt → EngMaskBitBlt → AlphaBltMask/BltMask(eng/bitblt.c)用dc->eboText画刷逐像素着色/混合写入目标表面------这就是"文本怎么变成图形显示输出"的最终答案,也解释了为什么文本绘制不需要驱动实现DrvTextOut。 -
全链路可追溯 :从
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.c、win32ss/gdi/ntgdi/freetype.c、win32ss/gdi/ntgdi/font.c、win32ss/gdi/ntgdi/gdibatch.c、win32ss/gdi/eng/bitblt.c、win32ss/gdi/eng/surface.c、win32ss/gdi/eng/xlateobj.c、win32ss/gdi/ntgdi/font.h、win32ss/gdi/eng/engobjects.h、sdk/lib/3rdparty/freetype/include/freetype/freetype.h。