ReactOS 图形系统分析(47):GDI 批处理 — gdibatch.c

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 次,显著减少用户态/内核态切换的开销。典型受益操作包括 TextOutPatBltSetBrushOrgExtSelectClipRgnSelectObjectDeleteObject 等高频小操作。

整体架构如下(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 │
                                              └──────────────────────────────────────────┘

图中四个关键执行点:

  1. 刷出触发点(内核调度层)ntoskrnl\ke\i386\traphdlr.cKiSystemService 在进入任何 win32k(GUI)系统调用前,若 NtCurrentTeb()->GdiBatchCount 非零,就调用 KeGdiFlushUserBatch()
  2. 刷出入口(本文件)NtGdiFlushUserBatch ------ 注册到 WIN32_CALLOUTSBatchFlushRoutine(见 ntuser\main.cPsEstablishWin32Callouts),内核在任何需要同步批处理的地方调用它。
  3. 批命令解析器(本文件)GdiFlushUserBatch ------ 逐条读取 GDIBATCHHDR,按命令号分发到对应的 Int*/DC_*/Gre* 内部函数。
  4. 显式刷出 + 设备同步(本文件)NtGdiFlush ------ 用户态 GdiFlush/GdiAllocBatchCommand 满批时调用;它本身只负责驱动同步(SynchronizeDriver(GCAPS2_SYNCFLUSH)),真正的用户批缓冲清理由第 1 点的系统调用入口路径顺带完成。

设计要点:用户批的解析执行发生在"下一次任意 win32k 系统调用"时 (而不是由 NtGdiFlush 显式执行)。这保证无论应用程序通过哪条路径进入内核(画线、取消息、查询状态......),之前攒下的所有绘制命令都已先行落地,语义与 Windows 一致。

2. 设计动机:减少系统调用

在图形密集型应用中,TextOutPatBltSetBrushOrg 这类操作往往以极高频次出现,且每次调用都只携带几十字节的参数。如果每次都走系统调用:

  • 每次调用都要经历用户态→内核态切换(保存/恢复寄存器、栈切换、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.cGdiProcessCreate()每个进程创建时写入:

    c 复制代码
    Process->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.cGdiProcessSetup() 在 gdi32 初始化时读取:

    c 复制代码
    GDI_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 句柄。一批内所有命令必须属于同一个 DCGdiAllocBatchCommand 强制检查),否则拒绝入批直接走系统调用;唯一例外是"纯对象删除"类命令(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),调用方还要追加变长数据并修正 SizeOffset(见第 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.hPOLYPATBLT = {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 已经推进了 OffsetGdiBatchCount,若变长数据放不下,必须把它们减回去,再走 NtGdiPolyPatBlt。变长命令(TextOut 同理)都遵循这一"失败回滚"约定。

5.3 ExtTextOutW / TextOut

gdi32\objects\text.cExtTextOutW 是批路径最复杂的一支。前置检查:

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.cExtSelectClipRgn 在"区域为矩形"时走批命令:

c 复制代码
PGDIBSEXTSELCLPRGN pgO = GdiAllocBatchCommand(hdc, GdiBCExtSelClipRgn);
if (pgO)
{
    pgO->fnMode = fnMode;
    pgO->rcl    = Rect;   /* 矩形区域 */
    return hrgn;          /* 返回原区域句柄(语义:返回旧区域) */
}

5.6 GdiSelectFont(SelectObject 的字体特例)

gdi32\objects\dc.cGdiSelectFont:先更新用户态 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 也能执行(这正是 NtGdiFlushUserBatchhDC || 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;
}

流程拆解:

  1. 计数合法性检查GdiBatchCount > 0<= GDIBATCHBUFSIZE/4(即 ≤ 310,缓冲最多能容纳的最小记录数,防御异常值)。
  2. DC 判定 :批 HDC 必须仍是一个合法 DC 句柄(GreIsHandleValid)才加锁;删除对象批(HDC=0)不锁 DC,pDC = NULL
  3. 解析循环 :从头开始逐条调用 GdiFlushUserBatch(pDC, pHdr),返回该记录的字节数 SizepHdr += Size 前进;返回 0(如 SEH 捕获到用户缓冲访问违例)则中断。
  4. 善后 :解锁 DC,并把 TEB 批状态复位(Offset=0GdiBatchCount=0HDC=0),为下一轮攒批做准备。
  5. 末尾 FIXME 提示:Windows XP 上此函数可能返回 &pTeb->RealClientId(而非 VOID),属于兼容性遗留问题。

注册方式ntuser\main.cInitMachineDependents/驱动初始化):

c 复制代码
CalloutData.BatchFlushRoutine = NtGdiFlushUserBatch;
...
PsEstablishWin32Callouts(&CalloutData);

ntoskrnl\ps\win32.c 将其保存为全局 KeGdiFlushUserBatch,随后在以下位置被调用(已核对源码):

  • ntoskrnl\ke\i386\traphdlr.cKiSystemService):进入 GUI 系统调用前,if (NtCurrentTeb()->GdiBatchCount) KeGdiFlushUserBatch();
  • ntoskrnl\ke\i386\usercall.cKiSystemServiceHandler 等):同样按 Teb->GdiBatchCount 条件调用;
  • ntoskrnl\ke\amd64\traphandler.camd64\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→ROP4MAKEROP4(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 实际装的是 nWidthr.bottom 装的是 nHeight------所以这里把 r.right/r.bottom 直接传给 IntPatBlt 的 Width/Height 参数是有意为之 (与 NtGdiPolyPatBltIntGdiPolyPatBlt(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_UPDATECPETO_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==0ETO_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 是内核内部裁剪区操作,按 fnModeRGN_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);
       }
     }
  }
}

要点:

  • 前置条件两条(注释即源码语义):
    1. 不 puntingPDEV_DRIVER_PUNTED_CALL 未置位------若驱动把调用"踢回"(punting)给引擎处理,则无需同步;
    2. 设备托管表面Surface->dhsurf 非零------dhsurf 是"设备管理表面时给引擎的句柄,否则为 0"。
  • 同步方式二选一:优先 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.cpdi->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 系统调用(包括 GetMessageGetDC、乃至下一次绘制)都会被动刷出当前线程的批。这意味着批的"滞留窗口"至多到线程下一次进入内核为止,批命令不会无限期悬挂。

9.3 与消息循环的关系

Windows 规范要求线程在取消息(GetMessage/PeekMessage)后,其之前的 GDI 绘制必须已经可见。ReactOS 的批机制通过两条路径满足该语义:

  1. NtUserGetMessage 本身是 win32k 系统调用,KiSystemService 在其入口处就已刷出批;
  2. 即便不经过消息循环,任何后续 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.cDoDeviceSync 的注释明确写着 "based on IntEngEnter from eng/engmisc.c"------即驱动同步逻辑取自 GDI 引擎的设备进入例程 IntEngEnter。二者处理的是同一类问题:GDI 引擎/批处理要访问设备表面时,必须保证设备侧状态就绪

IntEngEnterwin32ss\gdi\eng\engmisc.c,引擎进入例程)的职责(依据源码):

  • 归一化目标矩形(左右/上下颠倒时交换端点);
  • 若目标表面非位图表面(STYPE_BITMAP)且无 pvScan0/lDelta(设备管理表面),则要求驱动支持 DrvCopyBitsHOOK_COPYBITS),否则返回 FALSE;
  • 需要时 EngCreateBitmap 创建临时位图作为输出,把设备表面访问转换为软件位图访问,并记录还原所需的 DestRect 等现场。

DoDeviceSync 则简化聚焦到"同步"本身:

  • 跳过 punting 的驱动(PDEV_DRIVER_PUNTED_CALL);
  • 仅在设备托管表面(dhsurf 非零)时调用 DrvSynchronizeSurfaceDrvSynchronize
  • 不创建临时表面------同步只是"让设备把内部状态刷到位",不涉及表面内容的搬运。

两者互补: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) 批处理刷出核心:NtGdiFlushNtGdiFlushUserBatchGdiFlushUserBatch(9 命令分发)、DoDeviceSyncSynchronizeDriver
init.c(file:///d:/reactos/win32ss/gdi/ntgdi/init.c) GdiProcessCreatePeb->GdiDCAttributeList = GDI_BATCH_LIMIT(20)
ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h) GDIBATCHCMD 枚举、GDIBATCHBUFSIZEGDI_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) GdiFlushGdiSetBatchLimitGdiGetBatchLimit
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) IntPatBltNtGdiPatBltNtGdiPolyPatBlt/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 批处理是一条贯穿用户态与内核态的成熟优化路径:

  1. 攒批(用户态)GdiAllocBatchCommandPatBlt/PolyPatBlt/TextOut/ExtTextOut(空串不透明)/SetBrushOrg/ExtSelectClipRgn/SelectFont/DeleteRegion/DeleteObject 九类高频小操作连同 DC 属性快照写入线程 TEB 的 GdiTebBatch,条数上限 GDI_BATCH_LIMIT(20,源自 Peb->GdiDCAttributeList)、字节上限 GDIBATCHBUFSIZE(1240)。
  2. 刷出(内核调度层) :任意 win32k 系统调用入口、满批、显式 GdiFlush 都会触发 KeGdiFlushUserBatchNtGdiFlushUserBatch,后者锁定批 DC 后循环调用 GdiFlushUserBatch
  3. 执行(内核解析器)GdiFlushUserBatch 以 SEH 保护读取用户缓冲,按 GDIBATCHHDR.Cmd 分发------图案填充走 IntPatBlt、文本走 IntExtTextOutW、其余走 DC_*/Gre* 内部函数;属性采用"保存→快照覆盖→执行→恢复"模式保证语义等价。
  4. 同步(驱动层)NtGdiFlushSynchronizeDriver/DoDeviceSync 调用 DrvSynchronize(Surface) 完成刷出后的设备同步(当前 SynchronizeDriverEngLockSurface 未实现而为空操作,属于待完善项)。

该机制把"每操作一次系统调用"降为"每批一次",是 Windows 兼容图形栈性能的关键一环;同时其 TEB 布局与 Windows 一致,为二进制兼容的批处理行为提供了基础。


本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/gdibatch.c 及关联模块分析(2026 年 8 月)。文中所有函数签名、结构布局、标志位与调用关系均以源码为准。

相关推荐
qq21084629531 小时前
在python中什么是 self 和 cls?
开发语言·前端·python
杨充1 小时前
05.多用组合和少继承
开发语言·bash
傻啦嘿哟2 小时前
王者荣耀爬虫:爬取全英雄皮肤数据,用Python做可视化分析
开发语言·爬虫·python
AI直播技术杂谈2 小时前
直播画面突然模糊了,排查了三个小时
开发语言·php
菜冻鱼2 小时前
Python-pytorch-数据加载
开发语言·人工智能·pytorch·python·深度学习·机器学习
heimeiyingwang2 小时前
【架构实战】可观测性三支柱实战:Metrics、Logging、Tracing 如何统一落地
开发语言·架构·php
caimouse2 小时前
ReactOS 图形系统分析(53):系统库存对象 — stockobj.c
c语言·开发语言·spring
ZJU_统一阿萨姆3 小时前
【推理优化进阶】通信关键路径:NCCL、RDMA 与计算通信重叠
开发语言·人工智能·语言模型·架构·系统架构