ReactOS 图形系统分析(47):GDI 批处理 --- gdibatch.c
1. 概述
本文分析 ReactOS 内核态 GDI(win32k.sys / ntgdi)中的 GDI 批处理(GDI Batch)刷出机制 ,核心源码为
gdibatch.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdibatch.c)(全文 533 行)。该文件实现了把"攒在用户态 TEB 中的一批小 GDI 命令"一次性解析并执行的内核逻辑,同时承担设备驱动同步(DrvSynchronize)的职责。
GDI 批处理是 Windows GDI 中一个经典且重要的性能优化:gdi32.dll 把多个小 GDI 调用批量缓存 (缓存在线程 TEB 的 GdiTebBatch 环形缓冲中),当缓冲满、达到批上限 GDI_BATCH_LIMIT、或显式调用 GdiFlush 时,一次性刷入内核执行------从而把 N 次系统调用合并为 1 次,显著减少用户态/内核态切换的开销。典型受益操作包括 TextOut、PatBlt、SetBrushOrg、ExtSelectClipRgn、SelectObject、DeleteObject 等高频小操作。
整体架构如下(ASCII 图):
用户态 (gdi32.dll) 内核态 (win32k.sys / ntgdi)
┌─────────────────────────────────────┐ ┌──────────────────────────────────────────┐
│ 应用程序 │ │ KiSystemService (ntoskrnl) │
│ PatBlt / TextOut / SetBrushOrg / │ │ │ 进入任何 GUI 系统调用时: │
│ DeleteObject ... │ │ │ if (Teb->GdiBatchCount != 0) │
│ │ │ │ │ KeGdiFlushUserBatch(); (1) │
│ ▼ │ │ ▼ │
│ 对应 API 入口 │ │ NtGdiFlushUserBatch (gdibatch.c) (2) │
│ │ GdiGetDcAttr + 条件检查 │ │ │ 逐条解析 TEB 批缓冲 │
│ ▼ │ │ ▼ │
│ GdiAllocBatchCommand(hdc, Cmd) │ │ GdiFlushUserBatch(dc, pHdr) (3) │
│ │ 把命令记录写入 │ │ │ switch(Cmd) 分发 │
│ │ Teb->GdiTebBatch.Buffer │ │ ├─► IntPatBlt (GdiBCPatBlt) │
│ │ Teb->GdiBatchCount++ │ │ ├─► IntPatBlt×N (GdiBCPolyPatBlt) │
│ ▼ │ │ ├─► IntExtTextOutW(GdiBCTextOut) │
│ 缓冲满 / GdiBatchCount>=GDI_BatchLimit│ │ ├─► IntExtTextOutW(GdiBCExtTextOut) │
│ ▼ │ │ ├─► DC_vSetBrushOrigin (SetBrushOrg) │
│ NtGdiFlush() 系统调用 ────────────────► │ ├─► IntGdiExtSelectClipRect(ExtSel) │
│ (刷出本身由 (1) 完成) │ │ ├─► DC_hSelectFont (SelObj) │
│ │ │ │ └─► GreDeleteObject (DelObj/DelRgn) │
└─────────────────────────────────────┘ │ NtGdiFlush (4) │
│ └─► SynchronizeDriver(GCAPS2_SYNCFLUSH)│
│ └─► DoDeviceSync → DrvSynchronize │
└──────────────────────────────────────────┘
图中四个关键执行点:
- 刷出触发点(内核调度层) :
ntoskrnl\ke\i386\traphdlr.c的KiSystemService在进入任何 win32k(GUI)系统调用前,若NtCurrentTeb()->GdiBatchCount非零,就调用KeGdiFlushUserBatch()。 - 刷出入口(本文件) :
NtGdiFlushUserBatch------ 注册到WIN32_CALLOUTS的BatchFlushRoutine(见ntuser\main.c的PsEstablishWin32Callouts),内核在任何需要同步批处理的地方调用它。 - 批命令解析器(本文件) :
GdiFlushUserBatch------ 逐条读取GDIBATCHHDR,按命令号分发到对应的Int*/DC_*/Gre*内部函数。 - 显式刷出 + 设备同步(本文件) :
NtGdiFlush------ 用户态GdiFlush/GdiAllocBatchCommand满批时调用;它本身只负责驱动同步(SynchronizeDriver(GCAPS2_SYNCFLUSH)),真正的用户批缓冲清理由第 1 点的系统调用入口路径顺带完成。
设计要点:用户批的解析执行发生在"下一次任意 win32k 系统调用"时 (而不是由
NtGdiFlush显式执行)。这保证无论应用程序通过哪条路径进入内核(画线、取消息、查询状态......),之前攒下的所有绘制命令都已先行落地,语义与 Windows 一致。
2. 设计动机:减少系统调用
在图形密集型应用中,TextOut、PatBlt、SetBrushOrg 这类操作往往以极高频次出现,且每次调用都只携带几十字节的参数。如果每次都走系统调用:
- 每次调用都要经历用户态→内核态切换(保存/恢复寄存器、栈切换、
KiSystemService分发),数百个周期的固定开销; - 每个调用都需要在内核中完成句柄校验、DC 加锁/解锁、对象引用计数等固定步骤;
- 高频调用会使 CPU 在两条特权级路径上来回奔波,破坏指令缓存与 TLB 局部性。
批处理把"一次一个系统调用"变成"一批一个系统调用":
- 用户态把命令记录(含参数快照)顺序写入线程 TEB 的
GdiTebBatch.Buffer; - 只有当缓冲满(
GdiBatchCount >= GDI_BatchLimit或字节数超出GDIBATCHBUFSIZE)或应用显式GdiFlush()时,才发起一次系统调用; - 内核端一次进入、循环解析整批命令,逐一执行。
由于批记录中的 DC 状态(前景色、背景色、画刷、字体、文本对齐等)是在攒批时刻做快照保存的,因此即使后续调用修改了 DC 属性,批命令执行时仍能还原出正确的绘制语义------这正是内核解析时"保存属性→设置快照→执行→恢复属性"模式的由来(详见第 7 章)。
Windows 本身使用同样的机制(用户批缓冲在 TEB 中的布局与 Windows 完全一致),ReactOS 的 gdibatch.c 是其重新实现。文件头注释指出 DoDeviceSync "based on IntEngEnter from eng/engmisc.c"(基于引擎的进入/同步例程),说明驱动同步部分参照了 GDI 引擎的设备表面兼容逻辑(详见第 10 章)。
3. 核心数据结构
3.1 批缓冲尺寸:GDI_BATCH_LIMIT / GDIBATCHBUFSIZE / GDI_BATCH_BUFFER_SIZE
三个关键宏分布在两处:
内核侧定义(ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h)):
c
#define GDIBATCHBUFSIZE 0x136*4 /* = 1240 字节:批缓冲总字节容量 */
#define GDI_BATCH_LIMIT 20 /* 单批最多容纳的命令条数(默认) */
用户侧定义(peb_teb.h(file:///d:/reactos/sdk/include/ndk/peb_teb.h)):
c
#define GDI_BATCH_BUFFER_SIZE 0x136 /* = 310:批缓冲按 ULONG 计数的容量 */
两者一致:缓冲字节容量 = 0x136 * 4 = 1240 字节 = 310 个 DWORD。GDI_BATCH_LIMIT = 20 是默认的命令条数上限 (而非字节上限),二者取先到者生效(见 GdiAllocBatchCommand 第 4.1 节)。
PEB 字段:GdiDCAttributeList
-
内核侧
win32ss\gdi\ntgdi\init.c的GdiProcessCreate()在每个进程创建时写入:cProcess->Peb->GdiSharedHandleTable = GDI_MapHandleTable(Process); Process->Peb->GdiDCAttributeList = GDI_BATCH_LIMIT; /* 20 */注意:
Peb->GdiDCAttributeList虽然名字带有 "DC Attribute List",在 ReactOS 中实际承载的是批限制(batch limit) ,即默认GDI_BATCH_LIMIT。这正是历史命名与现用途不一致的地方。 -
用户侧
gdi32\main\dllmain.c的GdiProcessSetup()在 gdi32 初始化时读取:cGDI_BatchLimit = (DWORD) NtCurrentTeb()->ProcessEnvironmentBlock->GdiDCAttributeList;于是每个进程的批上限默认 20;该全局变量
GDI_BatchLimit(初值 1,见gdi32\misc\misc.c)在进程初始化后被覆盖为 PEB 中的值。
3.2 线程批缓冲:TEB 中的 GDI_TEB_BATCH 与 GdiBatchCount
批缓冲不是 gdi32 的全局量,而是每线程 的,存放在 TEB 中(x86 偏移 0x1D4,x64 偏移 0x2F0,见 peb_teb.h 中的 C_ASSERT 校验)。这样多线程应用各自攒批、各自刷出,互不干扰,也无需加锁。
c
typedef struct STRUCT(_GDI_TEB_BATCH)
{
ULONG Offset; /* 当前写指针(相对 Buffer 的字节偏移) */
PTR(HANDLE) HDC; /* 本批关联的 DC(0 表示纯对象删除批) */
ULONG Buffer[GDI_BATCH_BUFFER_SIZE]; /* 310 个 DWORD = 1240 字节命令区 */
} STRUCT(GDI_TEB_BATCH), *STRUCT(PGDI_TEB_BATCH);
配合 TEB 中的计数器字段:
Teb->GdiTebBatch.Offset:下一条命令记录应写入的字节偏移(从 0 开始累加);Teb->GdiBatchCount:当前批中已排队的命令条数(与Offset并行维护;命令记录定长部分 + 变长部分合计为字节数,而GdiBatchCount只数条数);Teb->GdiTebBatch.HDC:本批所属的 DC 句柄。一批内所有命令必须属于同一个 DC (GdiAllocBatchCommand强制检查),否则拒绝入批直接走系统调用;唯一例外是"纯对象删除"类命令(GdiBCDelObj/GdiBCDelRgn),它们以hdc = NULL入批,与任何 DC 命令混批后仍可执行。
每条命令记录的布局:
+----------------+----------------+
| SHORT Size | SHORT Cmd | ← GDIBATCHHDR 头(4 字节,每条命令必有)
+----------------+----------------+
| ... 命令特有参数(含属性快照)... | ← 紧随其后的变长数据
+--------------------------------+
Size 为整条记录(含头部)的字节数,供内核顺序遍历;Cmd 为命令号。
3.3 批命令枚举与记录结构
命令号定义(ntgdityp.h):
c
typedef enum _GDIBATCHCMD
{
GdiBCPatBlt, /* 0:图案填充(PatBlt) */
GdiBCPolyPatBlt, /* 1:多矩形图案填充(PolyPatBlt,ROS 扩展) */
GdiBCTextOut, /* 2:文本输出(TextOut/ExtTextOutW 带串) */
GdiBCExtTextOut, /* 3:仅背景填充式文本输出(空串 + ETO_OPAQUE)*/
GdiBCSetBrushOrg, /* 4:设置画刷原点(SetBrushOrgEx) */
GdiBCExtSelClipRgn, /* 5:扩展选择裁剪矩形(ExtSelectClipRgn) */
GdiBCSelObj, /* 6:选择对象(SelectObject→字体) */
GdiBCDelObj, /* 7:删除对象(DeleteObject→画刷/画笔) */
GdiBCDelRgn, /* 8:删除区域(DeleteRegion) */
} GDIBATCHCMD, *PGDIBATCHCMD;
通用头:
c
typedef struct _GDIBATCHHDR
{
SHORT Size; /* 整条记录字节数(含头),内核按此推进指针 */
SHORT Cmd; /* GDIBATCHCMD 命令号 */
} GDIBATCHHDR, *PGDIBATCHHDR;
GdiBCPatBlt 记录 (GDIBSPATBLT)------每个字段都是用户在 PatBlt 攒批时刻的"参数 + DC 属性快照":
c
typedef struct _GDIBSPATBLT
{
GDIBATCHHDR gbHdr; /* 头 */
int nXLeft, nYLeft; /* 目标矩形左上角 */
int nWidth, nHeight; /* 宽、高 */
HANDLE hbrush; /* 当前 DC 画刷句柄快照 */
DWORD dwRop; /* ROP3 */
COLORREF crForegroundClr; /* 前景色快照(COLORREF 形式) */
COLORREF crBackgroundClr; /* 背景色快照 */
COLORREF crBrushClr; /* DC 画刷色快照 */
INT IcmBrushClr; /* ICM 画刷色索引(保留) */
POINTL ptlViewportOrg; /* 视口原点快照 */
ULONG ulForegroundClr; /* 前景色(内部 ULONG 形式) */
ULONG ulBackgroundClr; /* 背景色(内部 ULONG 形式) */
ULONG ulBrushClr; /* 画刷色(内部 ULONG 形式) */
} GDIBSPATBLT, *PGDIBSPATBLT;
GdiBCPolyPatBlt 记录 (GDIBSPPATBLT)------多矩形版本,矩形数组紧随其后(变长):
c
/* FIXME: this should go to some "public" GDI32 header */
typedef struct _PATRECT
{
RECT r; /* 矩形(注意:批路径中实为 left/top/width/height 的别名,见 7.2 节) */
HBRUSH hBrush; /* 该矩形使用的画刷句柄 */
} PATRECT, * PPATRECT;
typedef struct _GDIBSPPATBLT
{
GDIBATCHHDR gbHdr;
DWORD rop4; /* ROP(用户态直接给 ROP4 语义) */
DWORD Mode; /* 模式(保留) */
DWORD Count; /* 矩形个数 */
COLORREF crForegroundClr;/* 属性快照(同 GDIBSPATBLT) */
COLORREF crBackgroundClr;
COLORREF crBrushClr;
ULONG ulForegroundClr;
ULONG ulBackgroundClr;
ULONG ulBrushClr;
POINTL ptlViewportOrg;
PATRECT pRect[1]; /* 变长数组:Count 个 PATRECT */
} GDIBSPPATBLT, *PGDIBSPPATBLT;
GdiBCTextOut 记录 (GDIBSTEXTOUT)------文本批命令,字符串与 Dx 间距数组紧跟在结构体之后(变长):
c
//
// Both ExtSelectClipRgn and TextOut pass a nill RECT.
//
#define GDIBS_NORECT 0x80000000 /* Options 中的私有标志:Rect 无效 */
typedef struct _GDIBSTEXTOUT
{
GDIBATCHHDR gbHdr;
COLORREF crForegroundClr; /* 前景色快照 */
COLORREF crBackgroundClr; /* 背景色快照 */
LONG lBkMode; /* 背景模式(OPAQUE/TRANSPARENT) */
ULONG ulForegroundClr; /* 内部前景色 */
ULONG ulBackgroundClr; /* 内部背景色 */
int x, y; /* 起点 */
UINT Options; /* ETO_* 标志(含 GDIBS_NORECT 私有位) */
RECT Rect; /* 裁剪/不透明矩形 */
DWORD iCS_CP; /* 代码页(当前恒为 0) */
UINT cbCount; /* 字符数 */
UINT Size; /* Dx 数据的字节数(用于定位字符串起点) */
HANDLE hlfntNew; /* 字体句柄快照 */
FLONG flTextAlign; /* 文本对齐快照 */
POINTL ptlViewportOrg; /* 视口原点快照 */
union {
WCHAR String[2]; /* 字符串起点(实际上字符串在 Size 字节之后) */
ULONG Buffer[1]; /* Dx 数据起点(紧随结构体之后) */
};
} GDIBSTEXTOUT, *PGDIBSTEXTOUT;
内存布局技巧:Buffer[1] 与 String[2] 共用同一块 4 字节空间。攒批时若带 lpDx,先拷贝 DxSize 字节的间距数据到 Buffer 处,再把字符串拷贝到 &String[DxSize/sizeof(WCHAR)],并用 Size = DxSize 记录间距字节数。内核执行时:
- Dx 指针:
pgO->Size ? (const INT*)&pgO->Buffer : NULL; - 字符串指针:
(LPCWSTR)&pgO->String[pgO->Size / sizeof(WCHAR)]。
这样既保证 Dx(INT,4 字节对齐)对齐,又避免两块独立分配,攒批时只需一次 RtlCopyMemory 即可连续拷贝(见 text.c 第 5.3 节)。
GdiBCExtTextOut 记录 (GDIBSEXTTEXTOUT)------专用于"空字符串 + ETO_OPAQUE 背景矩形"的快速路径:
c
typedef struct _GDIBSEXTTEXTOUT
{
GDIBATCHHDR gbHdr;
UINT Count; /* 字符数(恒 0) */
UINT Options; /* ETO_* 标志 */
RECT Rect; /* 不透明背景矩形 */
POINTL ptlViewportOrg; /* 视口原点快照 */
ULONG ulBackgroundClr; /* 背景色快照(内部形式) */
} GDIBSEXTTEXTOUT, *PGDIBSEXTTEXTOUT;
GdiBCSetBrushOrg 记录 (GDIBSSETBRHORG):
c
typedef struct _GDIBSSETBRHORG
{
GDIBATCHHDR gbHdr;
POINTL ptlBrushOrigin; /* 新画刷原点 */
} GDIBSSETBRHORG, *PGDIBSSETBRHORG;
GdiBCExtSelClipRgn 记录 (GDIBSEXTSELCLPRGN):
c
typedef struct _GDIBSEXTSELCLPRGN
{
GDIBATCHHDR gbHdr;
int fnMode; /* RGN_AND/RGN_OR/... 合并模式 */
RECTL rcl; /* 裁剪矩形 */
} GDIBSEXTSELCLPRGN, *PGDIBSEXTSELCLPRGN;
GdiBCSelObj / GdiBCDelObj / GdiBCDelRgn 共用记录 (GDIBSOBJECT):
c
/* Use with GdiBCSelObj, GdiBCDelObj and GdiBCDelRgn. */
typedef struct _GDIBSOBJECT
{
GDIBATCHHDR gbHdr;
HGDIOBJ hgdiobj; /* 要选择/删除的 GDI 对象句柄 */
} GDIBSOBJECT, *PGDIBSOBJECT;
3.4 用户态批上限:GDI_BatchLimit 与 GdiSetBatchLimit / GdiGetBatchLimit
gdi32\misc\misc.c:
c
DWORD GDI_BatchLimit = 1; /* 模块级默认;GdiProcessSetup 会覆盖为 PEB 值(20) */
DWORD WINAPI GdiSetBatchLimit(DWORD Limit)
{
DWORD OldLimit = GDI_BatchLimit;
if ( (!Limit) || (Limit >= GDI_BATCH_LIMIT) ) /* 0 或 ≥20 被拒绝 */
return Limit; /* 返回请求值表示失败 */
GdiFlush(); /* 先刷出旧批 */
GDI_BatchLimit = Limit; /* 再收紧上限 */
return OldLimit; /* 返回旧值表示成功 */
}
DWORD WINAPI GdiGetBatchLimit(VOID)
{
return GDI_BatchLimit;
}
应用可以通过 GdiSetBatchLimit 把批上限从默认 20 收紧(下限 1,即退化为每条命令都刷出)。GdiFlush() 用户态实现极简:
c
BOOL WINAPI GdiFlush(VOID)
{
NtGdiFlush();
return TRUE;
}
它把工作全部交给内核 NtGdiFlush(第 8 章),并通过系统调用入口路径触发用户批的清理。
4. 用户态攒批核心:GdiAllocBatchCommand
所有批命令的统一入口是 gdi32p.h 中的内联函数 GdiAllocBatchCommand(HDC hdc, USHORT Cmd)(FORCEINLINE)。它完成:校验环境 → 计算记录尺寸 → 绑定批 DC → 检查容量(满则刷出)→ 分配槽位并写头。
c
FORCEINLINE
PVOID
GdiAllocBatchCommand(HDC hdc, USHORT Cmd)
{
PTEB pTeb;
USHORT cjSize;
PGDIBATCHHDR pHdr;
pTeb = NtCurrentTeb();
/* 1. 必须有 TEB 且属于 GUI 线程(Win32ThreadInfo 非空) */
if (!pTeb || !pTeb->Win32ThreadInfo) return NULL;
/* 2. 由命令号查表得记录固定尺寸 */
if (Cmd == GdiBCPatBlt) cjSize = sizeof(GDIBSPATBLT);
else if (Cmd == GdiBCPolyPatBlt) cjSize = sizeof(GDIBSPPATBLT);
else if (Cmd == GdiBCTextOut) cjSize = sizeof(GDIBSTEXTOUT);
else if (Cmd == GdiBCExtTextOut) cjSize = sizeof(GDIBSEXTTEXTOUT);
else if (Cmd == GdiBCSetBrushOrg) cjSize = sizeof(GDIBSSETBRHORG);
else if (Cmd == GdiBCExtSelClipRgn) cjSize = sizeof(GDIBSEXTSELCLPRGN);
else if (Cmd == GdiBCSelObj) cjSize = sizeof(GDIBSOBJECT);
else if (Cmd == GdiBCDelRgn) cjSize = sizeof(GDIBSOBJECT);
else if (Cmd == GdiBCDelObj) cjSize = sizeof(GDIBSOBJECT);
else cjSize = 0;
/* 3. 不支持的命令 */
if (cjSize == 0) return NULL;
/* 4. 批与 DC 的绑定规则:
- 本批尚无 DC → 记录 hdc;
- 本批已有 DC 且与 hdc 相同 → 允许;
- 本批已有 DC 且不同 → 拒绝入批(返回 NULL,走系统调用) */
if (hdc)
{
if (!pTeb->GdiTebBatch.HDC) pTeb->GdiTebBatch.HDC = hdc;
else if (pTeb->GdiTebBatch.HDC != hdc) return NULL;
}
/* 5. 容量检查:条数达到上限 或 字节数将要越界 → 先刷出 */
if ((pTeb->GdiBatchCount >= GDI_BatchLimit) ||
((pTeb->GdiTebBatch.Offset + cjSize) > GDIBATCHBUFSIZE))
{
/* 调 NtGdiFlush:内核系统调用入口会先调用 NtGdiFlushUserBatch 清空本批 */
NtGdiFlush();
// If Flushed, lose the hDC for this batch job! See CORE-15839.
if (hdc)
{
if (!pTeb->GdiTebBatch.HDC) pTeb->GdiTebBatch.HDC = hdc;
}
}
/* 6. 分配槽位:取当前写指针,推进 Offset、增加计数 */
pHdr = (PVOID)((PUCHAR)pTeb->GdiTebBatch.Buffer + pTeb->GdiTebBatch.Offset);
pTeb->GdiTebBatch.Offset += cjSize;
pTeb->GdiBatchCount++;
/* 7. 填写通用头并返回槽位,由调用方继续填具体字段 */
pHdr->Cmd = Cmd;
pHdr->Size = cjSize;
return pHdr;
}
要点解读:
Win32ThreadInfo检查:批缓冲与 win32k 线程结构关联,非 GUI 线程无法攒批,直接回退到系统调用路径。- 批内 DC 一致性 :一批只服务一个 DC。若应用交替在两个 DC 上绘制,第二个 DC 的调用会"踢出"批路径,直接走
NtGdi*系统调用(正确性优先)。 - 满批刷出 :
NtGdiFlush()调用本身不解析批,但进入该系统调用的路径 (KiSystemService发现GdiBatchCount != 0)会先执行NtGdiFlushUserBatch()清空批缓冲------刷出后GdiTebBatch.HDC被复位为 0,因此第 5 步后必须按 CORE-15839 的注释重新设置批 DC,否则后续命令会被误判为"跨 DC 混批"而拒绝。 - 返回的槽位已经写好了
Cmd/Size,调用方只需填充各自的语义字段;对变长命令(TextOut/PolyPatBlt),调用方还要追加变长数据并修正Size与Offset(见第 5 章各节)。
5. 各 API 的攒批路径(用户态)
gdi32 中所有批命令的"入批条件"高度一致:句柄是普通 DC(非元文件 DC/信息 DC)、DC 未绑定 DIB section(ulDirty_ & DC_DIBSECTION 为 0)、能拿到 PDC_ATTR 。一旦不满足或 GdiAllocBatchCommand 返回 NULL,就回退到相应的 NtGdi* 系统调用。下面逐一列出。
5.1 PatBlt(以及 BitBlt 的无源退化)
gdi32\objects\painting.c:
c
BOOL WINAPI PatBlt(HDC hdc, INT nXLeft, INT nYLeft, INT nWidth, INT nHeight, DWORD dwRop)
{
PDC_ATTR pdcattr;
HANDLE_EMETAFDC(BOOL, PatBlt, FALSE, hdc, nXLeft, nYLeft, nWidth, nHeight, dwRop);
if ( GdiConvertAndCheckDC(hdc) == NULL ) return FALSE;
pdcattr = GdiGetDcAttr(hdc);
if (pdcattr && !(pdcattr->ulDirty_ & DC_DIBSECTION))
{
PGDIBSPATBLT pgO;
pgO = GdiAllocBatchCommand(hdc, GdiBCPatBlt);
if (pgO)
{
pdcattr->ulDirty_ |= DC_MODE_DIRTY;
pgO->nXLeft = nXLeft; pgO->nYLeft = nYLeft;
pgO->nWidth = nWidth; pgO->nHeight = nHeight;
pgO->dwRop = dwRop;
/* 快照当前 DC 属性 */
pgO->hbrush = pdcattr->hbrush;
pgO->crForegroundClr = pdcattr->crForegroundClr;
pgO->crBackgroundClr = pdcattr->crBackgroundClr;
pgO->crBrushClr = pdcattr->crBrushClr;
pgO->ulForegroundClr = pdcattr->ulForegroundClr;
pgO->ulBackgroundClr = pdcattr->ulBackgroundClr;
pgO->ulBrushClr = pdcattr->ulBrushClr;
return TRUE;
}
}
return NtGdiPatBlt( hdc, nXLeft, nYLeft, nWidth, nHeight, dwRop);
}
另外 BitBlt 在 ROP 不使用源表面时(!ROP_USES_SOURCE(dwRop))会退化为 PatBlt,从而同样享受批处理:
c
BOOL WINAPI BitBlt(HDC hdcDest, ... , DWORD dwRop)
{
/* Use PatBlt for no source blt, like windows does */
if (!ROP_USES_SOURCE(dwRop))
return PatBlt(hdcDest, xDest, yDest, cx, cy, dwRop);
...
return NtGdiBitBlt(...);
}
5.2 PolyPatBlt(ROS 扩展,多矩形批)
PolyPatBlt 是 ReactOS 扩展 API(ntgdi.h 中 POLYPATBLT = {nXLeft, nYLeft, nWidth, nHeight, hBrush},20 字节,布局与 PATRECT 完全一致)。攒批时把整个矩形数组追加在结构体之后:
c
pgO = GdiAllocBatchCommand(hdc, GdiBCPolyPatBlt);
if (pgO)
{
USHORT cjSize = 0;
if (nCount > 1) cjSize = (nCount-1) * sizeof(PATRECT); /* 第一个矩形已在结构体内 */
if ((pTeb->GdiTebBatch.Offset + cjSize) <= GDIBATCHBUFSIZE)
{
pdcattr->ulDirty_ |= DC_MODE_DIRTY;
pgO->Count = nCount; pgO->Mode = dwMode; pgO->rop4 = dwRop;
/* 快照属性(同 PatBlt) */
...
RtlCopyMemory(pgO->pRect, pPoly, nCount * sizeof(PATRECT));
/* 修正偏移与尺寸:追加的变长字节数 */
pTeb->GdiTebBatch.Offset += cjSize;
((PGDIBATCHHDR)pgO)->Size += cjSize;
return TRUE;
}
/* 空间不足:回滚本次分配,落入系统调用路径 */
pTeb->GdiTebBatch.Offset -= sizeof(GDIBSPPATBLT);
pTeb->GdiBatchCount--;
}
注意这里的回滚 模式:GdiAllocBatchCommand 已经推进了 Offset 与 GdiBatchCount,若变长数据放不下,必须把它们减回去,再走 NtGdiPolyPatBlt。变长命令(TextOut 同理)都遵循这一"失败回滚"约定。
5.3 ExtTextOutW / TextOut
gdi32\objects\text.c 的 ExtTextOutW 是批路径最复杂的一支。前置检查:
c
pdcattr = GdiGetDcAttr(hdc);
if ( pdcattr &&
!(pdcattr->ulDirty_ & DC_DIBSECTION) && /* 无 DIB section */
!(pdcattr->lTextAlign & TA_UPDATECP)) /* 非 TA_UPDATECP(批内无法追踪当前点) */
分两条子路径:
(a)空串 + 不透明背景矩形 → GdiBCExtTextOut:
c
if ( lprc && !cwc )
{
if ( fuOptions & ETO_OPAQUE )
{
PGDIBSEXTTEXTOUT pgO;
pgO = GdiAllocBatchCommand(hdc, GdiBCExtTextOut);
if (pgO)
{
pdcattr->ulDirty_ |= DC_MODE_DIRTY;
pgO->Count = cwc;
pgO->Rect = *lprc;
pgO->Options = fuOptions;
pgO->ulBackgroundClr = pdcattr->ulBackgroundClr; /* 快照 */
pgO->ptlViewportOrg = pdcattr->ptlViewportOrg; /* 快照 */
return TRUE;
}
}
else /* 空串且无 ETO_OPAQUE:无事可做(老 explorer 会弹掉这种调用) */
{
DPRINT("GdiBCExtTextOut nothing\n");
return TRUE;
}
}
(b)普通短串 → GdiBCTextOut(串长上限约 580 字符,注释 "Max 580 wchars, if offset 0"):
c
else if ( cwc <= ((GDIBATCHBUFSIZE - sizeof(GDIBSTEXTOUT)) / sizeof(WCHAR)) )
{
PGDIBSTEXTOUT pgO;
PTEB pTeb = NtCurrentTeb();
pgO = GdiAllocBatchCommand(hdc, GdiBCTextOut);
if (pgO)
{
USHORT cjSize = 0;
ULONG DxSize = 0;
if (cwc > 2) cjSize = (cwc * sizeof(WCHAR)) - sizeof(pgO->String); /* 超出内嵌 2 字符的部分 */
if (lpDx)
{
/* ETO_PDY 时 Dx 是成对的 INT(每个字符两个值) */
DxSize = (cwc * sizeof(INT)) * (fuOptions & ETO_PDY ? 2 : 1);
cjSize += DxSize;
}
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 无效 */
/* 快照属性:颜色、背景模式、字体、对齐、视口原点 */
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; /* 记录 Dx 字节数 */
/* 布局:Dx 在前(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;
}
/* 放不下:回滚并落到系统调用 */
pTeb->GdiTebBatch.Offset -= sizeof(GDIBSTEXTOUT);
pTeb->GdiBatchCount--;
}
}
return NtGdiExtTextOutW(hdc, x, y, fuOptions, lprc, lpString, cwc, lpDx, 0);
超过约 580 字符、带 TA_UPDATECP、绑定 DIB section、或带 ETO_GLYPH_INDEX/ETO_IGNORELANGUAGE 的调用全部走 NtGdiExtTextOutW 系统调用。
5.4 SetBrushOrgEx
gdi32\objects\brush.c:先比较新旧原点,若相同直接返回;否则入批:
c
if (GdiGetHandleUserData((HGDIOBJ) hdc, GDI_OBJECT_TYPE_DC, (PVOID)&Dc_Attr))
{
PGDIBSSETBRHORG pgSBO;
if (lppt) { lppt->x = Dc_Attr->ptlBrushOrigin.x; lppt->y = Dc_Attr->ptlBrushOrigin.y; }
if ((nXOrg == Dc_Attr->ptlBrushOrigin.x) && (nYOrg == Dc_Attr->ptlBrushOrigin.y))
return TRUE;
pgSBO = GdiAllocBatchCommand(hdc, GdiBCSetBrushOrg);
if (pgSBO != NULL)
{
Dc_Attr->ptlBrushOrigin.x = nXOrg; /* 先更新用户态属性 */
Dc_Attr->ptlBrushOrigin.y = nYOrg;
pgSBO->ptlBrushOrigin = Dc_Attr->ptlBrushOrigin; /* 再写入批记录 */
return TRUE;
}
}
return NtGdiSetBrushOrg(hdc, nXOrg, nYOrg, lppt);
注意这里立即更新了用户态 DC_ATTR(保证后续查询语义正确),批记录只是把新原点推迟通知内核。
5.5 ExtSelectClipRgn(矩形特例)
gdi32\objects\region.c 的 ExtSelectClipRgn 在"区域为矩形"时走批命令:
c
PGDIBSEXTSELCLPRGN pgO = GdiAllocBatchCommand(hdc, GdiBCExtSelClipRgn);
if (pgO)
{
pgO->fnMode = fnMode;
pgO->rcl = Rect; /* 矩形区域 */
return hrgn; /* 返回原区域句柄(语义:返回旧区域) */
}
5.6 GdiSelectFont(SelectObject 的字体特例)
gdi32\objects\dc.c 的 GdiSelectFont:先更新用户态 pdcattr->hlfntNew 并设置脏标志,然后尝试入批:
c
hfontOld = pdcattr->hlfntNew;
if (hfontOld == hfont) return hfontOld; /* 相同字体直接返回 */
pdcattr->hlfntNew = hfont;
pdcattr->ulDirty_ &= ~SLOW_WIDTHS;
pdcattr->ulDirty_ |= DIRTY_CHARSET;
if (!(pdcattr->ulDirty_ & DC_DIBSECTION))
{
PGDIBSOBJECT pgO;
pgO = GdiAllocBatchCommand(hdc, GdiBCSelObj);
if (pgO)
{
pgO->hgdiobj = hfont;
return hfontOld;
}
}
return NtGdiSelectFont(hdc, hfont);
SelectObject 的画笔/画刷/位图等分支不批处理(批命令仅覆盖字体选择),这是 Windows 语义限制(批量选择字体是安全的,因为字体选择不会改变表面内容,可以延迟)。
5.7 DeleteRegion 与 GdiDeleteBrushOrPen(无 DC 批命令)
这两个命令以 hdc = NULL 入批,从而可以与"纯删除批"合并,即使当前批没有绑定 DC 也能执行(这正是 NtGdiFlushUserBatch 中 hDC || GdiBatchCount 判断存在的意义------"如果 hDC 为零且缓冲里全是删除对象,也必须运行")。
c
/* region.c */
BOOL FASTCALL DeleteRegion(_In_ HRGN hrgn)
{
PRGN_ATTR Rgn_Attr = GdiGetRgnAttr(hrgn);
if ( Rgn_Attr )
{
PGDIBSOBJECT pgO;
pgO = GdiAllocBatchCommand(NULL, GdiBCDelRgn);
if (pgO) { pgO->hgdiobj = hrgn; return TRUE; }
}
return NtGdiDeleteObjectApp(hrgn);
}
/* gdiobj.c --- GdiDeleteBrushOrPen */
pgO = GdiAllocBatchCommand(NULL, GdiBCDelObj);
if (pgO)
{
/// FIXME: we need to mark the object as deleted!
pgO->hgdiobj = hobj;
return TRUE;
}
return NtGdiDeleteObjectApp(hobj);
6. 内核刷出入口:NtGdiFlushUserBatch
gdibatch.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdibatch.c) 第 485--533 行。函数注释为 "Callback for thread batch flush routine. Think small & fast!"------它被设计成轻量回调,因为每次 GUI 系统调用都可能触发它。
c
/*
* NtGdiFlushUserBatch
*
* Callback for thread batch flush routine.
*
* Think small & fast!
*/
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 is zero and the buffer fills up with delete objects we need
to run anyway.
*/
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);
}
// No need to init anything, just go!
for (; GdiBatchCount > 0; GdiBatchCount--)
{
ULONG Size;
// Process Gdi Batch!
Size = GdiFlushUserBatch(pDC, (PGDIBATCHHDR) pHdr);
if (!Size) break; /* 出错:停止解析(防止死循环) */
pHdr += Size; /* 按记录 Size 推进指针 */
}
if (pDC) DC_UnlockDc(pDC);
// Exit and clear out for the next round.
pTeb->GdiTebBatch.Offset = 0;
pTeb->GdiBatchCount = 0;
pTeb->GdiTebBatch.HDC = 0;
}
}
// FIXME: On Windows XP the function returns &pTeb->RealClientId, maybe VOID?
return STATUS_SUCCESS;
}
流程拆解:
- 计数合法性检查 :
GdiBatchCount > 0且<= GDIBATCHBUFSIZE/4(即 ≤ 310,缓冲最多能容纳的最小记录数,防御异常值)。 - DC 判定 :批 HDC 必须仍是一个合法 DC 句柄(
GreIsHandleValid)才加锁;删除对象批(HDC=0)不锁 DC,pDC = NULL。 - 解析循环 :从头开始逐条调用
GdiFlushUserBatch(pDC, pHdr),返回该记录的字节数Size,pHdr += Size前进;返回 0(如 SEH 捕获到用户缓冲访问违例)则中断。 - 善后 :解锁 DC,并把 TEB 批状态复位(
Offset=0、GdiBatchCount=0、HDC=0),为下一轮攒批做准备。 - 末尾 FIXME 提示:Windows XP 上此函数可能返回
&pTeb->RealClientId(而非 VOID),属于兼容性遗留问题。
注册方式 (ntuser\main.c 的 InitMachineDependents/驱动初始化):
c
CalloutData.BatchFlushRoutine = NtGdiFlushUserBatch;
...
PsEstablishWin32Callouts(&CalloutData);
ntoskrnl\ps\win32.c 将其保存为全局 KeGdiFlushUserBatch,随后在以下位置被调用(已核对源码):
ntoskrnl\ke\i386\traphdlr.c(KiSystemService):进入 GUI 系统调用前,if (NtCurrentTeb()->GdiBatchCount) KeGdiFlushUserBatch();ntoskrnl\ke\i386\usercall.c(KiSystemServiceHandler等):同样按Teb->GdiBatchCount条件调用;ntoskrnl\ke\amd64\traphandler.c与amd64\usercall.c:x64 对应路径。
正是这条"GUI 系统调用入口统一刷出"的机制,保证了批命令在任何内核操作之前必然先落地。
7. 批命令解析执行:GdiFlushUserBatch
gdibatch.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdibatch.c) 第 68--461 行。这是批处理的"内核执行引擎":
c
ULONG
FASTCALL
GdiFlushUserBatch(PDC dc, PGDIBATCHHDR pHdr)
{
ULONG Cmd = 0, Size = 0;
PDC_ATTR pdcattr = NULL;
if (dc) pdcattr = dc->pdcattr;
/* 用 SEH 保护对用户缓冲的读取:批缓冲位于用户态 TEB,必须防访问违例 */
_SEH2_TRY
{
Cmd = pHdr->Cmd;
Size = pHdr->Size; // Return the full size of the structure.
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
DPRINT1("WARNING! GdiBatch Fault!\n");
_SEH2_YIELD(return 0;)
}
_SEH2_END;
switch(Cmd)
{
case GdiBCPatBlt: { ... break; }
case GdiBCPolyPatBlt: { ... break; }
case GdiBCTextOut: { ... break; }
case GdiBCExtTextOut: { ... break; }
case GdiBCSetBrushOrg: { ... break; }
case GdiBCExtSelClipRgn:{ ... break; }
case GdiBCSelObj: { ... break; }
case GdiBCDelRgn: /* fall through */
case GdiBCDelObj: { ... break; }
default: break;
}
return Size;
}
关键点:批缓冲在用户地址空间(TEB),内核直接读取 ,因此整个解析循环都在 _SEH2_TRY 保护之下;一旦读取越界/非法(如缓冲区被恶意破坏),打印 WARNING! GdiBatch Fault! 并返回 0,外层循环随即终止。
下面逐一展开每个 case。
7.1 GdiBCPatBlt ------ 图案填充
c
case GdiBCPatBlt:
{
PGDIBSPATBLT pgDPB;
DWORD dwRop, flags;
HBRUSH hOrgBrush;
COLORREF crColor, crBkColor, crBrushClr;
ULONG ulForegroundClr, ulBackgroundClr, ulBrushClr;
if (!dc) break; /* 无 DC:直接丢弃该命令 */
pgDPB = (PGDIBSPATBLT) pHdr;
/* 把 ROP3 转成 ROP4(前景/背景两个 ROP) */
dwRop = pgDPB->dwRop;
dwRop = MAKEROP4(dwRop & 0xFF0000, dwRop);
/* ROP 使用源表面 → 不可能(PatBlt 无源),丢弃 */
if (WIN32_ROP4_USES_SOURCE(dwRop)) break;
/* 空 DC(信息 DC 或未选表面的内存 DC)→ 无事可做 */
if (dc->dclevel.pSurface == NULL) break;
// 保存当前 DC 属性与脏标志
crColor = dc->pdcattr->crForegroundClr;
crBkColor = dc->pdcattr->ulBackgroundClr;
crBrushClr = dc->pdcattr->crBrushClr;
ulForegroundClr = dc->pdcattr->ulForegroundClr;
ulBackgroundClr = dc->pdcattr->ulBackgroundClr;
ulBrushClr = dc->pdcattr->ulBrushClr;
hOrgBrush = dc->pdcattr->hbrush;
flags = dc->pdcattr->ulDirty_ & (DIRTY_BACKGROUND | DIRTY_TEXT | DIRTY_FILL | DC_BRUSH_DIRTY);
// 用攒批时刻的快照覆盖当前属性
dc->pdcattr->hbrush = pgDPB->hbrush;
dc->pdcattr->crForegroundClr = pgDPB->crForegroundClr;
dc->pdcattr->crBackgroundClr = pgDPB->crBackgroundClr;
dc->pdcattr->crBrushClr = pgDPB->crBrushClr;
dc->pdcattr->ulForegroundClr = pgDPB->ulForegroundClr;
dc->pdcattr->ulBackgroundClr = pgDPB->ulBackgroundClr;
dc->pdcattr->ulBrushClr = pgDPB->ulBrushClr;
// 根据快照与现值的差异,重新物化画刷/文本刷/背景刷
if (dc->pdcattr->ulDirty_ & (DIRTY_FILL | DC_BRUSH_DIRTY))
DC_vUpdateFillBrush(dc);
if (dc->pdcattr->ulDirty_ & DIRTY_TEXT)
DC_vUpdateTextBrush(dc);
if (pdcattr->ulDirty_ & DIRTY_BACKGROUND)
DC_vUpdateBackgroundBrush(dc);
/* 调用内部图案填充(与 NtGdiPatBlt 共享同一实现) */
IntPatBlt(dc, pgDPB->nXLeft, pgDPB->nYLeft,
pgDPB->nWidth, pgDPB->nHeight, dwRop, &dc->eboFill);
// 恢复 DC 属性与脏标志
dc->pdcattr->hbrush = hOrgBrush;
dc->pdcattr->crForegroundClr = crColor;
dc->pdcattr->crBackgroundClr = crBkColor;
dc->pdcattr->crBrushClr = crBrushClr;
dc->pdcattr->ulForegroundClr = ulForegroundClr;
dc->pdcattr->ulBackgroundClr = ulBackgroundClr;
dc->pdcattr->ulBrushClr = ulBrushClr;
dc->pdcattr->ulDirty_ |= flags;
break;
}
要点:
- ROP3→ROP4 :
MAKEROP4(dwRop & 0xFF0000, dwRop)(wingdi.h:MAKEROP4(f,b) ((((b)<<8)&0xFF000000)|(f)))------把 24 位 ROP3 扩展为 32 位 ROP4,前景 ROP 为高字节、背景 ROP 为低字节,与内核NtGdiPatBlt(bitblt.c 1037 行)的处理完全一致。 - 快照-恢复模式 :批记录携带的是攒批时刻的属性;执行前临时覆盖 DC 属性并重算刷子(
DC_vUpdateFillBrush/DC_vUpdateTextBrush/DC_vUpdateBackgroundBrush),执行后恢复。这样批内多条命令互不污染 DC 状态,也兼容"批被延迟执行"的语义。 IntPatBlt(bitblt.c 875 行):真正的填充实现,处理BR_IS_NULL空刷、负宽高等归一化,最终走引擎位块传输。&dc->eboFill是 DC 的扩展画刷对象,经过上面的"设置快照"后其颜色/图案已正确。
7.2 GdiBCPolyPatBlt ------ 多矩形图案填充
c
case GdiBCPolyPatBlt:
{
PGDIBSPPATBLT pgDPB;
EBRUSHOBJ eboFill;
PBRUSH pbrush;
PPATRECT pRects;
INT i;
DWORD dwRop, flags;
COLORREF crColor, crBkColor, crBrushClr;
ULONG ulForegroundClr, ulBackgroundClr, ulBrushClr;
if (!dc) break;
pgDPB = (PGDIBSPPATBLT) pHdr;
dwRop = pgDPB->rop4;
dwRop = MAKEROP4(dwRop & 0xFF0000, dwRop);
if (WIN32_ROP4_USES_SOURCE(dwRop)) break; /* 同 7.1 */
if (dc->dclevel.pSurface == NULL) break; /* 同 7.1 */
/* 保存属性(同 7.1)... */
flags = dc->pdcattr->ulDirty_ & (DIRTY_BACKGROUND | DIRTY_TEXT | DIRTY_FILL | DC_BRUSH_DIRTY);
/* 设置快照:前景/背景/画刷色 */
dc->pdcattr->crForegroundClr = pgDPB->crForegroundClr;
dc->pdcattr->crBackgroundClr = pgDPB->crBackgroundClr;
dc->pdcattr->crBrushClr = pgDPB->crBrushClr;
dc->pdcattr->ulForegroundClr = pgDPB->ulForegroundClr;
dc->pdcattr->ulBackgroundClr = pgDPB->ulBackgroundClr;
dc->pdcattr->ulBrushClr = pgDPB->ulBrushClr;
if (dc->pdcattr->ulDirty_ & DIRTY_TEXT) DC_vUpdateTextBrush(dc);
if (pdcattr->ulDirty_ & DIRTY_BACKGROUND) DC_vUpdateBackgroundBrush(dc);
DPRINT1("GdiBCPolyPatBlt Testing\n"); /* 调试输出(测试期遗留) */
pRects = &pgDPB->pRect[0];
for (i = 0; i < pgDPB->Count; i++)
{
pbrush = BRUSH_ShareLockBrush(pRects->hBrush); /* 锁定该矩形的画刷 */
if (pbrush != NULL)
{
EBRUSHOBJ_vInitFromDC(&eboFill, pbrush, dc); /* 用 DC 快照初始化画刷对象 */
IntPatBlt(
dc,
pRects->r.left,
pRects->r.top,
pRects->r.right, /* ← 实为 nWidth */
pRects->r.bottom, /* ← 实为 nHeight */
dwRop,
&eboFill);
EBRUSHOBJ_vCleanup(&eboFill);
BRUSH_ShareUnlockBrush(pbrush);
}
pRects++;
}
/* 恢复属性(同 7.1)... */
break;
}
要点:
- 结构体布局别名 :用户态
POLYPATBLT = {nXLeft, nYLeft, nWidth, nHeight, hBrush}与内核PATRECT = {RECT r; HBRUSH hBrush;}字节布局相同(20 字节),因此用户态RtlCopyMemory(pgO->pRect, pPoly, nCount * sizeof(PATRECT))是类型双关 。r.right实际装的是nWidth,r.bottom装的是nHeight------所以这里把r.right/r.bottom直接传给IntPatBlt的 Width/Height 参数是有意为之 (与NtGdiPolyPatBlt→IntGdiPolyPatBlt(bitblt.c 992 行)完全一致)。 - 每个矩形使用自己的画刷:
BRUSH_ShareLockBrush加引用计数锁定,EBRUSHOBJ_vInitFromDC按 DC 快照颜色初始化,绘制后EBRUSHOBJ_vCleanup+BRUSH_ShareUnlockBrush释放。 - 与 7.1 不同,PolyPatBlt 的画刷句柄不存于快照字段 ,而是每条记录各自的
pRects[i].hBrush;前景/背景/画刷色仍是全批共享的快照。
7.3 GdiBCTextOut ------ 文本输出(带字符串)
这是最长的 case(约 110 行),核心思想同样是"快照-执行-恢复",但属性比较是按字段逐个进行的,只为真正变化的属性设置脏标志:
c
case GdiBCTextOut:
{
PGDIBSTEXTOUT pgO;
COLORREF crColor = -1, crBkColor;
ULONG ulForegroundClr, ulBackgroundClr;
DWORD flags = 0, flXform = 0, saveflags, saveflXform = 0;
FLONG flTextAlign = -1;
HANDLE hlfntNew;
PRECTL lprc;
USHORT jBkMode;
LONG lBkMode;
POINTL ptlViewportOrg;
if (!dc) break;
pgO = (PGDIBSTEXTOUT) pHdr;
saveflags = dc->pdcattr->ulDirty_ & (DIRTY_BACKGROUND|DIRTY_LINE|DIRTY_TEXT|DIRTY_FILL|DC_BRUSH_DIRTY|DIRTY_CHARSET);
/* ① 前景色不同 → 置 DIRTY_FILL|DIRTY_LINE|DIRTY_TEXT */
if ( dc->pdcattr->crForegroundClr != pgO->crForegroundClr)
{
crColor = dc->pdcattr->crForegroundClr;
dc->pdcattr->crForegroundClr = pgO->crForegroundClr;
ulForegroundClr = dc->pdcattr->ulForegroundClr;
dc->pdcattr->ulForegroundClr = pgO->ulForegroundClr;
flags |= (DIRTY_FILL|DIRTY_LINE|DIRTY_TEXT);
}
/* ② 背景色不同 → 追加 DIRTY_BACKGROUND */
if (dc->pdcattr->crBackgroundClr != pgO->crBackgroundClr)
{
crBkColor = dc->pdcattr->ulBackgroundClr;
dc->pdcattr->crBackgroundClr = pgO->crBackgroundClr;
ulBackgroundClr = dc->pdcattr->ulBackgroundClr;
dc->pdcattr->ulBackgroundClr = pgO->ulBackgroundClr;
flags |= (DIRTY_FILL|DIRTY_LINE|DIRTY_TEXT|DIRTY_BACKGROUND);
}
/* ③ 文本对齐不同 */
if (dc->pdcattr->flTextAlign != pgO->flTextAlign)
{
flTextAlign = dc->pdcattr->flTextAlign;
dc->pdcattr->flTextAlign = pgO->flTextAlign;
}
/* ④ 字体不同 → DIRTY_CHARSET,并清除 SLOW_WIDTHS 缓存 */
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->ptlViewportOrg.y != pgO->ptlViewportOrg.y )
{
saveflXform = dc->pdcattr->flXform & (PAGE_XLATE_CHANGED|WORLD_XFORM_CHANGED|DEVICE_TO_WORLD_INVALID);
ptlViewportOrg = dc->pdcattr->ptlViewportOrg;
dc->pdcattr->ptlViewportOrg = pgO->ptlViewportOrg;
flXform = (PAGE_XLATE_CHANGED|WORLD_XFORM_CHANGED|DEVICE_TO_WORLD_INVALID);
}
dc->pdcattr->flXform |= flXform;
dc->pdcattr->ulDirty_ |= flags;
/* ⑥ 背景模式(OPAQUE/TRANSPARENT):jBkMode 与 lBkMode 都要换 */
jBkMode = dc->pdcattr->jBkMode;
dc->pdcattr->jBkMode = pgO->lBkMode;
lBkMode = dc->pdcattr->lBkMode;
dc->pdcattr->lBkMode = pgO->lBkMode;
/* ⑦ 解包矩形:GDIBS_NORECT 表示用户未给 lprc */
lprc = (pgO->Options & GDIBS_NORECT) ? NULL : &pgO->Rect;
pgO->Options &= ~GDIBS_NORECT;
/* ⑧ 真正绘制:字符串在 Size 字节之后,Dx 在 Buffer 处 */
IntExtTextOutW( dc,
pgO->x,
pgO->y,
pgO->Options,
lprc,
(LPCWSTR)&pgO->String[pgO->Size/sizeof(WCHAR)],
pgO->cbCount,
pgO->Size ? (const INT *)&pgO->Buffer : NULL,
pgO->iCS_CP );
// 恢复:背景模式
dc->pdcattr->jBkMode = jBkMode;
dc->pdcattr->lBkMode = lBkMode;
if (saveflXform)
{
dc->pdcattr->ptlViewportOrg = ptlViewportOrg;
dc->pdcattr->flXform |= saveflXform|flXform;
}
/* 逐项恢复被临时改动的属性 */
if (flags & DIRTY_TEXT && crColor != -1)
{
dc->pdcattr->crForegroundClr = crColor;
dc->pdcattr->ulForegroundClr = ulForegroundClr;
}
if (flags & DIRTY_BACKGROUND)
{
dc->pdcattr->crBackgroundClr = crBkColor;
dc->pdcattr->ulBackgroundClr = ulBackgroundClr;
}
if (flTextAlign != -1)
{
dc->pdcattr->flTextAlign = flTextAlign;
}
if (flags & DIRTY_CHARSET)
{
dc->pdcattr->hlfntNew = hlfntNew;
dc->pdcattr->ulDirty_ &= ~SLOW_WIDTHS;
}
dc->pdcattr->ulDirty_ |= saveflags | flags;
dc->pdcattr->flXform |= saveflXform | flXform;
break;
}
要点:
- 逐字段差异比较:与 7.1/7.2 的"无脑快照覆盖"不同,文本路径会先比较"当前值 vs 快照值",只为实际变化的属性设置脏标志(①~⑤)。原因:文本绘制依赖的刷子(前景/背景/文本刷)物化代价高,能省则省。
- 字体切换 (④):换字体后清除
SLOW_WIDTHS缓存并置DIRTY_CHARSET,让后续字符宽度查询重建。 - 视口原点 (⑤):变化时置
PAGE_XLATE_CHANGED | WORLD_XFORM_CHANGED | DEVICE_TO_WORLD_INVALID,使IntLPtoDP/IntExtTextOutW内部的坐标变换矩阵重算;恢复时用saveflXform保留原标志。 - GDIBS_NORECT 私有位 :用户态在无
lprc时置位,内核解包前先清除,避免把私有位传给IntExtTextOutW。 IntExtTextOutW(freetype.c 6729 行):FreeType 文本渲染的公共入口(详见《分析_16》),处理TA_UPDATECP、ETO_CLIPPED/ETO_OPAQUE、路径(PATH_IsPathOpen)、字形缓存与字体链接等。批路径传入的dc已由NtGdiFlushUserBatch加锁,且属性已临时就位。
7.4 GdiBCExtTextOut ------ 空串不透明矩形
专用于"ExtTextOutW(hdc, x, y, ETO_OPAQUE, &rect, NULL, 0, NULL)"这类用背景色填充矩形的调用:
c
case GdiBCExtTextOut:
{
PGDIBSEXTTEXTOUT pgO;
COLORREF crBkColor;
ULONG ulBackgroundClr;
POINTL ptlViewportOrg;
DWORD flags = 0, flXform = 0, saveflags, saveflXform = 0;
if (!dc) break;
pgO = (PGDIBSEXTTEXTOUT) pHdr;
saveflags = dc->pdcattr->ulDirty_ & (DIRTY_BACKGROUND|DIRTY_TEXT|DIRTY_FILL|DC_BRUSH_DIRTY|DIRTY_CHARSET);
/* 背景色变化 → 重算背景/行/填充刷 */
if (dc->pdcattr->crBackgroundClr != pgO->ulBackgroundClr)
{
crBkColor = dc->pdcattr->crBackgroundClr;
ulBackgroundClr = dc->pdcattr->ulBackgroundClr;
dc->pdcattr->crBackgroundClr = pgO->ulBackgroundClr;
dc->pdcattr->ulBackgroundClr = pgO->ulBackgroundClr;
flags |= (DIRTY_BACKGROUND|DIRTY_LINE|DIRTY_FILL);
}
/* 视口原点变化 → 变换重算标志 */
if ( dc->pdcattr->ptlViewportOrg.x != pgO->ptlViewportOrg.x ||
dc->pdcattr->ptlViewportOrg.y != pgO->ptlViewportOrg.y )
{
saveflXform = dc->pdcattr->flXform & (PAGE_XLATE_CHANGED|WORLD_XFORM_CHANGED|DEVICE_TO_WORLD_INVALID);
ptlViewportOrg = dc->pdcattr->ptlViewportOrg;
dc->pdcattr->ptlViewportOrg = pgO->ptlViewportOrg;
flXform = (PAGE_XLATE_CHANGED|WORLD_XFORM_CHANGED|DEVICE_TO_WORLD_INVALID);
}
dc->pdcattr->flXform |= flXform;
dc->pdcattr->ulDirty_ |= flags;
/* 空字符串 + Count=0 + 矩形:IntExtTextOutW 会以背景色填满 Rect */
IntExtTextOutW( dc,
0, /* x:无串,起点无意义 */
0, /* y */
pgO->Options, /* 含 ETO_OPAQUE */
&pgO->Rect, /* 填充矩形 */
NULL, /* 无字符串 */
pgO->Count, /* 0 */
NULL, /* 无 Dx */
0 ); /* 代码页 0 */
if (saveflXform)
{
dc->pdcattr->ptlViewportOrg = ptlViewportOrg;
dc->pdcattr->flXform |= saveflXform|flXform;
}
if (flags & DIRTY_BACKGROUND)
{
dc->pdcattr->crBackgroundClr = crBkColor;
dc->pdcattr->ulBackgroundClr = ulBackgroundClr;
}
dc->pdcattr->ulDirty_ |= saveflags | flags;
dc->pdcattr->flXform |= saveflXform | flXform;
break;
}
要点:
- 这是最廉价的批命令之一:不涉及字体/字形,只按背景色填充矩形。
- 用户态分支只在"
lprc非空、cwc==0、ETO_OPAQUE"时生成此命令;若带字符串但串太长或ETO_GLYPH_INDEX等,则走NtGdiExtTextOutW。 - 注意该记录没有保存
x/y:因为无字符串,起点无意义,内核固定传0,0。
7.5 GdiBCSetBrushOrg ------ 设置画刷原点
c
case GdiBCSetBrushOrg:
{
PGDIBSSETBRHORG pgSBO;
if (!dc) break;
pgSBO = (PGDIBSSETBRHORG) pHdr;
/* 同步用户态 DC_ATTR 与内核 DC 的画刷原点 */
pdcattr->ptlBrushOrigin = pgSBO->ptlBrushOrigin;
DC_vSetBrushOrigin(dc, pgSBO->ptlBrushOrigin.x, pgSBO->ptlBrushOrigin.y);
break;
}
要点:
- 该命令无"保存/恢复"环节------
SetBrushOrgEx是持久状态修改,直接生效即可。 DC_vSetBrushOrigin(dcobjs.c 142 行)不仅更新dclevel.ptlBrushOrigin,还同步更新填充原点ptlFillOrigin = 原点 + DC 原点,保证后续图案填充的对齐正确。- 用户态在攒批时已经立即更新了用户态
DC_ATTR(第 5.4 节),此处只是把状态同步到内核 DC 对象。
7.6 GdiBCExtSelClipRgn ------ 扩展选择裁剪矩形
c
case GdiBCExtSelClipRgn:
{
PGDIBSEXTSELCLPRGN pgO;
if (!dc) break;
pgO = (PGDIBSEXTSELCLPRGN) pHdr;
/* 用矩形 + 合并模式更新裁剪区 */
IntGdiExtSelectClipRect( dc, &pgO->rcl, pgO->fnMode);
break;
}
要点:
- 用户态只在"区域可表示为单个矩形"时使用此批命令(第 5.5 节);复杂区域仍走
NtGdiExtSelectClipRgn。 IntGdiExtSelectClipRect是内核内部裁剪区操作,按fnMode(RGN_AND/RGN_OR/RGN_XOR/RGN_DIFF/RGN_COPY)把矩形合并进 DC 的可见区域,并联动更新 DC 原点相关缓存。- 该命令同样不涉及属性快照,直接执行。
7.7 GdiBCSelObj ------ 选择字体
c
case GdiBCSelObj:
{
PGDIBSOBJECT pgO;
if (!dc) break;
pgO = (PGDIBSOBJECT) pHdr;
/* 在内核 DC 中选择字体(批命令仅覆盖字体) */
DC_hSelectFont(dc, (HFONT)pgO->hgdiobj);
break;
}
要点:
- 用户态
GdiSelectFont已经先行更新了pdcattr->hlfntNew并置DIRTY_CHARSET(第 5.6 节),内核此处用DC_hSelectFont把字体真正装载进内核 DC(更新dclevel.plfnt、字体对象引用计数等)。 - 该命令的延迟执行是安全的:字体选择只影响后续文本绘制与度量查询,不影响已绘制内容;Windows 的批处理同样只批量选择字体。
7.8 GdiBCDelRgn / GdiBCDelObj ------ 删除对象
c
case GdiBCDelRgn:
DPRINT("Delete Region Object!\n");
/* Fall through */
case GdiBCDelObj:
{
PGDIBSOBJECT pgO = (PGDIBSOBJECT) pHdr;
GreDeleteObject( pgO->hgdiobj );
break;
}
要点:
- 两个命令共用
GDIBSOBJECT结构,GdiBCDelRgn仅多打印一条调试信息后贯通 到GdiBCDelObj。 GreDeleteObject是内核通用对象删除例程,按句柄类型分发到区域/画刷/画笔等对象的销毁逻辑。- 它们以
hdc = NULL入批(第 5.7 节),执行时dc == NULL------此 case 不检查dc,直接删除即可,因此与"批无 DC"的场景天然兼容。 - 潜在风险(源码中 FIXME 注释):对象在攒批与执行之间可能已被用户态再次使用,延迟删除存在句柄复用窗口;当前实现依赖"对象在被删除前不会被复用"的应用行为。
7.9 default ------ 未知命令
c
default:
break;
静默忽略未知命令号并返回 Size(继续遍历后续记录)。这是防御性设计:未来新增命令号时旧内核不会崩溃,只会跳过。
8. 设备同步:DoDeviceSync / SynchronizeDriver / NtGdiFlush
批处理除了执行绘制命令,还负责驱动同步------确保显示驱动(如 PanDisplay)在批刷出后把硬件/软件渲染状态对齐。
8.1 DoDeviceSync
c
//
// DoDeviceSync
//
// based on IntEngEnter from eng/engmisc.c
//
VOID
FASTCALL
DoDeviceSync( SURFOBJ *Surface, PRECTL Rect, FLONG fl)
{
PPDEVOBJ Device = (PDEVOBJ*)Surface->hdev;
// No punting and "Handle to a surface, provided that the surface is device-managed.
// Otherwise, dhsurf is zero".
if (!(Device->flFlags & PDEV_DRIVER_PUNTED_CALL) && (Surface->dhsurf))
{
if (Device->DriverFunctions.SynchronizeSurface)
{
Device->DriverFunctions.SynchronizeSurface(Surface, Rect, fl);
}
else
{
if (Device->DriverFunctions.Synchronize)
{
Device->DriverFunctions.Synchronize(Surface->dhpdev, Rect);
}
}
}
}
要点:
- 前置条件两条(注释即源码语义):
- 不 punting :
PDEV_DRIVER_PUNTED_CALL未置位------若驱动把调用"踢回"(punting)给引擎处理,则无需同步; - 设备托管表面 :
Surface->dhsurf非零------dhsurf是"设备管理表面时给引擎的句柄,否则为 0"。
- 不 punting :
- 同步方式二选一:优先
DrvSynchronizeSurface(Surface, Rect, fl)(Winddi 新式逐表面同步),否则退化为DrvSynchronize(dhpdev, Rect)(旧式按设备同步)。 - 注释 "based on IntEngEnter from eng/engmisc.c":本函数参照引擎
IntEngEnter的设备进入逻辑实现(见第 10 章)。
8.2 SynchronizeDriver
c
VOID
FASTCALL
SynchronizeDriver(FLONG Flags)
{
SURFOBJ *SurfObj;
if (Flags & GCAPS2_SYNCFLUSH)
Flags = DSS_FLUSH_EVENT;
if (Flags & GCAPS2_SYNCTIMER)
Flags = DSS_TIMER_EVENT;
//Device = IntEnumHDev();
// UNIMPLEMENTED;
//ASSERT(FALSE);
SurfObj = 0;// EngLockSurface( Device->pSurface );
if(!SurfObj) return;
DoDeviceSync( SurfObj, NULL, Flags);
EngUnlockSurface(SurfObj);
return;
}
要点:
- 把图形能力标志映射为同步事件:
GCAPS2_SYNCFLUSH → DSS_FLUSH_EVENT(刷出完成事件),GCAPS2_SYNCTIMER → DSS_TIMER_EVENT(定时同步事件)。 - 当前实现为半成品 :
IntEnumHDev/EngLockSurface被注释掉(UNIMPLEMENTED),SurfObj恒为 0,因此if(!SurfObj) return;直接返回------DoDeviceSync实际不会被执行。这是 ReactOS 尚未完善的已知部分,显示驱动的同步目前主要依赖引擎其他路径。 GCAPS2_SYNCFLUSH | GCAPS2_SYNCTIMER由显示驱动声明(见eng\pandisp.c的pdi->flGraphicsCaps2),表明驱动希望 GDI 在刷出时调用其同步回调。
8.3 NtGdiFlush
c
/*
* NtGdiFlush
*
* Flushes the calling thread's current batch.
*/
__kernel_entry
NTSTATUS
APIENTRY
NtGdiFlush(
VOID)
{
SynchronizeDriver(GCAPS2_SYNCFLUSH);
return STATUS_SUCCESS;
}
要点:
- 用户态
GdiFlush()/GdiAllocBatchCommand满批时调用此系统调用。 - 函数体本身并不解析批缓冲 ------用户批的清理由系统调用入口路径完成(
KiSystemService在进入本调用前发现GdiBatchCount != 0,先行调用KeGdiFlushUserBatch,见第 6 章)。因此NtGdiFlush只负责"刷出之后"的驱动同步。 - 返回
STATUS_SUCCESS,用户态GdiFlush恒返回 TRUE。
9. 批处理机制详解:生命周期与触发时机
9.1 一次批的完整生命周期
时间线 ─────────────────────────────────────────────────────────────────►
[攒批阶段] [刷出阶段] [下一轮]
应用调用 PatBlt/TextOut/... 下一次任意 win32k 系统调用 批状态复位
│ │ (KiSystemService 入口) Offset=0
▼ ▼ GdiBatchCount=0
GdiAllocBatchCommand KeGdiFlushUserBatch HDC=0
│ 校验环境 │
│ 绑定/校验批 DC │ NtGdiFlushUserBatch
│ 容量检查(满→刷出) │ │ 校验 GdiBatchCount
│ 写记录(头+参数+属性快照) │ │ 按 DC 类型锁定 DC
│ Offset+=Size;Count++ │ ▼
▼ GdiFlushUserBatch ×N(循环)
逐条: SEH 读头 → switch(Cmd) → 执行
失败(Size==0) → 中断循环
9.2 刷出触发时机汇总
| 触发点 | 调用方 | 说明 |
|---|---|---|
| 缓冲满(条数) | GdiAllocBatchCommand |
GdiBatchCount >= GDI_BatchLimit(默认 20)时调 NtGdiFlush() |
| 缓冲满(字节) | GdiAllocBatchCommand |
Offset + cjSize > GDIBATCHBUFSIZE(1240 字节)时调 NtGdiFlush() |
| 应用显式刷出 | GdiFlush() |
用户态直接调 NtGdiFlush();也常被 GdiSetBatchLimit 等内部使用 |
| 任意 GUI 系统调用入口 | KiSystemService(traphdlr.c:1816) |
if (Teb->GdiBatchCount) KeGdiFlushUserBatch(); ------ 最主要的被动刷出点 |
| GUI 系统调用处理器 | KiSystemServiceHandler(usercall.c:203,i386/amd64) |
同上 |
| x64 陷阱处理 | traphandler.c:199(amd64) |
同上 |
| 线程/进程终结清理 | 内核 BatchFlushRoutine 回调 |
由 PsEstablishWin32Callouts 注册,供内核在需要时调用 |
结论:除了"满批"与"显式 Flush"两个主动触发点外,任何一次 win32k 系统调用(包括 GetMessage、GetDC、乃至下一次绘制)都会被动刷出当前线程的批。这意味着批的"滞留窗口"至多到线程下一次进入内核为止,批命令不会无限期悬挂。
9.3 与消息循环的关系
Windows 规范要求线程在取消息(GetMessage/PeekMessage)后,其之前的 GDI 绘制必须已经可见。ReactOS 的批机制通过两条路径满足该语义:
NtUserGetMessage本身是 win32k 系统调用,KiSystemService在其入口处就已刷出批;- 即便不经过消息循环,任何后续 GDI 调用同样触发刷出。
因此应用无需在每次绘制后手动 GdiFlush,除非它需要立即看到绘制结果(例如与硬件同步、测量性能)。
9.4 正确性设计要点
- 用户态/内核态属性一致性:批命令携带"属性快照",内核执行时临时覆盖、执行后恢复(第 7 章),确保批命令的执行结果与"逐条立即执行"等价;
- DC 生命周期 :刷出时用
GreIsHandleValid+DC_LockDc校验并锁定批 DC;若 DC 已被销毁,DC_LockDc失败,pDC == NULL,所有依赖 DC 的命令被跳过(if (!dc) break;),仅删除类命令继续执行; - 用户缓冲安全 :批缓冲在用户地址空间,全部读取置于
_SEH2_TRY保护下,非法访问返回 0 终止解析; - 单线程性:批状态在 TEB 中,天然线程私有,无需全局锁。
10. 与 IntEngEnter 的关系
gdibatch.c 中 DoDeviceSync 的注释明确写着 "based on IntEngEnter from eng/engmisc.c"------即驱动同步逻辑取自 GDI 引擎的设备进入例程 IntEngEnter。二者处理的是同一类问题:GDI 引擎/批处理要访问设备表面时,必须保证设备侧状态就绪。
IntEngEnter(win32ss\gdi\eng\engmisc.c,引擎进入例程)的职责(依据源码):
- 归一化目标矩形(左右/上下颠倒时交换端点);
- 若目标表面非位图表面(
STYPE_BITMAP)且无pvScan0/lDelta(设备管理表面),则要求驱动支持DrvCopyBits(HOOK_COPYBITS),否则返回 FALSE; - 需要时
EngCreateBitmap创建临时位图作为输出,把设备表面访问转换为软件位图访问,并记录还原所需的DestRect等现场。
DoDeviceSync 则简化聚焦到"同步"本身:
- 跳过 punting 的驱动(
PDEV_DRIVER_PUNTED_CALL); - 仅在设备托管表面(
dhsurf非零)时调用DrvSynchronizeSurface或DrvSynchronize; - 不创建临时表面------同步只是"让设备把内部状态刷到位",不涉及表面内容的搬运。
两者互补:IntEngEnter 解决"能不能直接访问"(不行就中转),DoDeviceSync 解决"访问之前先同步"。批处理的解析执行发生在 win32k 内(直接操作 DC/表面),因此只需后者;引擎路径(eng/ 系列)同时需要前者。这也是《分析_25》7.1 节所述"设备表面兼容"问题的两个侧面。
11. 调用链
11.1 攒批(用户态)
#mermaid-svg-3igjX9DOANuWLtSp{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-3igjX9DOANuWLtSp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3igjX9DOANuWLtSp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3igjX9DOANuWLtSp .error-icon{fill:#552222;}#mermaid-svg-3igjX9DOANuWLtSp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3igjX9DOANuWLtSp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3igjX9DOANuWLtSp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3igjX9DOANuWLtSp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3igjX9DOANuWLtSp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3igjX9DOANuWLtSp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3igjX9DOANuWLtSp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3igjX9DOANuWLtSp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3igjX9DOANuWLtSp .marker.cross{stroke:#333333;}#mermaid-svg-3igjX9DOANuWLtSp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3igjX9DOANuWLtSp p{margin:0;}#mermaid-svg-3igjX9DOANuWLtSp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3igjX9DOANuWLtSp .cluster-label text{fill:#333;}#mermaid-svg-3igjX9DOANuWLtSp .cluster-label span{color:#333;}#mermaid-svg-3igjX9DOANuWLtSp .cluster-label span p{background-color:transparent;}#mermaid-svg-3igjX9DOANuWLtSp .label text,#mermaid-svg-3igjX9DOANuWLtSp span{fill:#333;color:#333;}#mermaid-svg-3igjX9DOANuWLtSp .node rect,#mermaid-svg-3igjX9DOANuWLtSp .node circle,#mermaid-svg-3igjX9DOANuWLtSp .node ellipse,#mermaid-svg-3igjX9DOANuWLtSp .node polygon,#mermaid-svg-3igjX9DOANuWLtSp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3igjX9DOANuWLtSp .rough-node .label text,#mermaid-svg-3igjX9DOANuWLtSp .node .label text,#mermaid-svg-3igjX9DOANuWLtSp .image-shape .label,#mermaid-svg-3igjX9DOANuWLtSp .icon-shape .label{text-anchor:middle;}#mermaid-svg-3igjX9DOANuWLtSp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3igjX9DOANuWLtSp .rough-node .label,#mermaid-svg-3igjX9DOANuWLtSp .node .label,#mermaid-svg-3igjX9DOANuWLtSp .image-shape .label,#mermaid-svg-3igjX9DOANuWLtSp .icon-shape .label{text-align:center;}#mermaid-svg-3igjX9DOANuWLtSp .node.clickable{cursor:pointer;}#mermaid-svg-3igjX9DOANuWLtSp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3igjX9DOANuWLtSp .arrowheadPath{fill:#333333;}#mermaid-svg-3igjX9DOANuWLtSp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3igjX9DOANuWLtSp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3igjX9DOANuWLtSp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3igjX9DOANuWLtSp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3igjX9DOANuWLtSp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3igjX9DOANuWLtSp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3igjX9DOANuWLtSp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3igjX9DOANuWLtSp .cluster text{fill:#333;}#mermaid-svg-3igjX9DOANuWLtSp .cluster span{color:#333;}#mermaid-svg-3igjX9DOANuWLtSp 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-3igjX9DOANuWLtSp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3igjX9DOANuWLtSp rect.text{fill:none;stroke-width:0;}#mermaid-svg-3igjX9DOANuWLtSp .icon-shape,#mermaid-svg-3igjX9DOANuWLtSp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3igjX9DOANuWLtSp .icon-shape p,#mermaid-svg-3igjX9DOANuWLtSp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3igjX9DOANuWLtSp .icon-shape .label rect,#mermaid-svg-3igjX9DOANuWLtSp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3igjX9DOANuWLtSp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3igjX9DOANuWLtSp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3igjX9DOANuWLtSp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
否
应用程序
gdi32.dll PatBlt / PolyPatBlt / ExtTextOutW / SetBrushOrgEx / ExtSelectClipRgn / GdiSelectFont / DeleteRegion / GdiDeleteBrushOrPen
GdiGetDcAttr 成功
且非 DIB section?
直接 NtGdi* 系统调用
GdiAllocBatchCommand
TEB 可用
批 DC 一致?
批满?
Count>=Limit 或 超字节
NtGdiFlush 系统调用
内核: 入口先刷出旧批
NtGdiFlushUserBatch
批状态复位
HDC=0
写入批记录
头+参数+属性快照
Offset+=Size
GdiBatchCount++
返回 TRUE 给应用
11.2 刷出(内核态)
渲染错误: Mermaid 渲染失败: Parse error on line 35: ...ync → DrvSynchronize(Surface)] -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
11.3 关键函数对照表
| 层 | 函数 | 文件 |
|---|---|---|
| 用户态 | GdiAllocBatchCommand |
win32ss\gdi\gdi32\include\gdi32p.h |
| 用户态 | PatBlt / BitBlt |
win32ss\gdi\gdi32\objects\painting.c |
| 用户态 | PolyPatBlt |
win32ss\gdi\gdi32\objects\painting.c |
| 用户态 | ExtTextOutW |
win32ss\gdi\gdi32\objects\text.c |
| 用户态 | SetBrushOrgEx |
win32ss\gdi\gdi32\objects\brush.c |
| 用户态 | ExtSelectClipRgn / DeleteRegion |
win32ss\gdi\gdi32\objects\region.c |
| 用户态 | GdiSelectFont |
win32ss\gdi\gdi32\objects\dc.c |
| 用户态 | GdiDeleteBrushOrPen |
win32ss\gdi\gdi32\objects\gdiobj.c |
| 用户态 | GdiFlush / GdiSetBatchLimit |
win32ss\gdi\gdi32\misc\misc.c |
| 用户态 | GdiProcessSetup(读 PEB) |
win32ss\gdi\gdi32\main\dllmain.c |
| 内核 | NtGdiFlush / NtGdiFlushUserBatch / GdiFlushUserBatch / DoDeviceSync / SynchronizeDriver |
win32ss\gdi\ntgdi\gdibatch.c |
| 内核 | GdiProcessCreate(写 PEB) |
win32ss\gdi\ntgdi\init.c |
| 内核 | IntPatBlt / NtGdiPatBlt / NtGdiPolyPatBlt |
win32ss\gdi\ntgdi\bitblt.c |
| 内核 | IntExtTextOutW |
win32ss\gdi\ntgdi\freetype.c |
| 内核 | DC_vSetBrushOrigin |
win32ss\gdi\ntgdi\dcobjs.c |
| 内核 | IntEngEnter(设计参照) |
win32ss\gdi\eng\engmisc.c |
| 内核 | 批命令/记录结构定义 | win32ss\include\ntgdityp.h |
| 内核 | 脏标志 / DC_ATTR |
win32ss\include\ntgdihdl.h |
| 内核 | BatchFlushRoutine 注册 |
win32ss\user\ntuser\main.c |
| 内核 | KeGdiFlushUserBatch 保存 |
ntoskrnl\ps\win32.c |
| 内核 | 系统调用入口刷出 | ntoskrnl\ke\i386\traphdlr.c、usercall.c、amd64\traphandler.c |
| 用户态 | TEB GDI_TEB_BATCH |
sdk\include\ndk\peb_teb.h |
| 用户态 | GDI_BATCH_LIMIT/GDIBATCHBUFSIZE |
win32ss\include\ntgdityp.h |
12. 源码索引
| 文件 | 关键内容 |
|---|---|
| gdibatch.c(file:///d:/reactos/win32ss/gdi/ntgdi/gdibatch.c) | 批处理刷出核心:NtGdiFlush、NtGdiFlushUserBatch、GdiFlushUserBatch(9 命令分发)、DoDeviceSync、SynchronizeDriver |
| init.c(file:///d:/reactos/win32ss/gdi/ntgdi/init.c) | GdiProcessCreate:Peb->GdiDCAttributeList = GDI_BATCH_LIMIT(20) |
| ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h) | GDIBATCHCMD 枚举、GDIBATCHBUFSIZE、GDI_BATCH_LIMIT、全部 GDIB* 批记录结构 |
| ntgdihdl.h(file:///d:/reactos/win32ss/include/ntgdihdl.h) | DC_ATTR 结构、DIRTY_*/SLOW_WIDTHS/DC_BRUSH_DIRTY/DC_DIBSECTION 等脏标志、flXform 相关标志 |
| peb_teb.h(file:///d:/reactos/sdk/include/ndk/peb_teb.h) | TEB GDI_TEB_BATCH(Offset/HDC/Buffer)、GdiBatchCount 字段布局 |
| gdi32p.h(file:///d:/reactos/win32ss/gdi/gdi32/include/gdi32p.h) | 用户态 GdiAllocBatchCommand(攒批核心)、GdiGetDcAttr |
| dllmain.c(file:///d:/reactos/win32ss/gdi/gdi32/main/dllmain.c) | GdiProcessSetup:读 PEB 初始化 GDI_BatchLimit |
| misc.c(file:///d:/reactos/win32ss/gdi/gdi32/misc/misc.c) | GdiFlush、GdiSetBatchLimit、GdiGetBatchLimit |
| painting.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/painting.c) | PatBlt/BitBlt/PolyPatBlt 攒批 |
| text.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/text.c) | ExtTextOutW 攒批(GdiBCTextOut/GdiBCExtTextOut) |
| brush.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/brush.c) | SetBrushOrgEx 攒批 |
| region.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/region.c) | ExtSelectClipRgn/DeleteRegion 攒批 |
| dc.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/dc.c) | GdiSelectFont 攒批 |
| gdiobj.c(file:///d:/reactos/win32ss/gdi/gdi32/objects/gdiobj.c) | GdiDeleteBrushOrPen 攒批 |
| bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c) | IntPatBlt、NtGdiPatBlt、NtGdiPolyPatBlt/IntGdiPolyPatBlt |
| freetype.c(file:///d:/reactos/win32ss/gdi/ntgdi/freetype.c) | IntExtTextOutW(FreeType 文本渲染公共入口) |
| engmisc.c(file:///d:/reactos/win32ss/gdi/eng/engmisc.c) | IntEngEnter(DoDeviceSync 的设计参照) |
| main.c(file:///d:/reactos/win32ss/user/ntuser/main.c) | PsEstablishWin32Callouts 注册 BatchFlushRoutine = NtGdiFlushUserBatch |
| win32.c(file:///d:/reactos/ntoskrnl/ps/win32.c) | 保存 KeGdiFlushUserBatch |
| traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) | GUI 系统调用入口刷出 |
| usercall.c(file:///d:/reactos/ntoskrnl/ke/i386/usercall.c) | GUI 系统调用处理器刷出 |
| traphandler.c(file:///d:/reactos/ntoskrnl/ke/amd64/traphandler.c) | x64 对应刷出 |
关联模块: init.c(批缓冲初始化/PEB 写入)、bitblt.c(PatBlt 语义)、freetype.c(文本输出)、dcobjs.c(画刷原点)、engmisc.c(IntEngEnter 参照)、ntuser/main.c(回调注册)、ntoskrnl(系统调用入口刷出)。
13. 总结
ReactOS 的 GDI 批处理是一条贯穿用户态与内核态的成熟优化路径:
- 攒批(用户态) :
GdiAllocBatchCommand把PatBlt/PolyPatBlt/TextOut/ExtTextOut(空串不透明)/SetBrushOrg/ExtSelectClipRgn/SelectFont/DeleteRegion/DeleteObject九类高频小操作连同 DC 属性快照写入线程 TEB 的GdiTebBatch,条数上限GDI_BATCH_LIMIT(20,源自Peb->GdiDCAttributeList)、字节上限GDIBATCHBUFSIZE(1240)。 - 刷出(内核调度层) :任意 win32k 系统调用入口、满批、显式
GdiFlush都会触发KeGdiFlushUserBatch→NtGdiFlushUserBatch,后者锁定批 DC 后循环调用GdiFlushUserBatch。 - 执行(内核解析器) :
GdiFlushUserBatch以 SEH 保护读取用户缓冲,按GDIBATCHHDR.Cmd分发------图案填充走IntPatBlt、文本走IntExtTextOutW、其余走DC_*/Gre*内部函数;属性采用"保存→快照覆盖→执行→恢复"模式保证语义等价。 - 同步(驱动层) :
NtGdiFlush经SynchronizeDriver/DoDeviceSync调用DrvSynchronize(Surface)完成刷出后的设备同步(当前SynchronizeDriver因EngLockSurface未实现而为空操作,属于待完善项)。
该机制把"每操作一次系统调用"降为"每批一次",是 Windows 兼容图形栈性能的关键一环;同时其 TEB 布局与 Windows 一致,为二进制兼容的批处理行为提供了基础。
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/gdibatch.c及关联模块分析(2026 年 8 月)。文中所有函数签名、结构布局、标志位与调用关系均以源码为准。