ReactOS 图形系统分析(37):路径子系统 — path.c

ReactOS 图形系统分析(37):路径子系统 --- path.c

本文基于 ReactOS 源码 win32ss/gdi/ntgdi/path.c(2026 年 8 月,全文 3168 行)与

win32ss/gdi/ntgdi/path.h 逐函数分析。路径子系统是 GDI 中"先记录、后执行"思想的

典型实现:BeginPath/EndPath 之间的一切绘图调用不再直接画到表面,而是被折算成

"点 + 标志"数组累积到 PATH 对象中,EndPath 之后再统一交给填充/描边/宽化/区域化

等输出管线消费。


1. 概述

1.1 什么是"路径"(Path)

路径是 GDI 提供的一种**延迟渲染(deferred rendering)**机制。应用可以:

c 复制代码
BeginPath(hdc);                       // 进入路径记录模式
MoveToEx(hdc, 10, 10, NULL);
LineTo(hdc, 100, 50);                 // 直线
PolyBezier(hdc, pts, 9);              // 三次贝塞尔曲线
RoundRect(hdc, 0, 0, 80, 80, 10, 10); // 圆角矩形(贝塞尔近似)
EndPath(hdc);                         // 结束记录,路径进入 PATH_Closed 状态

StrokePath(hdc);      // 用当前画笔描边
FillPath(hdc);        // 用当前画刷填充
StrokeAndFillPath(hdc);
WidenPath(hdc);       // 把路径按笔宽"轮廓化"成新路径
FlattenPath(hdc);     // 把贝塞尔全部转成折线
HRGN r = PathToRegion(hdc);  // 路径转区域
GetPath(hdc, pts, types, size);  // 取出路径的点与类型

在记录期间,LineTo/PolylineTo/PolyBezierTo/Arc/Ellipse/RoundRect/Rectangle/

PolyDraw/PolyPolygon/PolyPolyline/TextOutExtTextOut)等都会把图元折算成

POINT + BYTE 标志对写入 PATH 的数组。当前画笔、画刷、变换等一律不参与记录

直到输出阶段才结合 DC 属性真正光栅化。

1.2 分层架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│ 应用层(GDI API)                                                    │
│   BeginPath / LineTo / PolyBezier / ExtTextOut / EndPath            │
│   StrokePath / FillPath / StrokeAndFillPath / WidenPath /           │
│   FlattenPath / PathToRegion / GetPath / CloseFigure / AbortPath    │
└───────────────────────────────┬─────────────────────────────────────┘
                                ↓ 用户态 gdi32.dll → win32u.dll
┌───────────────────────────────┴─────────────────────────────────────┐
│ win32ss/gdi/ntgdi/path.c(本分析对象)                               │
│   13 个 NtGdi* 系统调用入口(第 6 节逐一展开)                        │
│   ├─ 生命周期/管理  PATH_CreatePath / PATH_Delete / PATH_InitGdiPath │
│   │                 PATH_AssignGdiPath / PATH_SavePath /             │
│   │                 PATH_RestorePath / PATH_EmptyPath /              │
│   │                 PATH_DestroyGdiPath / IntGdiCloseFigure          │
│   ├─ 记录层        PATH_AddEntry / PATH_ReserveEntries(数组扩容)    │
│   ├─ 图元层        PATH_MoveTo / LineTo / Rectangle / RoundRect /    │
│   │                 Ellipse / Arc(+DoArcPart) / PolyBezier(To) /     │
│   │                 PolyDraw / PolylineTo / PolyPolygon /            │
│   │                 PolyPolyline / ExtTextOut(+add_outline)          │
│   ├─ 变换层        PATH_FlattenPath(+AddFlatBezier) 贝塞尔→折线      │
│   │                 IntGdiWidenPath / PATH_WidenPath(Ex) 宽化轮廓    │
│   └─ 输出层        PATH_PathToRegion / PATH_FillPath(Ex) /           │
│                     PATH_StrokePath                                   │
└───────────────┬───────────────────────────────┬──────────────────────┘
                ↓                                ↓
   细笔描边: IntGdiPolyline → IntEngPolyline/   宽笔/填充: PATH_WidenPathEx →
   IntEngLineTo(line.c,《分析_5》)           PATH_FillPathEx → IntGdiFillRgn
                                               (bitblt.c,《分析_17》)

1.3 双表示关系(与《分析_18》衔接)

PATH 结构里同时存在两套坐标表示

  1. Wine/ReactOS 风格pPointsPOINT 数组,设备坐标)+ pFlagsBYTE
    标志数组,PT_MOVETO/PT_LINETO/PT_BEZIERTO/PT_CLOSEFIGURE)。path.c 的全部
    记录、图元、输出逻辑都建立在这套数组之上。
  2. DDI 风格ppdFirst/ppdLast/ppdCurrent 串起的 EXTPATHDATA 链表(内含
    PATHDATA,定点数 POINTFIX 坐标,PD_* 标志),由 eng/pathobj.c
    PATHOBJ_* 服务维护。正如《分析_18》所指出,这套链表只是为 DDI 契约预留
    全树没有任何驱动实际调用,路径的真正消费完全走第一套表示。

1.4 设计动机

  • 解耦"几何构造"与"光栅化" :贝塞尔曲线的折线化、宽笔的轮廓化(endcap/join)
    等昂贵计算只在输出阶段执行一次;FillPathStrokePath 可复用同一份几何。
  • 字体轮廓复用BeginPathExtTextOut 通过 FreeType 的 GGO_NATIVE
    直接取字形原生轮廓折成路径(PATH_add_outline),使文字可作为几何参与
    Stroke/Fill/区域裁剪------这是"把文字变成路径"的唯一内核途径。
  • 区域化 :路径可以折成多边形区域(PATH_PathToRegionREGION_SetPolyPolygonRgn),
    从而获得区域的所有能力(命中测试、裁剪、CombineRgn 等)。
  • 统一坐标基准 :记录阶段就把逻辑坐标转成设备坐标 存入路径(IntLPtoDP),
    输出阶段再临时把 DC 置为 MM_TEXT + 单位世界变换,避免二次变换失真。

2. 核心数据结构(path.h(file:///d:/reactos/win32ss/gdi/ntgdi/path.h))

2.1 DCPATH_* 标志(enum _DCPATHFLAGS

定义在 path.h 头部,存放在 DC.dclevel.flPath 中,描述路径与 DC 的关系

c 复制代码
enum _DCPATHFLAGS
{
    DCPATH_ACTIVE    = 0x0001,   /* DC 正处于 BeginPath..EndPath 记录区间  */
    DCPATH_SAVE      = 0x0002,   /* 当前路径是被 SaveDC 保存的副本          */
    DCPATH_CLOCKWISE = 0x0004,   /* 多边形类图元按顺时针生成(Arc 缺省方向)*/
    /* ReactOS only */
    DCPATH_SAVESTATE = 0x80000000 /* SaveDC 保存的状态标志(ReactOS 私有)  */
};
标志 作用点 说明
DCPATH_ACTIVE NtGdiBeginPath 置位、NtGdiEndPath/AbortPath 清位 配合 PATH_IsPathOpen 判定"DC 正在记录路径";图元 API(line.c/arc.c/fillshap.c/freetype.c)据此转向 PATH_* 记录函数
DCPATH_SAVE SaveDC 期间 BeginPath 若遇 SAVE 状态不清除旧路径而新建副本;RestorePath 用它决定重建还是覆盖
DCPATH_CLOCKWISE NtGdiSetArcDirection PATH_Rectangle/RoundRect/Ellipse/Arc 的缺省绕行方向(点序反向用 reverse_points

配套宏:

c 复制代码
#define PATH_IsPathOpen(dclevel) ( ((dclevel).hPath) && ((dclevel).flPath & DCPATH_ACTIVE) )

2.2 路径状态机 GdiPathState

c 复制代码
typedef enum tagGdiPathState
{
   PATH_Null,    /* 空路径(无内容或刚初始化) */
   PATH_Open,    /* BeginPath 之后、EndPath 之前:可继续累积图元 */
   PATH_Closed   /* EndPath 之后:只读,可被 Stroke/Fill/Widen/GetPath 消费 */
} GdiPathState;

状态流转(第 3、6 节有详述):

复制代码
PATH_Null ──PATH_CreatePath──▶ PATH_Open ──NtGdiEndPath──▶ PATH_Closed
    ▲                            │                            │
    └──────────PATH_EmptyPath────┘                            ├─StrokePath/FillPath/...
    (NtGdiBeginPath 复用/重建)                                └─消费后 PATH_Delete/重置

要点:PATH_Closed 后所有 PATH_* 记录函数(如 PATH_ExtTextOut

state != PATH_Open 检查)会拒绝写入;WidenPathEx/GetPath/PathToRegion

要求必须是 PATH_Closed ,否则返回 ERROR_CAN_NOT_COMPLETE

2.3 PATHTYPE_*(历史遗留标志)

c 复制代码
// Path type flags
#define PATHTYPE_KEEPME 1
#define PATHTYPE_STACK  2

这两个宏在 path.c/path.h 中仅定义、从未被使用 ,属于从 Wine 早期版本继承的

"路径类型"占位标志,保留以兼容外部引用。

2.4 PATH 结构全字段(path.h L34-L59)

c 复制代码
typedef struct _PATH
{
  BASEOBJECT   BaseObject;        /* GDI 对象头(含句柄 hHmgr、类型 GDI_OBJECT_TYPE_PATH)*/

  /* ---- DDI/PATHOBJ 表示(《分析_18》:eng/pathobj.c 维护,预留未用) ---- */
  //PVOID        ppachain;         /* 已注释:旧版子路径链 */
  RECTFX       rcfxBoundBox;      /* 路径包围盒(28.4 定点数)*/
  POINTFX      ptfxSubPathStart;  /* 当前子路径起点(定点数)*/
  FLONG        flType;            /* 路径类型标志 */
  PEXTPATHDATA ppdFirst;          /* EXTPATHDATA 链表头 */
  PEXTPATHDATA ppdLast;           /* EXTPATHDATA 链表尾 */
  FLONG        flags;             /* 当前 PATHDATA 段标志 */
  PEXTPATHDATA ppdCurrent;        /* 枚举游标(PATHOBJ_bEnum 用)*/
  FLONG        fl;                /* 保存的 PATHOBJ 标志(PO_*)*/
  ULONG        cCurves;           /* 保存的子路径/曲线计数 */

  struct _EPATHOBJ *epo;          /* 反向指针:EPATHOBJ(含 PATHOBJ 头 + CLIPOBJ)*/

  /* ---- Wine/ReactOS 表示:path.c 真正使用的记录数组 ---- */
  FLONG        state;             /* GdiPathState:PATH_Null/Open/Closed */
  POINT        *pPoints;          /* 点数组(设备坐标),TAG_PATH 分页池 */
  BYTE         *pFlags;           /* 标志数组:PT_MOVETO/PT_LINETO/PT_BEZIERTO(|PT_CLOSEFIGURE) */
  int          numEntriesUsed;    /* 已用条目数 */
  int          numEntriesAllocated;/* 已分配容量(条目数)*/
  BOOL         newStroke;         /* 下一个图元是否应开启新子路径(true 时先写 PT_MOVETO)*/
  POINT        pos;               /* 路径内"当前点"(设备坐标,stroke 起点/续接依据)*/
} PATH, *PPATH;

配套句柄宏(path.h L68-L72):

c 复制代码
#define PATH_AllocPath()          ((PPATH) GDIOBJ_AllocObj(GDIObjType_PATH_TYPE))
#define PATH_AllocPathWithHandle()((PPATH) GDIOBJ_AllocObjWithHandle(GDI_OBJECT_TYPE_PATH, sizeof(PATH)))
#define PATH_LockPath(hPath)      ((PPATH)GDIOBJ_ShareLockObj((HGDIOBJ)hPath, GDI_OBJECT_TYPE_PATH))
#define PATH_UnlockPath(pPath)    (GDIOBJ_vDereferenceObject((POBJ)pPath))
  • 路径句柄是共享句柄 :同一线程内递归调用(如 IntGdiPolyline 宽笔分支内部
    再建路径)也允许重复锁定,故 PATH_CreatePath 使用 ShareLock。
  • 加锁/解锁必须成对:几乎所有 PATH_* 函数都以 PATH_LockPath(dc->dclevel.hPath)
    开始、PATH_UnlockPath 结束;PATH_LockPath 失败(句柄非法)返回 NULL 并导致
    函数直接返回 FALSE。

2.5 EXTPATHDATA 与 eng/pathobj.c 的双表示关系

c 复制代码
/* extended PATHDATA */
typedef struct _EXTPATHDATA
{
    PATHDATA pd;             /* 公开 DDI 单元:flags + count + pptfx(POINTFIX*) */
    struct _EXTPATHDATA *ppdNext;  /* 链表后继 */
} EXTPATHDATA, *PEXTPATHDATA;

其中 PATHDATA(winddi.h)为:

c 复制代码
typedef struct _PATHDATA {
  FLONG     flags;   /* PD_BEGINSUBPATH/PD_ENDSUBPATH/PD_CLOSEFIGURE/PD_BEZIERS */
  ULONG     count;   /* 本段点数 */
  POINTFIX *pptfx;   /* 定点数坐标(1/64 像素)*/
} PATHDATA, *PPATHDATA;

eng/pathobj.cPATHOBJ_bMoveTo/bPolyLineTo/bPolyBezierTo/bCloseFigure/ vEnumStart/bEnum/vGetBounds 全部透过 EPATHOBJ.pPath 操作这套链表:追加

EXTPATHDATA 节点、合并同类型连续段、用 ppdCurrent 做枚举游标、把边界盒写入

rcfxBoundBox。但正如《分析_18》的结论:没有任何驱动调用这些 DDI 服务

EngStrokePath/EngFillPatheng/stubs.c 中仍是桩;path.c 的双表示只是

"结构与 DDI 兼容",实际数据流完全走 pPoints/pFlags 数组。

2.6 常量与调试设施

c 复制代码
#define NUM_ENTRIES_INITIAL 16   /* pPoints/pFlags 初始容量(PATH_CreatePath 缺省)*/
#define GROW_FACTOR_NUMER    2   /* 扩容因子分子 */
#define GROW_FACTOR_DENOM    1   /* 扩容因子分母(即 ×2 指数增长,保证摊还 O(1))*/
DBG_DEFAULT_CHANNEL(GdiPath);    /* 调试通道:GdiPath(TRACE/ERR 前缀 trace:/err:)*/
#ifdef DBG
static int PathCount = 0;        /* 调试用:现存路径计数(Create++/Delete--)*/
#endif

内存标签一律 TAG_PATHExAllocatePoolWithTag(PagedPool, ...)),贝塞尔折线化

临时点用 TAG_BEZIER(bezier.c 分配)。

PT_ 记录标志 *(wingdi.h):pFlags 数组每个字节取值如下,其中

PT_CLOSEFIGURE 是可与前三种 OR 的组合位:

标志 含义
PT_CLOSEFIGURE 0x01 本记录(通常为最后一点)闭合当前子路径
PT_LINETO 0x02 直线段端点(起点为上一记录)
PT_BEZIERTO 0x04 三次贝塞尔控制点(每 3 条一组,曲线从上一记录到第 3 条)
PT_MOVETO 0x06 子路径起点(新笔画)

典型组合:PT_LINETO | PT_CLOSEFIGURE(0x03,多边形的末点)、

PT_BEZIERTO | PT_CLOSEFIGURE(0x05,闭合贝塞尔段末点,如椭圆)。

路径合法性的不变量(被消费方 ASSERT/校验):

  1. 首条记录必须是 PT_MOVETOPATH_PathToRegionASSERT(pFlags[0]==PT_MOVETO)
    PATH_StrokePath 对 i==0 的检查);
  2. 每条子路径以 PT_MOVETO 开头,其后紧跟 LINETO 或 3 条一组的 BEZIERTO;
  3. PT_CLOSEFIGURE 之后必须紧跟 PT_MOVETO(PATH_StrokePath/IntGdiWidenPath
    都会校验"上一记录 CLOSEFIGURE 后本记录必须 MOVETO");
  4. 贝塞尔段必须连续 3 条 PT_BEZIERTO(PATH_StrokePath 校验
    pFlags[i+1]==PT_BEZIERTO && (pFlags[i+2]&~PT_CLOSEFIGURE)==PT_BEZIERTO)。

3. 生命周期与管理函数

3.1 PATH_CreatePath(int count) --- 创建并初始化路径

c 复制代码
PPATH FASTCALL PATH_CreatePath(int count);

作用 :分配一个带句柄的 PATH 对象,初始容量为 max(16, count),并预分配

pPointscount*sizeof(POINT))与 pFlagscount*sizeof(BYTE))两块分页池。

实现流程

  1. PATH_AllocPathWithHandle()GDIOBJ_AllocObjWithHandle(GDI_OBJECT_TYPE_PATH, sizeof(PATH)))分配对象;失败则 EngSetLastError(ERROR_NOT_ENOUGH_MEMORY) 返回 NULL。
  2. GDIOBJ_vUnlockObject 解锁后改用 PATH_LockPath(pPath->BaseObject.hHmgr)
    共享锁 ------注释说明"Path handles are shared. Also due to recursion with in
    the same thread"(句柄共享、允许同线程递归加锁)。
  3. PATH_EmptyPath 确保对象初始为空。
  4. count = max(NUM_ENTRIES_INITIAL, count)numEntriesAllocated = count
    两块数组 RtlZeroMemory 清零。
  5. 初始化 numEntriesUsed = 0newStroke = TRUE(第一条图元需 MOVETO)、
    state = PATH_Openpos = (0,0)
  6. 调试构建下 PathCount++

使用方式NtGdiBeginPathPATH_SavePathPATH_FlattenPath

IntGdiWidenPathIntGdiPolyline(宽笔临时路径)等所有需要新路径的地方。

注意事项 :返回的路径处于 PATH_Open已加共享锁 ,调用方用完必须

PATH_UnlockPath;若不再需要则用 PATH_Delete(hPath)

3.2 PATH_DestroyGdiPath(PPATH pPath) --- 释放记录数组

c 复制代码
VOID FASTCALL PATH_DestroyGdiPath(PPATH pPath);

作用ExFreePoolWithTag 释放 pPointspFlags 两块数组(各判 NULL)。

不释放 PATH 结构本身、不删除 GDI 对象、不改状态------它只是"清内存"。

使用方式PATH_Delete 删除对象前调用;IntGdiWidenPath 在丢弃临时 stroke/

上下路径时逐个调用。

3.3 PATH_Delete(HPATH hPath) --- 按句柄删除路径

c 复制代码
BOOL FASTCALL PATH_Delete(HPATH hPath);

流程hPath 为 NULL 返回 FALSE → PATH_LockPath 加锁(失败返回 FALSE)→

PATH_DestroyGdiPath 释放数组 → GDIOBJ_vDeleteObject(&pPath->BaseObject) 删除

GDI 对象 → 调试 PathCount-- → 返回 TRUE。

使用方式NtGdiAbortPathNtGdiEndPath 系列输出后清理、NtGdiWidenPath

换路径、PATH_FlattenPath 失败清理等。注意调用前通常需要先 PATH_UnlockPath

或者依赖 GDIOBJ_vDeleteObject 内部对引用计数的处理(源码中两种用法都有:

PATH_FlattenPath 失败分支先 UnlockPathDeleteNtGdiFillPath 则先

UnlockPathDelete)。

3.4 PATH_InitGdiPath(PPATH pPath) --- 初始化(置空,不分配)

c 复制代码
VOID FASTCALL PATH_InitGdiPath(PPATH pPath);

state = PATH_NullpPoints = NULLpFlags = NULL

numEntriesUsed = numEntriesAllocated = 0。用于把一块裸内存变成合法的空 PATH

IntGdiWidenPath 中手工 ExAllocatePool(sizeof(PATH)) 的临时 stroke 路径就是

先 Init 再填充)。

PATH_EmptyPath 的区别 :Init 用于"从未使用过的对象"(指针置 NULL);

EmptyPath 用于"已分配过数组的对象"(保留内存、仅清逻辑内容)。

3.5 PATH_AssignGdiPath(PPATH pPathDest, const PPATH pPathSrc) --- 深拷贝

c 复制代码
BOOL FASTCALL PATH_AssignGdiPath(PPATH pPathDest, const PPATH pPathSrc);

作用 :把 src 的记录数组逐字节拷贝 到 dest(深拷贝,不是指针共享),并复制

pos/state/numEntriesUsed/newStroke。注释强调:dest 应先用 PATH_InitGdiPath

初始化过(语义上相当于 C++ 的"赋值运算符"而非"拷贝构造")。

流程PATH_ReserveEntries(dest, src->numEntriesUsed) 确保容量(失败返回

FALSE)→ memcpy 两块数组 → 复制四个标量字段 → TRUE。

使用方式PATH_SavePath/PATH_RestorePath 的核心;SaveDC 对路径做快照。

3.6 PATH_SavePath(DC *dst, DC *src) --- SaveDC 时保存路径

c 复制代码
BOOL PATH_SavePath(DC *, DC *);

流程 :锁 src 的路径 → PATH_CreatePath(src->numEntriesAllocated) 按原容量新建

dst->dclevel.flPath = src->dclevel.flPath(连同 DCPATH_ACTIVE/SAVE/CLOCKWISE

一起复制)→ dst->dclevel.hPath = 新路径句柄PATH_AssignGdiPath 深拷贝 →

解锁两者。

说明 :SaveDC 路径快照是整对象复制 (含记录数组内容),这使

BeginPath...EndPath 区间内 SaveDC/RestoreDC 也能正确还原路径状态。调用方

(dcobjs.c 的 NtGdiSaveDC)在保存前会给 flPathDCPATH_SAVE 标志。

3.7 PATH_RestorePath(DC *dst, DC *src) --- RestoreDC 时恢复路径

c 复制代码
BOOL PATH_RestorePath(DC *, DC *);

流程(按 dst 当前是否有路径分两支):

  1. dst->dclevel.hPath == NULL:与 SavePath 相同------新建并深拷贝 src 路径,
    flPath 整体复制。
  2. 已有路径:锁两边的路径,dst->dclevel.flPath = src->dclevel.flPath & (DCPATH_CLOCKWISE|DCPATH_ACTIVE)只回拷方向与激活位 ,不清 SAVE 位),
    PATH_AssignGdiPath 覆盖内容。

注意事项:恢复后旧路径对象不复存在(被覆盖),RestoreDC 层负责最终释放。

3.8 PATH_EmptyPath(PPATH pPath) --- 清空(保留容量)

c 复制代码
VOID FASTCALL PATH_EmptyPath(PPATH pPath);

state = PATH_Null; numEntriesUsed = 0;不释放、不重分配数组 ,下次写入复用

现有容量。PATH_CreatePath 用它确保新对象为空。

3.9 PATH_AddEntry(PPATH pPath, const POINT *pPoint, BYTE flags) --- 追加一条记录

c 复制代码
BOOL FASTCALL PATH_AddEntry (PPATH pPath, const POINT *pPoint, BYTE flags);

作用 :路径记录层的唯一写入原语:追加一个 (点, 标志) 对。

流程PATH_ReserveEntries(pPath, numEntriesUsed + 1) 保证容量(失败 FALSE)→

pPoints[numEntriesUsed] = *pPoint; pFlags[numEntriesUsed] = flags;

numEntriesUsed++ → TRUE。

flags 合法值PT_MOVETO(6)/PT_LINETO(2)/PT_BEZIERTO(4),可 OR

PT_CLOSEFIGURE(1)。源码中带 FIXME:newStroke 为 true 时应当校验传入的是

PT_MOVETO(当前未做校验)。

使用方式 :所有图元函数(PATH_LineToPATH_RectanglePATH_Arc

PATH_PolyPolylinePATH_AddFlatBezierPATH_add_outline 等)以及

IntGdiWidenPath 的轮廓生成都调用它。

3.10 PATH_ReserveEntries(PPATH pPath, INT numEntries) --- 数组扩容

c 复制代码
BOOL FASTCALL PATH_ReserveEntries (PPATH pPath, INT numEntries);

作用 :确保 pPoints/pFlags 至少可容纳 numEntries 条记录,必要时扩容并迁移。

流程

  1. numEntries > numEntriesAllocated 才动作。
  2. 已有容量则按 ×2/1 指数增长 直到不小于 numEntries(注释:指数增长保证
    总复杂度线性,摊还 O(1)/次);首分配(容量 0)则直接分配恰好 numEntries。
  3. 先分配新 pPoints(失败返回 FALSE),再分配新 pFlags(失败则回滚释放
    pPoints 并返回 FALSE)------两段分配防止半途泄漏。
  4. 若有旧数组:memcpy 已用部分到新数组,ExFreePoolWithTag 旧数组。
  5. 更新指针与 numEntriesAllocated

注意 :调用者(如 add_log_points)在 Reserve 之后直接向
pFlags[numEntriesUsed] 批量写标志
并一次性推进 numEntriesUsed,因此本函数

返回后数组内容未初始化区域必须被立即填充。

3.11 PATH_ScaleNormalizedPoint(POINT corners[], FLOATL x, FLOATL y, POINT *pPoint)(static)

把归一化坐标 (x,y) ∈ [-1,1]×[-1,1] 映射到以 corners[0](左上)、corners[1]

(右下)为对角顶点的框内:

复制代码
pPoint->x = corners[0].x + (corners[1].x - corners[0].x) * 0.5 * (x + 1.0);
pPoint->y = corners[0].y + (corners[1].y - corners[0].y) * 0.5 * (y + 1.0);

全程用 FLOATOBJ 做定点/浮点混合运算(gef1/gef2 为预置常量 1.0/2.0),最后

FLOATOBJ_bConvertToLong 取整;转换溢出返回 FALSE (调用方 PATH_DoArcPart

据此失败退出)。

用途:圆弧/端帽的贝塞尔控制点(单位圆上的三角函数值)投影到椭圆框内。

3.12 PATH_NormalizePoint(POINTL corners[], const POINTL *pPoint, FLOATL *pX, FLOATL *pY)(static)

ScaleNormalizedPoint逆映射(框内点 → 归一化 -1,1):

复制代码
*pX = (pPoint->x - corners[0].x) * 2.0 / (corners[1].x - corners[0].x) - 1.0;
*pY = (pPoint->y - corners[0].y) * 2.0 / (corners[1].y - corners[0].y) - 1.0;

用途PATH_Arc 计算起点/终点相对于包围框的极角(atan2(y,x))。

3.13 PATH_CheckRect(DC *dc, RECTL* rect, INT x1, INT y1, INT x2, INT y2)(static)

PATH_Rectangle/PATH_RoundRect/PATH_Ellipse 的公共前置处理:

  1. RECTL_vSetRect(rect, x1, y1, x2, y2)
  2. IntLPtoDP(dc, (PPOINT)rect, 2) 把两对角点转设备坐标;
  3. RECTL_vMakeWellOrdered(rect) 保证 left≤right、top≤bottom;
  4. GM_COMPATIBLE 语义 :若 left == right || top == bottom(退化矩形)返回
    FALSE;否则 rect->right--; rect->bottom--(兼容模式下矩形不包含右/下边缘,
    与 GDI 的 Rectangle 光栅化规则一致)。

返回值约定 :FALSE 表示"无需记录"------注意调用方对 FALSE 的处理不一

PATH_Rectangle/RoundRect 返回 TRUE(静默成功),PATH_Ellipse 也返回 TRUE,

只有路径锁失败才返回 FALSE。

3.14 IntGdiCloseFigure(PPATH pPath) --- 标记图元闭合

c 复制代码
VOID FASTCALL IntGdiCloseFigure(PPATH pPath);

作用 :把路径当前子路径 标记为闭合------给末条记录的标志补上

PT_CLOSEFIGURE 位,并把 newStroke 置 TRUE(下一条图元将开启新子路径)。

流程(源码 L106-L119):

c 复制代码
ASSERT(pPath->state == PATH_Open);          // 仅记录期有效
// FIXME: Shouldn't we draw a line to the beginning of the figure?
if (pPath->numEntriesUsed)
{
    pPath->pFlags[pPath->numEntriesUsed - 1] |= PT_CLOSEFIGURE;
    pPath->newStroke = TRUE;
}

关键语义

  • 不真正补线 :源码自带 FIXME("Shouldn't we draw a line to the beginning of
    the figure?")------闭合线段并不写入路径,而是由消费方隐式处理:
    PATH_StrokePath 遇 CLOSEFIGURE 回填 pLinePts[0](起点);FillPath 的区域
    构造与 IntGdiWidenPath 的闭合 stroke 判定也都依赖该标志而非实际线段。
  • newStroke = TRUE 使 CloseFigure 之后的 LineTo/PolyBezierTo 自动以
    start_new_stroke 补写 PT_MOVETO 开启新子路径。
  • numEntriesUsed == 0(空路径)时静默跳过。

使用方式PATH_Rectangle/RoundRect/Ellipse 构造完闭合图形后调用;

PATH_Arc 的 chord 分支调用;PATH_PolyPolygon

PT_LINETO | PT_CLOSEFIGURE 直接写标志等价实现;PATH_add_outline 每个字形

轮廓结束后调用;NtGdiCloseFigure 是对它的唯一系统调用包装。

3.15 GdiPathDPtoLP(PDC pdc, PPOINT ppt, INT count) --- 路径点回逻辑坐标

c 复制代码
BOOL FASTCALL GdiPathDPtoLP(PDC pdc, PPOINT ppt, INT count);

XFORMOBJmxDeviceToWorld 矩阵)做 XF_LTOL(设备→逻辑)批量变换,

NtGdiGetPath 把设备坐标点转回逻辑坐标返回给应用。注释引用 MSDN:

设备坐标超过 27 位、或转换后逻辑坐标超过 32 位时调用失败 (对应

NtGdiGetPathERROR_ARITHMETIC_OVERFLOW)。


4. 图元累积层(BeginPath 期间)

4.0 内部辅助函数(batch 写入与笔画管理)

这些 static 函数是图元层的"积木",全部围绕"点+标志"数组操作:

c 复制代码
/* 追加 count 个逻辑点(已转设备坐标后写入),返回首个标志字节指针以便改写 */
static BYTE *add_log_points(DC *dc, PPATH path, const POINT *points,
                            DWORD count, BYTE type, BOOL bExtraPt);
static BYTE *add_points(PPATH path, const POINT *points, DWORD count, BYTE type);
static void reverse_points(POINT *points, UINT count);          /* 反序(顺时针) */
static BOOL start_new_stroke(PPATH path);                       /* 需要时写 PT_MOVETO */
static void update_current_pos(PPATH path);                     /* pos = 最后一点 */
static void close_figure(PPATH path);                           /* 末点 |= PT_CLOSEFIGURE */
static BOOL add_log_points_new_stroke(DC *dc, PPATH path, const POINT *points,
                                      DWORD count, BYTE type, BOOL bExtraPt);
  • add_log_pointsPATH_ReserveEntries(numEntriesUsed+count) 后:
    • bExtraPt == TRUE && numEntriesUsed == 1(刚由 start_new_stroke 写入唯一的
      PT_MOVETO)时,把输入缓冲的 count+1 个点 整体拷入 pPoints[0..count]
      一次性 IntLPtoDP------首点直接充当新笔画的 MOVETO 位置(合并/覆盖 MoveTo
      点,供 PolyBezierTo/PolylineTo 复用输入缓冲)。
    • 否则在 pPoints[numEntriesUsed] 处拷入 count 个点并转换。
    • memset(ret, type, count) 批量写标志(ret 指向首个新标志),推进
      numEntriesUsed += count,返回 ret(调用方据此把 type0 改成 PT_MOVETO 等)。
  • add_points:同前者但不做坐标变换(点已是设备坐标)。
  • start_new_stroke :若 !newStroke && 已有记录 && 末条非 CLOSEFIGURE && 末点 == path->pos 则直接返回 TRUE(延续当前笔画);否则置 newStroke=FALSE
    add_points(&path->pos, 1, PT_MOVETO)------每条子路径都以 PT_MOVETO 开头
  • update_current_pospath->pos = pPoints[numEntriesUsed-1]
  • close_figure :末条标志 |= PT_CLOSEFIGURE
  • add_log_points_new_stroke :串联 start_new_strokeadd_log_points
    update_current_posIntGdiMoveToEx(dc, pos.x, pos.y, NULL)(把 DC 的当前点
    同步到路径终点,保证后续 LineTo 语义连续)。几乎所有"To"系列图元(LineTo、
    PolylineTo、PolyBezierTo)都走它。

4.1 PATH_MoveTo(PDC dc, PPATH pPath) --- 登记新笔画起点

c 复制代码
BOOL FASTCALL PATH_MoveTo(PDC dc, PPATH pPath);

语义MoveToEx 作用在开路径 DC 上时,DC 层的当前位置已被移动(DIRTY_ STYLESTATE 脏位置位);本函数只是把新的当前位置登记为下一条 stroke 的起点

流程 :若 dc->pdcattr->ulDirty_ & DIRTY_STYLESTATEnewStroke = TRUE

IntGetCurrentPositionEx(dc, &pPath->pos) 取 DC 当前点并 IntLPtoDP 转设备坐标;

返回 TRUE。否则返回 FALSE(无需处理)。

使用方式PATH_LineTo 开头先调它("Coalesce a MoveTo point");IntGdiPolyline

宽笔分支构造临时路径时也调用。注意 :它只改 newStroke 与 pos,不写任何路径
记录
------真正的 PT_MOVETO 由随后 start_new_stroke/PATH_LineTo 写入。

4.2 PATH_LineTo(PDC dc, INT x, INT y) --- 记录直线

c 复制代码
BOOL FASTCALL PATH_LineTo (PDC dc, INT x, INT y);

流程 :锁路径 → point = (x, y) → 先试 PATH_MoveTo(若 DC 有 MoveTo 脏位则

直接完成登记)→ 否则若 pPath->newStrokenewStroke = FALSE,取 DC 当前点

IntGetCurrentPositionEx + CoordLPtoDPPATH_AddEntry(PT_MOVETO) 写子路径

起点 → add_log_points_new_stroke(dc, pPath, &point, 1, PT_LINETO, FALSE) 追加

一条 PT_LINETO 并推进当前点 → 解锁。

路径结果示例(MoveTo(10,10) → LineTo(20,30) → LineTo(40,50)):

复制代码
i:  0          1          2
pt: (10,10)    (20,30)    (40,50)
fl: PT_MOVETO  PT_LINETO  PT_LINETO

4.3 PATH_Rectangle(PDC dc, INT x1, INT y1, INT x2, INT y2) --- 矩形

流程 :锁路径 → PATH_CheckRect(退化/兼容模式剔除;失败返回 TRUE)→ 生成

4 个角点,顺序为 (right,top) (left,top) (left,bottom) (right,bottom)

DCPATH_CLOCKWISEreverse_points(points, 4)add_points(..., 4, PT_LINETO)type[0] = PT_MOVETO(首点改 MOVETO)→ IntGdiCloseFigure 闭合 →

解锁。

路径结果[MOVETO (r,t), LINETO (l,t), LINETO (l,b), LINETO (r,b) | CLOSEFIGURE],共 4 条记录,整图闭合。

4.4 PATH_RoundRect(DC *dc, INT x1, INT y1, INT x2, INT y2, INT ell_width, INT ell_height) --- 圆角矩形

流程

  1. ell_width == 0 || ell_height == 0 → 退化为 PATH_Rectangle
  2. 锁路径、PATH_CheckRect
  3. (0,0,ell_width,ell_height) 转设备坐标并 well-ordered,宽高夹取到矩形
    尺寸以内
    min)。
  4. 计算圆角偏移:xOffset = EngMulDiv(ell_width, 44771525, 200000000),即
    w * (1 − 0.5522847) / 2(0.5522847 为三次贝塞尔逼近 1/4 圆弧的经典常量
    k = 4/3·(√2−1));yOffset 同理。半宽高用 EngMulDiv(w, 1, 2)四舍五入
    的整数(注释特意强调不能直接整数除法)。
  5. 生成 16 个点 (注释引用 stackoverflow 的圆角矩形贝塞尔构造法):
    • points[0]:起点 (right, top+ell_height)
    • [1..3]:右上角贝塞尔 3 控制点;
    • [4]:上边直线端点 (left+ell_width, top)
    • [5..7]:左上角贝塞尔;[8]:左边直线端点;
    • [9..11]:左下角贝塞尔;[12]:下边直线端点;
    • [13..15]:右下角贝塞尔(终点 (right, bottom-ell_height),闭合回起点)。
  6. 顺时针则 reverse_points(16)add_points(..., 16, PT_BEZIERTO) 后修正标志:
    type[0]=PT_MOVETO; type[4]=type[8]=type[12]=PT_LINETO(四条直边),其余保持
    PT_BEZIERTO(四角每 3 点一组);IntGdiCloseFigure

路径结果MOVETO, (3×BEZIERTO, LINETO) × 4, CLOSEFIGURE,共 16 条。

4.5 PATH_Ellipse(PDC dc, INT x1, INT y1, INT x2, INT y2) --- 椭圆(4 段贝塞尔)

流程PATH_CheckRect(失败返回 TRUE)→ xRadius/yRadius = 半宽/半高

取 4 个切点 left/top/right/bottom(各边中点)→ 控制点偏移

xOffset = EngMulDiv(宽, 55428475, 200000000),即 w * 0.55428475 / 2

(= w·k/2,k=0.5522847)→ 构造 13 个点

复制代码
points[0]  = right                     (起点:最右点)
points[1]  = right 上移 yOffset         (第 1 段控制点1)
points[2]  = top   右移 xOffset         (第 1 段控制点2)
points[3]  = top                       (第 1 段终点 = 第 2 段起点)
points[4]  = top   左移 xOffset
points[5]  = left  上移 yOffset
points[6]  = left
points[7]  = left  下移 yOffset
points[8]  = bottom 左移 xOffset
points[9]  = bottom
points[10] = bottom 右移 xOffset
points[11] = right  下移 yOffset
points[12] = right                     (回到起点,闭合)

顺时针则反序;add_points(..., 13, PT_BEZIERTO)type[0]=PT_MOVETO

IntGdiCloseFigure共 13 条记录 = 4 段三次贝塞尔(首尾闭合点重复)。

4.6 PATH_DoArcPart(PPATH pPath, POINT corners[], double angleStart, double angleEnd, BYTE startEntryType)(static)

作用:把**不超过 90°(M_PI_2)**的一段圆弧用一段三次贝塞尔近似并追加到路径。

算法(经典圆弧贝塞尔公式):

复制代码
halfAngle = (angleEnd - angleStart) / 2;
a = 4/3 · (1 − cos(halfAngle)) / sin(halfAngle);   /* 控制点张量 */
p0 = (cos s, sin s);            p3 = (cos e, sin e);
p1 = p0 − a·(−sin s, cos s) 的 y/x 叉乘形式:
     xNorm[1] = xNorm[0] − a·yNorm[0];  yNorm[1] = yNorm[0] + a·xNorm[0];
     xNorm[2] = xNorm[3] + a·yNorm[3];  yNorm[2] = yNorm[3] − a·xNorm[3];
  • 角度差极小(|halfAngle| ≤ 1e-8)时 4 个控制点退化为同一点(避免除零/抖动)。
  • 4 个归一化控制点经 PATH_ScaleNormalizedPoint 映射进 corners 框(椭圆框)。
  • startEntryType 非零时首条记录用该类型(如 PT_MOVETO/PT_LINETO),否则假定
    当前点即弧起点(start = !startEntryType 跳过第 0 点)。
  • add_points(pPath, points+start, 4−start, PT_BEZIERTO) 批量写入。

使用方式PATH_Arc 每个象限段调用一次;IntGdiWidenPath 的 ROUND 端帽用

4 段 45° 弧拼出 180° 半圆(复用同一函数)。

4.7 PATH_Arc(...) --- 弧 / 弦 / 扇形 / 弧到

c 复制代码
BOOL FASTCALL PATH_Arc(PDC dc, INT x1, INT y1, INT x2, INT y2,
                       INT xStart, INT yStart, INT xEnd, INT yEnd,
                       INT direction, INT lines);

参数(x1,y1)-(x2,y2) 为外接矩形;(xStart,yStart)/(xEnd,yEnd) 为起止

径点;directionAD_CLOCKWISE/AD_COUNTERCLOCKWISE(0 时取

DCPATH_CLOCKWISE);lines 决定收尾方式:

lines 语义 收尾
0 Arc(纯弧)
-1 (GdiTypeArcTo) ArcTo 仅更新当前点(若 newStroke 先写 MOVETO)
1 (GdiTypeChord) Chord(弦) IntGdiCloseFigure 闭合弦
2 (GdiTypePie) Pie(扇形) 加中心点 `PT_LINETO

流程

  1. 锁路径;x1==x2 || y1==y2(零宽/零高)直接返回 TRUE。
  2. INTERNAL_LPTODP 转换矩形与起止点,well-ordered 矩形(手动交换保证左上/右下)。
  3. PATH_NormalizePoint 求起止归一化坐标 → angleStart/angleEnd = atan2(y,x)
  4. 角度校正 :顺时针要求 angleEnd 落在 angleStart 之后(angleEnd <= angleStart,否则 += 2π);逆时针相反(>=,否则 −= 2π)------保证扫角方向
    与 direction 一致。
  5. GM_COMPATIBLEcorners[1].x--/y--(不包含右/下边缘)。
  6. arcto 且 newStroke:写 PT_MOVETO(当前点)开新子路径。
  7. 逐象限循环 (do-while,每段 ≤ 90°):首段起角 = angleStart,终角 = 下一个
    象限边界(顺时针 (floor(angleStart/π/2)+1)·π/2,逆时针 (ceil−1)·π/2);后续
    段每次 ±π/2;最后一段被 angleEnd 截断end = TRUE)。每段调
    PATH_DoArcPart(..., start ? (arcto ? PT_LINETO : PT_MOVETO) : FALSE)------
    即整条弧的首条记录为 MOVETO(arcto 时为 LINETO,因前面已有 MOVETO)。
  8. 收尾:arcto → update_current_pos;chord → IntGdiCloseFigure;pie →
    PATH_AddEntry(centre, PT_LINETO | PT_CLOSEFIGURE)(中心点连线并闭合)。

注意事项 :源码注释 FIXME------未校验所有错误返回、未尊重 newStroke(非 arcto

分支)。

4.8 PATH_PolyBezier(PDC dc, const POINT *pts, DWORD cbPoints) --- 整段贝塞尔(含起点)

c 复制代码
BOOL FASTCALL PATH_PolyBezier (PDC dc, const POINT *pts, DWORD cbPoints);

add_log_points(dc, pPath, pts, cbPoints, PT_BEZIERTO, FALSE)(cbPoints 须为

3 的倍数:每 3 个点为一段贝塞尔的 3 个控制点,起点取上一记录)→

type[0] = PT_MOVETO首点强制成为子路径起点 )→ 解锁。与 "To" 版本不同,

PolyBezier 的起点由输入 pts0 直接充当 MOVETO,不依赖 DC 当前点。

4.9 PATH_PolyBezierTo(PDC dc, const POINT *pts, DWORD cbPoints) --- 续接贝塞尔

add_log_points_new_stroke(dc, pPath, pts, cbPoints, PT_BEZIERTO, TRUE)

若 newStroke 先补 PT_MOVETO(当前点),再批量追加;bExtraPt=TRUE 时首点

覆盖为新笔画起点。由 line.cIntGdiPolyBezierToPATH_IsPathOpen

调用(LineTo 同理经 IntGdiLineTo)。

4.10 PATH_PolyDraw(PDC dc, const POINT *pts, const BYTE *types, DWORD cbPoints) --- 混合绘制

作用 :PolyDraw 允许一次调用混合 MOVETO/LINETO/BEZIERTO(可带 CLOSEFIGURE),

按 types 数组逐点解释。

流程 :锁路径 → 要求 state == PATH_Open (否则 FALSE)→ 扫描已有记录找

lastmove(最后一个 PT_MOVETO 的索引)并保存 orig_pos、DC 当前点 cur_pos

→ 逐点 switch:

  • PT_MOVETOnewStroke = TRUEpos = pts[i](转设备坐标);lastmove = numEntriesUsed
  • PT_LINETO / PT_LINETO|PT_CLOSEFIGUREadd_log_points_new_stroke(1 点, PT_LINETO, FALSE)
  • PT_BEZIERTO:须后续两点也是 BEZIERTO(types[i+2] & ~PT_CLOSEFIGURE)
    校验),一次 add_log_points_new_stroke(3 点, PT_BEZIERTO, FALSE)i += 2
    否则视为非法落到 default。
  • default(非法类型/孤立的 BEZIERTO):回滚 ------pos = orig_pos
    IntGdiMoveToEx(dc, cur_pos, ...) 恢复 DC 当前点,返回 FALSE。

每步处理后若 types[i] & PT_CLOSEFIGUREclose_figure(pPath)(末点闭合)且

pos = pPoints[lastmove](当前点跳回子路径起点,后续 LINETO 从起点续接)。

4.11 PATH_PolylineTo(PDC dc, const POINT *pts, DWORD cbPoints) --- 续接折线

cbPoints < 1 → FALSE;若 newStrokecbPoints--(首个点由 start_new_stroke

的 MOVETO 承担/覆盖);add_log_points_new_stroke(..., PT_LINETO, TRUE)

line.cIntGdiPolylineTo 在路径打开时调用。

4.12 PATH_PolyPolygon(PDC dc, const POINT* pts, const INT* counts, UINT polygons) --- 多多边形

流程 :校验每个 counts[poly] >= 2(<2 直接 FALSE)→ 汇总总点数 → 一次性

add_log_points(..., PT_LINETO, FALSE) 写入全部点 → 遍历每个多边形:type[0] = PT_MOVETO(首点开子路径)、type[counts[poly]-1] = PT_LINETO | PT_CLOSEFIGURE

(末点闭合)→ 解锁。即一个 PolyPolygon 产生 polygons 条闭合子路径

4.13 PATH_PolyPolyline(PDC dc, const POINT* pts, const DWORD* counts, DWORD polylines) --- 多折线

逐点多边形循环:CoordLPtoDP 转设备坐标,PATH_AddEntry(pt, (point==0) ? PT_MOVETO : PT_LINETO)------每个子折线首点 MOVETO、其余 LINETO,不闭合

4.14 PATH_ExtTextOut(...) 与字形轮廓(PATH_add_outline/PATH_BezierTo

c 复制代码
BOOL FASTCALL PATH_ExtTextOut(PDC dc, INT x, INT y, UINT flags, const RECTL *lprc,
                              LPCWSTR str, UINT count, const INT *dx);

作用 :BeginPath 下 TextOut/ExtTextOut(freetype.c 在 PATH_IsPathOpen

转发到本函数)把每个字符的原生字形轮廓 折成路径记录------这是"文字路径化"

(Path 上写字、字转区域)的实现。

流程

  1. 锁路径;state != PATH_Open → FALSE;count == 0 → TRUE。
  2. ETO_GLYPH_INDEXggo_flags |= GGO_GLYPH_INDEX
  3. 逐字符:ftGdiGetGlyphOutline(dc, ch, GGO_NATIVE, &gm, 0, NULL, &identity, TRUE) 先查询轮廓大小:
    • 返回 GDI_ERROR(位图字体无原生轮廓)→ continue (注释:对位图字体
      ExtTextOut 不应失败,只是生成空路径;参考 test_emf_ExtTextOut_on_path)。
    • dwSize > 0 → 分配缓冲、再取轮廓,PATH_add_outline(dc, pPath, x+offset.x, y+offset.y, outline, dwSize) 追加到路径。
  4. 步进:dx 非空时按 dx 累加(ETO_PDY 时 dx 成对、y 也推进),否则按
    gm.gmCellIncX/gmCellIncY 前进。

PATH_add_outline(PDC dc, PPATH pPath, INT x, INT y, TTPOLYGONHEADER *header, DWORD size)(static):

  • 遍历 TTPOLYGONHEADER 多边形块(校验 dwType == TT_POLYGON_TYPE);
  • 每多边形首点 PT_MOVETOx + int_from_fixed(pfxStart.x)y 取反 ------
    FreeType 轮廓 y 轴向上,GDI 向下);
  • 遍历 TTPOLYCURVE
    • TT_PRIM_LINE:逐点 PT_LINETO;
    • TT_PRIM_QSPLINE/TT_PRIM_CSPLINE:把"曲线起点(前一 POINTFX)+ 各控制点"
      收集成点数组交 PATH_BezierTo
  • 结束统一 IntGdiCloseFigure(每个字形轮廓自闭合)。

PATH_BezierTo(PPATH pPath, POINT *lppt, INT n) (static):把 n 个点构成的

任意阶样条折成三次贝塞尔序列:n2 → LINETO;n3 → 3 点 BEZIERTO;n>3 →

中点细分法(相邻控制点取中点切分,pt[2] = (lppt[i+1]+lppt[i+2])/2),逐段

add_points(3, PT_BEZIERTO)

int_from_fixed(FIXED f) (static inline):f.fract >= 0x8000 ? f.value+1 : f.value------FIXED 定点数四舍五入取整。

4.15 记录示例:一次典型路径的数组内容

以 MM_TEXT 模式(设备坐标 == 逻辑坐标,免去 LP→DP 换算)为例,应用执行:

c 复制代码
BeginPath(hdc);
MoveToEx(hdc, 10, 10, NULL);
LineTo(hdc, 100, 10);
PolyBezierTo(hdc, bez_pts /* 3 点 */, 3);   // 续接一段三次贝塞尔
CloseFigure(hdc);
MoveToEx(hdc, 200, 200, NULL);
Ellipse(hdc, 200, 200, 300, 260);
EndPath(hdc);

记录过程中 PATH 数组逐步演化(numEntriesUsednewStrokepos 关键状态):

复制代码
#  操作                       pPoints 追加        pFlags 追加          newStroke  pos
0  BeginPath                  ---                   ---                    TRUE       DC 当前点
1  MoveToEx(10,10)            ---                   ---                    TRUE       (10,10)
2  LineTo(100,10)             ① (10,10)           PT_MOVETO            FALSE      (10,10)
                              ② (100,10)          PT_LINETO            FALSE      (100,10)
3  PolyBezierTo(3 点)         ③ (bx1,by1)         PT_BEZIERTO          FALSE      (bx3,by3)
                              ④ (bx2,by2)         PT_BEZIERTO
                              ⑤ (bx3,by3)         PT_BEZIERTO
4  CloseFigure                ---                   [⑤] |= PT_CLOSEFIGURE TRUE      (bx3,by3)
5  MoveToEx(200,200)          ---                   ---                    TRUE       (200,200)
6  Ellipse(200,200,300,260)  ⑥..⑱ 13 个椭圆点    ⑥ PT_MOVETO           TRUE      (末点)
                              ⑦..⑱ PT_BEZIERTO
                              [⑱] |= PT_CLOSEFIGURE
7  EndPath                    state: Open → Closed;flPath &= ~DCPATH_ACTIVE

最终数组形态(共 18 条记录,两条子路径):

复制代码
i   0        1        2   3   4   5       6     7 ... 17
pt  (10,10)  (100,10) b1  b2  b3  (200,200) e1 ... e12
fl  MOVETO   LINETO   BZ  BZ  BZ|CF  MOVETO  BZ  ... BZ|CF

随后 StrokePath/FillPath 把该数组作为输入:子路径 1 是"直线 + 贝塞尔 + 闭合"

的开口图形(闭合由 CLOSEFIGURE 回填),子路径 2 是闭合椭圆。宽化时

IntGdiWidenPath 对每条子路径独立生成轮廓;填充时 PATH_PathToRegion

PT_MOVETO 切分出 2 个多边形。


5. 输出层(EndPath 之后)

5.1 PATH_AddFlatBezier(PPATH pPath, POINT *pt, BOOL closed) --- 贝塞尔折线化

c 复制代码
BOOL FASTCALL PATH_AddFlatBezier (PPATH pPath, POINT *pt, BOOL closed);

贝塞尔转折线算法 :调用 GDI_Bezier(pt, 4, &no)(bezier.c)把以 pt[0]

起点、pt[1..3] 为控制点/终点的一段三次贝塞尔离散成 no 个折线点

GDI_Bezier 内部递归细分直到平坦度阈值,输出数组含起点,TAG_BEZIER 池)→

i=1 起逐个 PATH_AddEntry(pts[i], PT_LINETO)(末点且 closed 时

PT_LINETO | PT_CLOSEFIGURE)→ ExFreePoolWithTag(pts, TAG_BEZIER)

关键点 :贝塞尔段在路径中的起点是上一记录pt[0] = pPoints[srcpt-1]),

因此只把折线点从 i=1 写入;闭合标志取自该贝塞尔第 3 个控制点的
PT_CLOSEFIGURE 位
(见 5.2)。

5.2 PATH_FlattenPath(PPATH pPath) --- 整路径转折线

c 复制代码
PPATH FASTCALL PATH_FlattenPath (PPATH pPath);

流程PATH_CreatePath(pPath->numEntriesUsed) 建新路径 → 遍历原路径每条

记录(flag & ~PT_CLOSEFIGURE 分派):

  • PT_MOVETO/PT_LINETO:原样 PATH_AddEntry(含原标志,保留 CLOSEFIGURE);
  • PT_BEZIERTO:以 pPoints[srcpt-1](上一记录)为起点、
    closed = pFlags[srcpt+2] & PT_CLOSEFIGURE(本贝塞尔最后控制点的闭合位)调
    PATH_AddFlatBezier,然后 srcpt += 2 跳过本段其余两个控制点;
  • 任何失败:解锁并 PATH_Delete 新路径、返回 NULL。

结束后 newPath->state = pPath->state返回的新路径已加锁且处于与源相同的
状态
(Closed 路径平化后仍 Closed);源路径不变。

使用NtGdiFlattenPath(平化后替换 DC 路径)、NtGdiFillPath/StrokePath/ StrokeAndFillPath/PathToRegion(先平化再输出)、IntGdiWidenPath(宽化前必须

平化------轮廓计算只处理折线)。

5.3 PATH_PathToRegion(PPATH pPath, INT Mode, PREGION Rgn) --- 路径→区域

c 复制代码
BOOL FASTCALL PATH_PathToRegion (PPATH pPath, INT nPolyFillMode, PREGION Rgn);

流程

  1. numEntriesUsed == 0 → FALSE。
  2. 分配 counts 数组(numEntriesUsed/2 个 ULONG,足够容纳子路径数)------
    pFlags[0] 必须为 PT_MOVETO(ASSERT)。
  3. 扫描记录,遇到每个 PT_MOVETO 就把上一段长度记入 counts[polygons++]
    (段长 = 当前 MOVETO 索引 − 上一段起点),最后补尾段(i > pos+1 时)。
  4. REGION_SetPolyPolygonRgn(Rgn, pPath->pPoints, counts, polygons, Mode)
    (region.c)按 Mode(ALTERNATE/WINDING 填充模式)把多边形组构造成区域;
    失败打印 ERR。
  5. 释放 counts,返回结果。路径本身不变

注意 :counts 数组大小按 numEntriesUsed/2 分配(子路径数 ≤ 条目数/2),

有 ASSERT 校验。

5.4 PATH_FillPath / PATH_FillPathEx --- 填充

c 复制代码
BOOL FASTCALL PATH_FillPath( PDC dc, PPATH pPath );          /* = FillPathEx(dc,pPath,NULL) */
BOOL FASTCALL PATH_FillPathEx(PDC dc, PPATH pPath, PBRUSH pbrFill);

流程(FillPathEx):

  1. IntSysCreateRectpRgn(0,0,0,0) 建临时区域(失败 → ERROR_NOT_ENOUGH_MEMORY)。
  2. PATH_PathToRegion(pPath, pdcattr->jFillMode, Rgn)------按 DC 填充模式
    (ALTERNATE/WINDING)区域化;失败删除区域返回 FALSE。
  3. 坐标基准切换 (关键技巧):路径点已是设备坐标 ,而 IntGdiFillRgn
    逻辑坐标 解释区域,故临时:
    • 保存 iMapMode、viewport/window 的 ext 与 org、世界变换(MatrixS2XForm);
    • IntGdiSetMapMode(dc, MM_TEXT)、viewport/window org 清零;
    • GM_ADVANCEDGreModifyWorldTransform(dc, &xform, MWT_IDENTITY)
      把世界变换临时置为单位矩阵(注释:不能只靠 GM_COMPATIBLE 重置,实测不生效)。
  4. IntGdiFillRgn(dc, Rgn, pbrFill)------实际填充(bitblt.c,《分析_17》;
    pbrFill 为 NULL 时用 DC 当前画刷)。
  5. 按保存值逐一恢复映射模式、ext/org、世界变换(同样借助 GM_ADVANCED +
    MWT_SET)。

PATH_FillPath 只是 FillPathEx(dc, pPath, NULL) 的薄封装(用 DC 画刷)。

源码注释带调侃:"You can play with this as long as you like, but if you break

Area.exe the purge will Begain on Path!!!"(Area.exe 是依赖 FillPath 的测试程序)。

5.5 PATH_StrokePath(DC *dc, PPATH pPath) --- 描边(完整流程)

c 复制代码
BOOL FASTCALL PATH_StrokePath(DC *dc, PPATH pPath);

总流程分两大分支:

复制代码
PATH_StrokePath(dc, pPath)
 ├─ 取 pbrLine = dc->dclevel.pbrLine
 ├─ IntIsEffectiveWidePen(pbrLine) ?(宽笔:宽度 > 1 或几何笔)
 │   ├─ pNewPath = PATH_WidenPathEx(dc, pPath)      // 轮廓化(5.6/5.7)
 │   ├─ 临时 jFillMode = WINDING
 │   ├─ PATH_FillPathEx(dc, pNewPath, pbrLine)      // 用笔刷填充轮廓
 │   ├─ 恢复 jFillMode;PATH_Delete(pNewPath);返回 TRUE
 │   └─ (WidenPathEx 失败则回落细笔分支)
 └─ 细笔分支(cosmetic/单位宽笔):
     ├─ 保存映射模式/viewport/window ext+org/世界变换
     ├─ 置 MM_TEXT + org 清零 + GM_ADVANCED + MWT_IDENTITY(理由同 FillPathEx)
     ├─ 分配折线缓冲 nAlloc = numEntriesUsed + 1 + 300(含贝塞尔展开余量)
     ├─ 逐条记录分派:
     │   ├─ 校验:i==0 或上一记录 CLOSEFIGURE 后必须 PT_MOVETO
     │   ├─ PT_MOVETO → 若 nLinePts>=2 先 IntGdiPolyline 刷出上一段;开新段
     │   ├─ PT_LINETO → 追加到缓冲
     │   ├─ PT_BEZIERTO → 校验连续 3 条;GDI_Bezier(pPoints+i-1, 4, &nBzrPts)
     │   │                折线化;按 nMinAlloc 计算必要时扩容(×2);
     │   │                拷入 pBzrPts[1..nBzrPts-1];i += 2
     │   └─ default → 错误退出
     ├─ 遇 PT_CLOSEFIGURE → 追加 pLinePts[0](闭合回起点)
     ├─ 循环结束 nLinePts>=2 → IntGdiPolyline(dc, pLinePts, nLinePts)
     └─ 恢复映射模式/世界变换;若移动过当前点,IntGetCurrentPositionEx +
        IntDPtoLP + IntGdiMoveToEx 修正 DC 当前点(描边会推进当前位置)

细笔分支的输出对象IntGdiPolyline(line.c)→ 非空表面时经

IntLPtoDP + DC 原点偏移后,细笔走 IntEngPolylineIntEngLineTo

(《分析_5》的线光栅化);若此时判定宽笔则会在 IntGdiPolyline 内递归 建临时

路径再走宽笔分支------但在 PATH_StrokePath 入口已排除宽笔,正常不会发生。

错误处理 :路径标志非法(如 CLOSEFIGURE 后非 MOVETO、贝塞尔不足 3 条、未知

标志)→ ERR + 释放缓冲 + 恢复状态 + 返回 FALSE。缓冲不足时按

nMinAlloc = nLinePts + (numEntriesUsed − i) + nBzrPts 预估并 ×2 重分配。

5.6 IntGdiWidenPath(PPATH pPath, UINT penWidth, UINT penStyle, FLOAT eMiterLimit) --- 宽化核心

c 复制代码
PPATH FASTCALL IntGdiWidenPath(PPATH pPath, UINT penWidth, UINT penStyle, FLOAT eMiterLimit);

作用 :把闭合路径按"笔宽 + 端帽样式 + 连接样式"轮廓化 成一条新闭合路径

(该轮廓填充后即得宽笔描边效果)。返回的新路径 state = PATH_Closed 且已加锁;

失败返回 NULL。

流程

  1. 提取 endcap = PS_ENDCAP_MASK & penStylejoint = PS_JOIN_MASK & penStyle
  2. PATH_FlattenPath(pPath) 平化(宽化只处理折线,贝塞尔必须先行离散)。
  3. 笔宽内外分配:penWidthIn = penWidth/2; penWidthOut = penWidth/2;
    In+Out < penWidthOut++奇数宽时外侧多 1 像素,保证总宽达标)。
  4. 切分子路径 :遍历平化路径,遇 PT_MOVETO 开新 stroke(pStrokes 数组逐次
    扩容重分配 + memcpy 迁移;上一 stroke 置 PATH_Closed,新 stroke 用
    PATH_InitGdiPath + state=PATH_Open 初始化,PT_LINETO 原样拷入;PT_BEZIERTO
    理论上不应出现------已平化,命中打印 ERR)。
  5. pNewPath = PATH_CreatePath(flat_path->numEntriesUsed) 作为轮廓输出路径;
    KeSaveFloatingPointState(&fpsave) 保存浮点状态(后面大量 atan2/cos/sin 需要
    FPU,且内核默认关闭浮点)。
  6. 逐 stroke 轮廓化 :每个 stroke 建两条临时路径 pUpPath/pDownPath
    (Init + Open),按法线偏移 生成上下两条边:
    • 端点(未闭合的 j0 或 jn-1) :取段角
      theta = atan2(ya−yo, xa−xo)(j0 用点 0→1,jn-1 用点 n-2→n-1),按
      endcap 处理:
      • PS_ENDCAP_SQUARE:向"后"延伸半宽(cap_ext = −halfWidth·(cosθ,sinθ)
        后,沿法线 ± 偏移各记一点(penWidthOut 侧与 penWidthIn 侧)------方形帽;
      • PS_ENDCAP_FLAT:仅沿法线 ± 偏移各记一点------平帽;
      • PS_ENDCAP_ROUND(缺省):以端点为中心、penWidthIn/Out 为半径的框,
        用 4 段 45° 的 PATH_DoArcPart(θ+π/2 → θ+3π/2)拼出半圆帽
    • 内部拐点(j>0 且 j<n-1,或闭合路径首尾) :取前驱/后继点,算入段角
      theta = atan2(yo−ya, xo−xa) 与转角
      alpha = atan2(yb−yo, xb−xo) − theta(调整到 (−π, π]:>0 减 π,否则加 π);
      • alpha == 0(共线)→ 跳过该点;
      • PS_JOIN_MITEReMiterLimit < |1/sin(alpha/2)|降级为 BEVEL
        (miter 尖角过长时按米特极限截断);
      • alpha > 0:内侧=pUpPath、外侧=pDownPath;alpha < 0 反之;
      • 内侧点 :沿入段法线偏移 penWidthIn 一点 + 沿出段法线
        θ+α)偏移一点 → 2 个 PT_LINETO;
      • 外侧点 按 joint 三态:
        • PS_JOIN_MITERmiterWidth = |penWidthOut / cos(π/2 − |α|/2)|
          顶点 = xo + miterWidth·(cos(θ+α/2), sin(θ+α/2)) → 1 个 PT_LINETO
          (尖角);
        • PS_JOIN_BEVEL:入段法线偏移点 + 出段法线偏移点 → 2 个 PT_LINETO
          (切角);
        • PS_JOIN_ROUND(缺省):入段法线点、角平分线方向点
          θ+α/2)、出段法线点 → 3 个 PT_BEZIERTO (圆弧连接,保留贝塞尔
          标志)。
    • 开路径单段特例 (未闭合且正好 2 点,j==0):显式补上/下边缘点
      pPoints[1] ± penWidthOut/In·法线)到 pUpPath/pDownPath------没有内部拐点
      来产生线段主体,必须靠端帽之间的这两点连接。
  7. 合并轮廓 :pUpPath 全部点按原标志拷入 pNewPath(k0 强制 PT_MOVETO);
    pDownPath 先 reverse_points逆序拷入(k0 且原 stroke 闭合时
    PT_MOVETO)------上下边反向串接成一条闭合轮廓。
  8. 清理:PATH_DestroyGdiPath + ExFreePoolWithTag 释放每个 stroke、
    pUpPath、pDownPath。
  9. 收尾:pNewPath->state = PATH_ClosedPATH_UnlockPath(pNewPath)
    KeRestoreFloatingPointState;释放 pStrokes 数组;删除平化路径
    (先 Unlock 再 PATH_Delete(flat_path->BaseObject.hHmgr))。

数学基础 :轮廓 = 骨架折线沿法线方向(θ+π/2)平移 ±penWidth/2 后,在拐角处

用 miter(角平分线延长)/bevel(切角)/round(贝塞尔圆弧)三种连接方式、端点处用

square/flat/round 三种端帽闭合。eMiterLimitSetMiterLimit 设置的米特极限

(存于 dc->dclevel.laPath.eMiterLimit)。

5.7 PATH_WidenPath / PATH_WidenPathEx --- 宽化入口

c 复制代码
static PPATH FASTCALL PATH_WidenPath(DC *dc);          /* NtGdiWidenPath 用 */
PPATH FASTCALL PATH_WidenPathEx(DC *dc, PPATH pPath);  /* StrokePath 宽笔分支用 */

PATH_WidenPathEx(dc, pPath) 流程:

  1. pPath->state != PATH_ClosedERROR_CAN_NOT_COMPLETE 返回 NULL。
  2. GreGetObject(pdcattr->hpen, 0, NULL) 查当前画笔对象大小(0 表示失败)。
  3. 分配 EXTLOGPEN 缓冲并 GreGetObject 取回;GDI_HANDLE_GET_TYPE(hpen) 分派:
    • GDI_OBJECT_TYPE_PENpenStyle = ((LOGPEN*)elp)->lopnStyle
    • GDI_OBJECT_TYPE_EXTPENpenStyle = elp->elpPenStyle
    • 其他类型 → ERROR_CAN_NOT_COMPLETE
  4. penWidth = elp->elpWidth;释放缓冲。
  5. 化妆品笔拒绝 :EXTPEN 且 (PS_TYPE_MASK & penStyle) == PS_COSMETIC
    ERROR_CAN_NOT_COMPLETE(化妆品笔宽度恒为 1,宽化无意义)。
  6. return IntGdiWidenPath(pPath, penWidth, penStyle, dc->dclevel.laPath.eMiterLimit)

PATH_WidenPath(dc) :锁 DC 路径 → PATH_WidenPathEx(dc, pPath) → 解锁 →

返回新路径(NtGdiWidenPath 据此替换 DC 路径句柄)。

5.8 输出管线对照与错误码汇总

输出操作 预处理 核心路径 输出对象 消费后路径
FillPath FlattenPath PathToRegion → FillPathEx → IntGdiFillRgn 表面(画刷) 删除
StrokePath(细笔) FlattenPath 折线缓冲 → IntGdiPolyline 表面(笔刷/线) 删除
StrokePath(宽笔) FlattenPath WidenPathEx → FillPathEx(WINDING) 表面(笔刷) 删除
StrokeAndFillPath FlattenPath FillPath 成功后再 StrokePath 表面 删除
WidenPath ---(内部 Flatten) WidenPathEx → IntGdiWidenPath 新 PATH(替换 DC) 原路径删除
FlattenPath --- FlattenPath → 新 PATH 新 PATH(替换 DC) 原路径删除
PathToRegion FlattenPath PathToRegion → REGION_SetPolyPolygonRgn HRGN(交应用) 删除
GetPath --- memcpy + GdiPathDPtoLP 用户缓冲 保留

错误码使用一览EngSetLastError):

错误码 触发场景
ERROR_NOT_ENOUGH_MEMORY PATH_CreatePath/FillPathEx/PATH_StrokePath 分配失败
ERROR_CAN_NOT_COMPLETE EndPath 状态错、CloseFigure 非 Open、WidenPathEx 非 Closed/化妆品笔/笔类型错、GetPath/PathToRegion 非 Closed
ERROR_INVALID_HANDLE 入口处 DC_LockDc 失败(部分入口用 INVALID_PARAMETER)
ERROR_INVALID_PARAMETER GetPath 缓冲过小、DC 锁失败等
ERROR_OUTOFMEMORY WidenPathEx 的 elp 分配失败
ERROR_ARITHMETIC_OVERFLOW GetPath 的 DP→LP 转换溢出(27/32 位限制)

6. NtGdi 系统调用入口(13 个)

path.c 共导出 13 个 NtGdi* 入口 (与《分析_31》第 2 节统计一致),全部遵循

统一模式:DC_LockDc(hDC)(失败 EngSetLastError(ERROR_INVALID_HANDLE/ ERROR_INVALID_PARAMETER))→ PATH_LockPath(dc->dclevel.hPath) → 状态判定 →

调内部 PATH_* 函数 → 逐级解锁。下表先总览,随后逐一展开:

# 入口 功能 状态要求
1 NtGdiBeginPath 开启路径记录 ---
2 NtGdiEndPath 结束记录(→ PATH_Closed) PATH_Open + DCPATH_ACTIVE
3 NtGdiAbortPath 放弃并删除路径 ---
4 NtGdiCloseFigure 闭合当前图(置 CLOSEFIGURE 位) PATH_Open
5 NtGdiFillPath 填充路径(平化→区域→IntGdiFillRgn) PATH_Closed
6 NtGdiStrokePath 描边路径(宽笔轮廓化/细笔折线) PATH_Closed
7 NtGdiStrokeAndFillPath 先填充再描边 PATH_Closed
8 NtGdiWidenPath 宽化路径并替换 DC 路径 PATH_Closed
9 NtGdiFlattenPath 平化路径并替换 DC 路径 PATH_Closed
10 NtGdiPathToRegion 路径转区域(返回 HRGN) PATH_Closed
11 NtGdiGetPath 取路径点+类型(设备→逻辑) PATH_Closed
12 NtGdiGetMiterLimit 取米特极限 ---
13 NtGdiSetMiterLimit 设米特极限 ---

6.1 NtGdiBeginPath(HDC hDC)

  1. DC_LockDc;若 DCPATH_ACTIVE DCPATH_SAVE(路径已开且不是 SaveDC
    副本)→ 直接返回 TRUE(幂等,重复 BeginPath 无副作用)。
  2. 若已有旧路径:非 SAVE → PATH_Delete 删除旧句柄;SAVE → 清
    DCPATH_SAVE|DCPATH_ACTIVE 并把 hPath 置 NULL(不动被保存的路径对象)。
  3. PATH_CreatePath(NUM_ENTRIES_INITIAL=16) 建新路径 → 立刻
    DCPATH_ACTIVE 并挂句柄("Set active ASAP!")→ IntGetCurrentPositionEx +
    IntLPtoDP 把 DC 当前点写入 pPath->pos(作为首个 stroke 的起点候选)→
    解锁、返回 TRUE。

6.2 NtGdiEndPath(HDC hDC)

锁路径后判定:pPath->state != PATH_Open || !(flPath & DCPATH_ACTIVE) → 打印

TRACE、ERROR_CAN_NOT_COMPLETE、返回 FALSE;否则 state = PATH_Closed

flPath &= ~DCPATH_ACTIVE、返回 TRUE。EndPath 是唯一把路径从 Open 变 Closed
的入口
(宽化/平化的临时路径由内部直接置 Closed)。

6.3 NtGdiAbortPath(HDC hDC)

dc->dclevel.hPath 为空 → 直接返回 TRUE(无路径可放弃);否则

PATH_Delete(dc->dclevel.hPath)(失败返回 FALSE)→ hPath = 0

flPath &= ~DCPATH_ACTIVE → TRUE。AbortPath 不要求路径处于任何特定状态

记录中或已闭合的路径都可直接丢弃。

6.4 NtGdiCloseFigure(HDC hDC)

锁 DC → PATH_LockPath → 仅当 pPath->state == PATH_Open 时调

IntGdiCloseFigure(pPath)(末条记录 |= PT_CLOSEFIGUREnewStroke = TRUE

并返回 TRUE;否则 ERROR_CAN_NOT_COMPLETE 返回 FALSE。注意 :CloseFigure

只是"标记"闭合------并不真正往路径里补一条回到起点的线段,闭合线由输出阶段

(StrokePath 的 pLinePts[0] 回填 / FillPath 的区域构造)隐式完成。

6.5 NtGdiFlattenPath(HDC hDC)

锁 DC → 锁路径 → 仅当 state == PATH_ClosedPATH_FlattenPath(pPath)

(否则 pNewPath 保持 NULL)→ 解锁原路径 → 若成功:PATH_Delete(hPath) 删除原

路径、hPath = pNewPath->BaseObject.hHmgr 换平化路径、解锁新路径、返回 TRUE;

否则 FALSE。平化是"就地替换":DC 路径句柄指向新的折线路径,原路径被删除。

6.6 NtGdiGetMiterLimit(HDC hdc, PDWORD pdwOut)

DC_LockDc_SEH2_TRY + ProbeForWrite(pdwOut, sizeof(DWORD), 1) 探测用户

缓冲 → *pdwOut = pDc->dclevel.laPath.eMiterLimit(FLOAT 按 DWORD 位模式回传)

→ SEH 异常时 SetLastNtError 返回 FALSE → 解锁。注意 :返回值是

laPath.eMiterLimit位模式(调用方 gdi32 自己解释为 FLOAT)。

6.7 NtGdiSetMiterLimit(HDC hdc, DWORD dwNew, PDWORD pdwOut)

gxf_long worker(union)把 dwNew 的位模式存为 FLOAT:

pDc->dclevel.laPath.eMiterLimit = worker.fpdwOut 非空时回写旧值

worker1.f 转回 DWORD),同样 SEH 保护。该值随后被 PATH_WidenPathEx 传给

IntGdiWidenPath 用于 miter 极限判断。

6.8 NtGdiGetPath(HDC hDC, LPPOINT Points, LPBYTE Types, INT nSize)

两阶段(探测 → 拷贝)

  1. _SEH2_TRYProbeForWrite(Points, nSize*sizeof(POINT), 4)
    ProbeForWrite(Types, nSize, 1) 探测用户缓冲;异常返回 -1。
  2. 锁 DC → 锁路径;state != PATH_ClosedERROR_CAN_NOT_COMPLETE 跳 done。
  3. 三态:
    • nSize == 0:返回 numEntriesUsed(应用先查大小再分配);
    • nSize < numEntriesUsedERROR_INVALID_PARAMETER 跳 done(缓冲太小);
    • 否则:memcpy 拷贝点与类型到用户缓冲 → GdiPathDPtoLP(dc, Points, numEntriesUsed) 设备→逻辑 转换(失败 ERROR_ARITHMETIC_OVERFLOW
      _SEH2_LEAVE 保留部分结果)→ 返回 numEntriesUsed
  4. done:解锁路径与 DC,返回 ret(失败时 ret 保持 -1)。

返回值语义:-1 = 失败;≥0 = 拷贝的条目数(或 nSize==0 时所需条目数)。

6.9 NtGdiFillPath(HDC hDC)

流程 :锁 DC → 锁路径 → DC_vPrepareDCsForBlit 准备绘制(裁剪/鼠标隐藏合成,

见《分析_24》)→ 若 ulDirty_ & (DIRTY_LINE|DC_PEN_DIRTY)

DC_vUpdateLineBrush;若 DIRTY_FILL|DC_BRUSH_DIRTYDC_vUpdateFillBrush

(确保画刷最新)→ PATH_FlattenPath(pPath) 平化:

  • 平化路径 state != PATH_Closed(理论上不成立,源已 Closed)→
    ERROR_CAN_NOT_COMPLETE
  • numEntriesUsed > 0PATH_FillPath(dc, pNewPath)(区域化 + IntGdiFillRgn);
  • 空路径 → 直接 TRUE(无内容也算成功)。

随后双删 (平化路径 + 原路径)并 hPath = 0、清 DCPATH_ACTIVE

DC_vFinishBlit、解锁,返回结果。FillPath 消费路径:调用后 DC 不再持有路径。

6.10 NtGdiStrokePath(HDC hDC)

与 6.9 同构,但只更新线画刷DIRTY_LINE|DC_PEN_DIRTY → DC_vUpdateLineBrush)

PATH_FlattenPathstate != PATH_Closed 报错;否则

PATH_StrokePath(dc, pNewPath)(宽笔→WidenPathEx 轮廓填充;细笔→IntGdiPolyline

折线)→ 双删、清路径、DC_vFinishBlit、解锁。

6.11 NtGdiStrokeAndFillPath(HDC hDC)

合并 6.9/6.10:更新填充与线两种画刷 → PATH_FlattenPath → 先

PATH_FillPath(pDc, pNewPath)成功后PATH_StrokePath(pDc, pNewPath)

(同一份平化几何先填后描,保证轮廓贴合填充边界)→ 双删、清理、返回

bRet。空路径直接 TRUE。

6.12 NtGdiPathToRegion(HDC hDC) --- 返回 HRGN

流程 :锁 DC → 锁路径 → state != PATH_ClosedERROR_CAN_NOT_COMPLETE

(路径保留,不删除)→ 否则:

  1. REGION_AllocUserRgnWithHandle(1) 分配带用户句柄 的区域(失败返回 NULL);
    hrgnRval = Rgn->BaseObject.hHmgr
  2. PATH_FlattenPath(pPath) 平化(失败:删区域、返回 NULL)。
  3. PATH_PathToRegion(pNewPath, pdcattr->jFillMode, Rgn)------按 DC 填充模式区域化;
    失败:删区域、hrgnRval = NULL;成功:REGION_UnlockRgn(Rgn)(把区域交给
    用户)。
  4. 双删路径、hPath = NULL、清 DCPATH_ACTIVE、解锁,返回 hrgnRval

与 FillPath 的区别 :PathToRegion 把路径几何物化为区域对象 返回给应用

(可继续用于 CombineRgn/SelectClipRgn/FrameRgn 等),而 FillPath 只是用画刷

填充一次即丢弃。

6.13 NtGdiWidenPath(HDC hDC)

锁 DC → PATH_WidenPath(pdc)(内部锁路径 + WidenPathEx)→ 成功则

PATH_Delete(pdc->dclevel.hPath) 删原路径、hPath = pPath->BaseObject.hHmgr

换上宽化轮廓路径 、返回 TRUE;失败返回 FALSE。宽化后路径保持 PATH_Closed

应用可继续对轮廓路径 FillPath/GetPath------这是"粗笔画变成几何"的标准手法。


7. 调用链(mermaid)

7.1 路径记录链(BeginPath 期间图元如何进入路径)

渲染错误: Mermaid 渲染失败: Parse error on line 15: ...TH_AddEntry: pPointsn+pFlagsn] G -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'

7.2 输出链(EndPath 之后)

渲染错误: Mermaid 渲染失败: Parse error on line 20: ... St5[PATH_FillPathEx(WINDING, 笔刷) → IntG -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

7.3 宽笔描边内部链(PATH_StrokePath 宽笔分支展开)

渲染错误: Mermaid 渲染失败: Parse error on line 8: ...OVETO 切分子路径 pStrokes\[\]] C5 --> C6[每 -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'


8. 与《分析_18》《分析_31》及分册关系

说明
《分析_31》第 2 节 系统调用层总览:path.c 共 13 个 NtGdi* 入口(本文第 6 节逐一展开)
PATH 结构与 PATHOBJ 《分析_18》(eng/pathobj.c):EXTPATHDATA 链表(ppdFirst/ppdLast/ppdCurrent)+ EPATHOBJ;本文 2.4/2.5 展开同一 PATH 的双表示
细笔描边输出 《分析_5》(eng/lineto.c 的 IntEngLineTo)------PATH_StrokePath 细笔分支经 IntGdiPolyline → IntEngPolyline 最终到达
填充输出 《分析_17》(bitblt.c 的 IntGdiFillRgn)------PATH_FillPathEx 的区域光栅化
裁剪合成 《分析_24》(MouseSafety/裁剪)------NtGdiFillPath/StrokePathDC_vPrepareDCsForBlit
贝塞尔折线化 bezier.c 的 GDI_BezierTAG_BEZIER 池),path.c 的 PATH_AddFlatBezier/PATH_StrokePath 共用
画刷更新 DC_vUpdateLineBrush/DC_vUpdateFillBrush(dirty 标志驱动),输出入口在绘制前调用

与《分析_18》的边界 :《分析_18》讲的是"面向驱动"的 PATHOBJ DDI 服务层

(eng/pathobj.c);本文讲的是"面向内核自身"的路径记录与输出层(ntgdi/path.c)。

两者共用同一个 PATH 结构(path.h),但数据面完全分离 :path.c 用

pPoints/pFlags 数组,pathobj.c 用 EXTPATHDATA 链表;全树无驱动调用后者。


9. 源码索引

文件 关键内容
path.c(file:///d:/reactos/win32ss/gdi/ntgdi/path.c) 本文全部函数:管理(CreatePath/DestroyGdiPath/Delete/Init/Assign/Save/Restore/Empty/AddEntry/ReserveEntries)、坐标(ScaleNormalizedPoint/NormalizePoint/CheckRect/GdiPathDPtoLP)、图元(MoveTo/LineTo/Rectangle/RoundRect/Ellipse/DoArcPart/Arc/PolyBezier(To)/PolyDraw/PolylineTo/PolyPolygon/PolyPolyline/ExtTextOut+add_outline+BezierTo)、输出(AddFlatBezier/FlattenPath/PathToRegion/FillPath(Ex)/StrokePath/IntGdiWidenPath/WidenPath(Ex))+ 13 个 NtGdi* 入口
path.h(file:///d:/reactos/win32ss/gdi/ntgdi/path.h) PATH 结构、DCPATH_* / GdiPathState / PATHTYPE_* / EXTPATHDATA / EPATHOBJ、PATH_* 声明与句柄宏(PATH_AllocPathWithHandle/PATH_LockPath/PATH_IsPathOpen)
eng/pathobj.c(file:///d:/reactos/win32ss/gdi/eng/pathobj.c) PATHOBJ DDI 服务(EXTPATHDATA 链表的维护者,《分析_18》)
bezier.c(file:///d:/reactos/win32ss/gdi/ntgdi/bezier.c) GDI_Bezier:三次贝塞尔递归细分折线化
line.c(file:///d:/reactos/win32ss/gdi/ntgdi/line.c) IntGdiPolyline/IntGdiPolyBezier(To)/IntGdiPolylineTo:路径打开时转发 PATH_*,否则光栅化(《分析_5》)
bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c) IntGdiFillRgn:区域填充(《分析_17》)
region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c) REGION_SetPolyPolygonRgn/REGION_AllocUserRgnWithHandle/IntSysCreateRectpRgn
coord.c(file:///d:/reactos/win32ss/gdi/ntgdi/coord.c) IntGdiSetMapMode/GreModifyWorldTransform/MatrixS2XForm(FillPathEx/StrokePath 的临时 MM_TEXT 技巧)
wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) PT_MOVETO(6)/PT_LINETO(2)/PT_BEZIERTO(4)/PT_CLOSEFIGURE(1)、PS_ENDCAP_/PS_JOIN_ 掩码、GGO_NATIVE 等

关联模块 :arc.c(IntGdiArcInternal 转发 PATH_Arc)、fillshap.c

(Ellipse/RoundRect 转发 PATH_Ellipse/RoundRect)、freetype.c

ftGdiGetGlyphOutline + ExtTextOut 转发 PATH_ExtTextOut)、dcobjs.c

(SaveDC/RestoreDC 调 PATH_SavePath/RestorePath)、pen.c(画笔对象,宽化取

EXTLOGPEN)。

9.1 path.c 函数行号索引(2026-08 快照)

函数 行号 函数 行号
PATH_CreatePath L34-L72 PATH_DestroyGdiPath L78-L86
PATH_Delete L88-L103 IntGdiCloseFigure L106-L119
GdiPathDPtoLP L123-L134 PATH_InitGdiPath L140-L152
PATH_AssignGdiPath L164-L185 PATH_SavePath L187-L207
PATH_RestorePath L209-L238 PATH_EmptyPath L244-L252
PATH_AddEntry L260-L286 PATH_ReserveEntries L294-L352
PATH_ScaleNormalizedPoint L361-L392 PATH_NormalizePoint L399-L425
PATH_CheckRect L431-L459 add_log_points L463-L486
add_points L490-L501 reverse_points L504-L513
start_new_stroke L516-L526 update_current_pos L529-L533
close_figure L536-L540 add_log_points_new_stroke L543-L554
PATH_MoveTo L562-L582 PATH_LineTo L591-L628
PATH_Rectangle L635-L676 PATH_RoundRect L684-L786
PATH_Ellipse L791-L887 PATH_DoArcPart L898-L951
PATH_Arc L963-L1136 PATH_PolyBezierTo L1138-L1159
PATH_PolyBezier L1161-L1185 PATH_PolyDraw L1187-L1269
PATH_PolylineTo L1271-L1296 PATH_PolyPolygon L1298-L1346
PATH_PolyPolyline L1348-L1383 PATH_AddFlatBezier L1388-L1410
PATH_FlattenPath L1417-L1453 PATH_PathToRegion L1460-L1509
PATH_FillPath L1516-L1523 PATH_FillPathEx L1525-L1606
PATH_StrokePath L1608-L1797 IntGdiWidenPath L1801-L2174
PATH_WidenPath L2176-L2193 PATH_WidenPathEx L2195-L2260
int_from_fixed L2262-L2265 PATH_BezierTo L2272-L2314
PATH_add_outline L2316-L2409 PATH_ExtTextOut L2414-L2511
NtGdiAbortPath L2518-L2546 NtGdiBeginPath L2548-L2602
NtGdiCloseFigure L2604-L2641 NtGdiEndPath L2643-L2683
NtGdiFillPath L2685-L2741 NtGdiFlattenPath L2743-L2785
NtGdiGetMiterLimit L2787-L2818 NtGdiGetPath L2820-L2903
NtGdiPathToRegion L2905-L2987 NtGdiSetMiterLimit L2989-L3027
NtGdiStrokeAndFillPath L3029-L3086 NtGdiStrokePath L3088-L3139
NtGdiWidenPath L3141-L3166 (文件结尾 EOF) L3168

10. 总结

path.c 是 ReactOS GDI 中"记录-回放"架构的完整实现:

  1. 记录面BeginPath 建 PATH(点+标志数组),期间所有图元经
    PATH_AddEntry/add_log_points 折算成统一表示------直线/贝塞尔/矩形/椭圆/圆弧
    (贝塞尔近似)/文字轮廓,坐标一律先转设备坐标。
  2. 状态机PATH_Null → PATH_Open → PATH_Closed,由
    DCPATH_ACTIVE/state 双重把关,保证输出前路径不可变、记录期不可输出。
  3. 输出面 :统一"先平化、再消费 "------PATH_FlattenPath(GDI_Bezier 折线化)
    是 Fill/Stroke/Widen/PathToRegion 的共同前置;填充走区域化 + IntGdiFillRgn
    细笔描边走 IntGdiPolyline,宽笔描边走 IntGdiWidenPath(法线偏移 + 端帽 +
    连接 + 米特极限)再填充轮廓。
  4. 工程细节 :指数扩容保证摊还 O(1);临时 MM_TEXT + 单位世界变换规避
    设备/逻辑坐标二次变换;SEH 探测保护所有用户缓冲;双删 + 句柄清零保证
    DC 生命周期无悬挂路径。

本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/path.cpath.h 及关联模块

(bezier.c/line.c/bitblt.c/region.c/eng/pathobj.c)逐函数分析(2026 年 8 月)。

相关推荐
CarIise2 小时前
C语言基础课程笔记:硬件原理、数据类型、结构体与格式化输入输出详解
c语言·开发语言·笔记
GeekZHR2 小时前
C语言指针2:数组名、二级指针、指针数组,一次把“指针和数组“讲透
c语言·数据结构·算法·指针
Dr.kangder2 小时前
嵌入式面试总结(二十一)——C语言关键字
c语言·开发语言·面试·职场和发展·架构·虚拟化
caimouse3 小时前
ReactOS 图形系统分析(31):内核 GDI — ntgdi
性能优化·reactos
caimouse3 小时前
ReactOS 图形系统分析(33):字体子系统 — font.c
c语言·开发语言·reactos
不悔哥3 小时前
Tinyftp 源码学习笔记:3000 行 C 实现的 FTP 服务器
服务器·c语言·网络·tcp/ip
灵晔君4 小时前
【Linux】进程控制(一)——进程创建、进程终止与进程等待
linux·c语言·算法
库玛西18 小时前
现代 C++ 智能指针全景指南:从 RAII 思想到工业级实践
c语言·开发语言·c++·笔记·面试
AC赳赳老秦18 小时前
风控岗应用:OpenClaw 采集公开司法与经营异常数据,自动生成企业风险评估报告
大数据·c语言·数据库·人工智能·python·php·openclaw