ReactOS 图形系统分析(48):弧线绘制 --- arc.c
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/arc.c(408 行,2026 年 8 月版本)及关联模块逐行分析。关联文件:
win32ss/gdi/ntgdi/path.c(PATH_Arc / PATH_DoArcPart)、win32ss/gdi/ntgdi/drawing.c(IntFillArc / IntDrawArc / app_fill_arc / app_draw_arc)、win32ss/include/ntgdityp.h(ARCTYPE)、win32ss/gdi/gdi32/objects/arc.c(Arc / ArcTo / Chord / Pie / AngleArc 用户态包装)、win32ss/win32u/win32u.spec(系统调用签名)。
目录
- 概述(架构图)
- 设计动机
- 核心数据结构
- 3.1 ARCTYPE 枚举
- 3.2 弧的参数化:矩形 + 起止矢径 + 方向
- 3.3 DC 路径状态与路径弧段
- 弧几何算法详解
- 4.1 起点矢径 → 角度(atan2)
- 4.2 角度区间处理(整圆边角 / 象限分段)
- 4.3 贝塞尔曲线近似圆弧
- 4.4 ARC / CHORD / PIE 三种类型的绘制差异
- 4.5 填充与描边算法概览
- 函数逐一分析
- 5.1 NtGdiArcInternal(系统调用入口)
- 5.2 IntGdiArcInternal(内部入口)
- 5.3 IntArc(静态核心实现)
- 5.4 NtGdiAngleArc(系统调用入口)
- 5.5 IntGdiAngleArc(AngleArc 内部实现)
- 5.6 PATH_Arc(路径模式下的弧)
- 5.7 PATH_DoArcPart(单段贝塞尔弧段)
- 5.8 IntFillArc / IntDrawArc(drawing.c)
- 5.9 文件级宏 PUTPIXEL / PUTLINE
- 调用链(mermaid 时序)
- 与相关模块的关系
- 注意事项与已知问题
- 源码索引
1. 概述(架构图)
arc.c 是 win32k(内核态 GDI)中负责弧线绘制 的模块,实现了 Windows 的 Arc、ArcTo、Chord、Pie、AngleArc 五个 API 的内核部分。整个模块只包含两个系统调用入口(NtGdiArcInternal、NtGdiAngleArc)和三个内部函数(IntGdiArcInternal、IntGdiAngleArc、静态的 IntArc),全部代码 408 行。
整体架构如下:
应用层(user32 / gdi32)
│
├─ Arc() ──→ NtGdiArcInternal(GdiTypeArc, ...) ──┐
├─ ArcTo() ──→ NtGdiArcInternal(GdiTypeArcTo, ...) │ 用户态包装
├─ Chord() ──→ NtGdiArcInternal(GdiTypeChord, ...) │ (gdi32/objects/arc.c)
├─ Pie() ──→ NtGdiArcInternal(GdiTypePie, ...) │
└─ AngleArc() ──→ NtGdiAngleArc(...) ──┘
│ 经 win32u.dll 进入内核
▼
win32k(ntgdi)
┌──────────────────────────────────────────────────────────────┐
│ arc.c │
│ NtGdiArcInternal ──→ IntGdiArcInternal │
│ NtGdiAngleArc ──→ IntGdiAngleArc ──→ IntGdiArcInternal │
│ │ │
│ 路径已打开? PATH_IsPathOpen │
│ / \ │
│ 是:PATH_Arc 否:IntArc │
│ (path.c) │ │
│ ┌─────────────────────────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ PATH_DoArcPart IntFillArc IntDrawArc │
│ (逐象限贝塞尔弧段) (drawing.c) (drawing.c) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ add_points / app_fill_arc app_draw_arc │
│ PATH_ScaleNormalizedPoint (中点椭圆扫描) (内/外椭圆环) │
└──────────────────────────────────────────────────────────────┘
│
▼
IntEngLineTo / IntGdiFillRgn / 路径描边(PATH_StrokePath)
从图中可以看出核心设计:弧线绘制分两条路线:
- 路径模式 (
BeginPath已打开路径):Arc/ArcTo/Chord/Pie全部转成路径操作PATH_Arc,把椭圆弧分解为若干个贝塞尔样条段写入路径,之后由统一的路径描边/填充管线输出。 - 直接绘制模式 (路径未打开):
IntArc先把起止角度算出来,然后调用IntFillArc(填充楔形)与IntDrawArc(描边弧线)完成绘制;Chord/Pie额外的弦线/径向线用PUTLINE宏(IntEngLineTo)补画。
AngleArc 是独立的 API:它以"当前点 → 弧起点"的连线为前置,按绝对角度+扫过角画弧,并把当前点移动到弧终点。它内部复用 IntGdiArcInternal(GdiTypeArcTo, ...) 实现,这体现了模块内部对 GdiTypeArcTo 语义的复用。
下表是模块内所有函数的速览(行号以当前 arc.c 为准):
| 函数 | 行号 | 可见性 | 作用 |
|---|---|---|---|
PUTPIXEL |
9--15 | 宏 | 以当前画笔画 1×1 单位线段 |
PUTLINE |
17--23 | 宏 | 以当前画笔画任意线段(用于 Pie/Chord 的补线) |
IntArc |
25--180 | static | 弧线核心实现:算角度、填充+描边、补线 |
IntGdiArcInternal |
183--250 | 内部 | 统一入口:路径分支 / ArcTo 连线 / 调 IntArc |
IntGdiAngleArc |
252--295 | 内部 | AngleArc 内部:算端点、设方向、委托 ArcTo |
NtGdiAngleArc |
299--343 | 系统调用 | AngleArc 内核入口:锁 DC、浮点保护、位模式转 FLOAT |
NtGdiArcInternal |
345--408 | 系统调用 | Arc 族内核入口:校验类型、锁 DC、浮点保护 |
注意:早期文档或任务描述中提到的 IntGdiGetArcAngles 在本版本源码中并不存在 (已在全仓库检索确认无此标识符);角度计算内联在 IntArc(atan2)与 PATH_Arc(atan2 + PATH_NormalizePoint)中,属于纯函数式辅助而非独立函数。
2. 设计动机
理解 arc.c 的设计,需要先回顾 Windows GDI 对"弧"的定义方式与 ReactOS 的实现取舍:
-
API 几何语义的一致性 。Windows 的
Arc/ArcTo/Chord/Pie共用同一套参数:一个边界矩形(Left, Top, Right, Bottom)加上两个"径向端点"(XStartArc, YStartArc, XEndArc, YEndArc)。弧本身是矩形的内切椭圆的一部分,由"椭圆中心到两个径向端点的射线"夹出角度区间。因此把四个 API 合并为一个内核入口NtGdiArcInternal(ARCTYPE, ...)并用枚举区分行为,是最自然的设计------这也正是 arc.c 的结构。 -
两条执行路径的复用 。路径(path)是 GDI 的核心抽象:
BeginPath之后,所有绘制图元都记录为路径点序列,可以统一描边(StrokePath)或填充(FillPath)。ReactOS 的做法是:路径打开时把弧翻译成贝塞尔弧段(PATH_Arc),路径未打开时走"填充+描边"直接光栅化(IntArc)。这样同一个NtGdiArcInternal在不同上下文中给出一致结果,代码只分叉一次。 -
浮点只在需要的地方出现 。弧的角度计算离不开
atan2/cos/sin(椭圆参数方程),而 win32k 是内核模块:内核默认不保存浮点状态,使用浮点寄存器前必须KeSaveFloatingPointState/KeRestoreFloatingPointState。因此两个系统调用入口都显式包住浮点区域,内部函数则尽量用整数运算(如IntArc中矩形中心、PATH_ScaleNormalizedPoint的 FLOATOBJ 定点模拟浮点)。 -
AngleArc 的特殊语义 。
AngleArc("角弧")与 Arc 族不同:它不依赖"径向端点",而是直接给圆心、半径、起始绝对角度和扫过角,且绘制前先画一条"当前点到弧起点"的线段,绘制后当前点位于弧终点。ReactOS 通过复用GdiTypeArcTo分支自然获得"连线 + 移点"的行为,再补上角度参数化转换(cos/sin求端点)与方向设置。 -
效率与正确的平衡 。填充弧楔形(Pie/Chord 的面)由 drawing.c 的
app_fill_arc用"中点椭圆扫描 + 扫描线楔形裁剪"完成,纯整数 Bresenham 风格,避免逐点调用昂贵的光栅接口;描边则用内外两个椭圆(笔宽差)求环形区域。这些算法与drawing.c中的椭圆(IntFillEllipse/IntDrawEllipse)共用,体现了 ntgdi 光栅代码的模块化复用。 -
与 Windows 的兼容性细节 。代码中处处可见对 Windows 语义的模拟:GM_COMPATIBLE 模式下右下边界减一(不含 right/bottom 边)、
PS_INSIDEFRAME笔向内收缩、Arc/Pie/Chord按逆时针语义处理整圆边角、SetArcDirection(AD_CLOCKWISE/AD_COUNTERCLOCKWISE)通过 DC 的DCPATH_CLOCKWISE标志传递到所有弧函数。
3. 核心数据结构
3.1 ARCTYPE 枚举
ARCTYPE 定义在 win32ss/include/ntgdityp.h(第 18--24 行):
c
typedef enum _ARCTYPE
{
GdiTypeArc, // 0
GdiTypeArcTo, // 1
GdiTypeChord, // 2
GdiTypePie, // 3
} ARCTYPE, *PARCTYPE;
各成员的含义:
| 枚举值 | 数值 | 对应 API | 语义 |
|---|---|---|---|
GdiTypeArc |
0 | Arc |
只画椭圆弧本身,不闭合 |
GdiTypeArcTo |
1 | ArcTo、AngleArc(内部) |
从当前点连线到弧起点,画弧,当前点移到弧终点 |
GdiTypeChord |
2 | Chord |
弧 + 连接弧两端点的弦(封闭图形) |
GdiTypePie |
3 | Pie |
弧 + 圆心到两端点的两条径向线(扇形封闭图形) |
几点说明:
- 该枚举同时也被当作 PATH_Arc 的
lines参数语义使用(见 5.6 节)。PATH_Arc 的注释里写着 "lines为 1 时加一条线得弦、为 2 时加两条线得饼、为 -1 时先连线再画弧(arcto)",这是从 Wine 移植时遗留的旧数值注释;实际调用中传入的就是 ARCTYPE 枚举值(GdiTypeArcTo == 1、GdiTypeChord == 2、GdiTypePie == 3),代码内部用枚举常量比较,与注释的绝对值不再一一对应。这是"注释过期"的典型例子,阅读时不要被注释误导。 NtGdiArcInternal用arctype > GdiTypePie做合法性校验(见 5.1 节),说明该参数只接受 0--3。IntFillArc/IntDrawArc只关心arctype == GdiTypeChord这一位(BOOL Chord = (arctype == GdiTypeChord)),因为填充/描边函数只区分"是否画弦",Pie 的径向线由IntArc用PUTLINE另行补画。
3.2 弧的参数化:矩形 + 起止矢径 + 方向
Windows 弧 API 的参数模型(以 Arc 为例):
c
Arc(hdc, Left, Top, Right, Bottom, XStartArc, YStartArc, XEndArc, YEndArc);
(Left, Top, Right, Bottom):包围椭圆的边界矩形(可与 DC 变换一起缩放/旋转)。(XStartArc, YStartArc):起点矢径端点。弧的起点 = "椭圆中心到该点的连线"与椭圆的交点。(XEndArc, YEndArc):终点矢径端点。弧的终点同理。- 弧的方向由 DC 的弧方向(
SetArcDirection设置的 AD_CLOCKWISE / AD_COUNTERCLOCKWISE,默认逆时针)决定。
因此一条弧由五个几何要素确定:
- 椭圆中心
C = ((Left+Right)/2, (Top+Bottom)/2); - 半轴长
a = |Right-Left|/2、b = |Bottom-Top|/2; - 起点方向角
θs = atan2(YStart-Cy, XStart-Cx); - 终点方向角
θe = atan2(YEnd-Cy, XEnd-Cx); - 方向(顺时针 / 逆时针)。
在 arc.c 的 IntArc 中,矢量端点被塞进一个 RECTL 结构体批量转换(RectSEpts),其中:
c
RectSEpts.left = XRadialStart; // 起点 x
RectSEpts.top = YRadialStart; // 起点 y
RectSEpts.right = XRadialEnd; // 终点 x
RectSEpts.bottom = YRadialEnd; // 终点 y
这样可以利用 IntLPtoDP(dc, (LPPOINT)&RectSEpts, 2) 一次完成两个点的逻辑→设备坐标转换(RECTL 的 4 个 LONG 恰好是 2 个 POINT)。角度计算直接以设备坐标进行:
c
CenterX = (RectBounds.right + RectBounds.left) / 2;
CenterY = (RectBounds.bottom + RectBounds.top) / 2;
AngleEnd = atan2((RectSEpts.bottom - CenterY), RectSEpts.right - CenterX) * (360.0/(M_PI*2));
AngleStart = atan2((RectSEpts.top - CenterY), RectSEpts.left - CenterX) * (360.0/(M_PI*2));
注意这里的"矢径"概念:起止点不一定在椭圆上,角度只取决于它们相对于中心的方向(atan2 只关心比例),这正是"径向端点"的语义------与椭圆半径无关。
3.3 DC 路径状态与路径弧段
路径打开时(BeginPath 之后),弧被翻译为路径弧段。相关数据结构定义在 win32ss/gdi/ntgdi/path.h。
DC 路径标志(path.h 第 4--12 行):
c
enum _DCPATHFLAGS
{
DCPATH_ACTIVE = 0x0001, // 路径已打开(BeginPath 生效)
DCPATH_SAVE = 0x0002, // 路径保存中(SaveDC 相关)
DCPATH_CLOCKWISE = 0x0004, // 弧方向:顺时针(否则逆时针)
/* ReactOS only */
DCPATH_SAVESTATE = 0x80000000
};
DCPATH_ACTIVE:路径是否打开。PATH_IsPathOpen(dclevel)宏(path.h 第 72 行)判断hPath非空且flPath & DCPATH_ACTIVE,arc.c 用它决定走PATH_Arc还是IntArc。DCPATH_CLOCKWISE:全局弧方向位。SetArcDirection(AD_CLOCKWISE)置位、AD_COUNTERCLOCKWISE清位(见 dcutil.c 第 501--503、633--653 行)。IntArc、PATH_Arc、IntFillArc、IntDrawArc、IntGdiArcInternal、IntGdiAngleArc全都读取/修改这个位。值得注意的是,SetArcDirection的写入通过DC_SetArcDirection(dcutil.c)完成,且在路径未打开时也有效,因为弧方向独立于路径状态。
路径对象 PATH(path.h 第 34--59 行,节选关键字段):
c
typedef struct _PATH
{
BASEOBJECT BaseObject;
...
FLONG state; // PATH_Null / PATH_Open / PATH_Closed
POINT *pPoints; // 设备坐标点数组
BYTE *pFlags; // 每个点的类型标志数组(PT_*)
int numEntriesUsed; // 已用条目数
int numEntriesAllocated;
BOOL newStroke; // 是否需要在新条目前插入 PT_MOVETO
POINT pos; // 路径"当前点"
} PATH, *PPATH;
路径条目类型标志(PT_*,wingdi.h 定义):
| 标志 | 含义 |
|---|---|
PT_MOVETO |
移动到该点(新子路径起点) |
PT_LINETO |
直线段到该点 |
PT_BEZIERTO |
该点与后续两个点构成贝塞尔曲线段(三点一组) |
PT_CLOSEFIGURE |
子路径闭合(可与上述标志按位或) |
PATH_Arc 通过 PATH_AddEntry/add_points 把弧段写入 pPoints/pFlags;PATH_DoArcPart 用 PATH_ScaleNormalizedPoint 把单位圆上的归一化控制点映射回矩形坐标。这些数据结构是弧→路径转换的载体。
4. 弧几何算法详解
4.1 起点矢径 → 角度(atan2)
标准平面几何中,给定椭圆中心 C 与一个矢径端点 P,方向角为:
θ = atan2(Py - Cy, Px - Cx) 单位:弧度([-π, π])
arc.c 中把弧度换算成度数(* 360.0 / (2π)),因为后续 IntFillArc/IntDrawArc 的 app_* 函数以"整数度数"为接口。度数取值落在 (-180, 180]。
AngleStart = atan2(StartY - CenterY, StartX - CenterX) * 180 / π
AngleEnd = atan2(EndY - CenterY, EndX - CenterX) * 180 / π
角度 → 弧上点的逆运算(AngleArc 使用,见 5.5 节):
x = cx + cos(θ) * r
y = cy - sin(θ) * r // 屏幕坐标系 Y 向下,故取负
注意 atan2 的坐标约定:数学上"逆时针为正",但屏幕坐标 Y 轴向下,所以 atan2(dy, dx) 得到的角度在视觉上是"顺时针为正"。这解释了为何 IntArc 中 AngleStart = AngleEnd + 360.0 的注释说"Arc/ArcTo/Pie/Chord 是逆时针 API"------这里的"逆时针"是数值递增方向的表述,与屏幕上看到的方向相反,阅读时不要混淆(细节见 4.2)。
重要边角情况 :当起点矢径与终点矢径同向(AngleEnd == AngleStart,例如两者重合或相差 360° 整数倍)时,理论上弧为零长度。但 Windows 语义要求此时画出完整椭圆(360° 弧)。IntArc 第 128--132 行处理:
c
if (AngleEnd == AngleStart)
{
AngleStart = AngleEnd + 360.0; // Arc(), ArcTo(), Pie() and Chord() are counterclockwise APIs.
}
即把起始角向后拨 360°,使角度区间变为 θ, θ+360,覆盖整个椭圆。这一处理对 IntFillArc(完整填充椭圆)和 IntDrawArc(完整描边椭圆)都成立。
4.2 角度区间处理(整圆边角 / 象限分段)
IntArc 侧(直接绘制) :起止角由 atan2 得到后,经过"相等 → +360°"的边角处理,保证 AngleStart < AngleEnd(数值上),于是弧扫过 (AngleEnd - AngleStart) 度的角度。之后原样传给 IntFillArc/IntDrawArc,后者(drawing.c)再按自己的规则归一化。
PATH_Arc 侧(路径模式,path.c 第 1037--1053 行):角度是弧度,且需要按方向调整区间,保证终点角在起点的"正确一侧":
c
if (clockwise)
{
if (angleEnd <= angleStart)
angleEnd += 2 * M_PI; // 顺指针扫过:终点角必须大于起点角
}
else
{
if (angleEnd >= angleStart)
angleEnd -= 2 * M_PI; // 逆指针扫过:终点角必须小于起点角
}
这样 angleStart 与 angleEnd 之间的有向区间就是弧的实际扫过范围,跨度可以是任意值(包括超过 360° 或为负方向的多个整圆)。
象限分段(path.c 第 1077--1116 行) :贝塞尔曲线只能精确逼近小角度弧段(误差随圆心角增大而增大),因此 PATH_Arc 把弧按 90°(π/2)为界切成若干段,每段调用一次 PATH_DoArcPart:
c
start = TRUE;
end = FALSE;
do
{
if (start)
{
angleStartQuadrant = angleStart;
if (clockwise)
angleEndQuadrant = (floor(angleStart / M_PI_2) + 1.0) * M_PI_2; // 下一个 90° 边界
else
angleEndQuadrant = (ceil(angleStart / M_PI_2) - 1.0) * M_PI_2; // 上一个 90° 边界
}
else
{
angleStartQuadrant = angleEndQuadrant;
if (clockwise) angleEndQuadrant += M_PI_2;
else angleEndQuadrant -= M_PI_2;
}
if ((clockwise && angleEnd < angleEndQuadrant) ||
(!clockwise && angleEnd > angleEndQuadrant))
{
angleEndQuadrant = angleEnd; // 最后一段:截断到终点角
end = TRUE;
}
PATH_DoArcPart(pPath, corners, angleStartQuadrant, angleEndQuadrant,
start ? (lines == GdiTypeArcTo ? PT_LINETO : PT_MOVETO) : FALSE);
start = FALSE;
}
while (!end);
- 首段的端点类型:
GdiTypeArcTo用PT_LINETO(从当前点连线到弧起点),其他类型用PT_MOVETO(新子路径)。 - 之后各段
startEntryType == 0,PATH_DoArcPart假定当前路径位置就是段起点,直接追加三个贝塞尔控制点。 - 顺时针时用
floor(向下取到最近的 90° 边界再 +90°),逆时针用ceil(向上取整再 -90°),保证第一段不超过 90° 且方向正确。
4.3 贝塞尔曲线近似圆弧
PATH_DoArcPart(path.c 第 898--951 行)实现"用一条三次贝塞尔曲线逼近圆心角 ≤ 90° 的圆弧"。其数学原理:
对圆心角 Δθ = angleEnd - angleStart,取半角 h = Δθ/2,标准圆弧贝塞尔逼近的控制点系数为:
a = 4/3 * (1 - cos h) / sin h = 4/3 * tan(h/2)
当 Δθ = 90°(h = 45°)时,a = 4/3 * (√2 - 1) ≈ 0.5523,即众所周知的四分之一圆系数。
代码(path.c 第 918--936 行):
c
halfAngle = (angleEnd - angleStart) / 2.0;
if (fabs(halfAngle) > 1e-8)
{
a = 4.0 / 3.0 * (1 - cos(halfAngle)) / sin(halfAngle);
xNorm[0] = cos(angleStart); yNorm[0] = sin(angleStart); // P0 = 单位圆起点
xNorm[1] = xNorm[0] - a * yNorm[0]; // P1 = P0 + a*(-y0, x0) 切向偏移
yNorm[1] = yNorm[0] + a * xNorm[0];
xNorm[3] = cos(angleEnd); yNorm[3] = sin(angleEnd); // P3 = 单位圆终点
xNorm[2] = xNorm[3] + a * yNorm[3]; // P2 = P3 - a*(y3, -x3) 切向偏移
yNorm[2] = yNorm[3] - a * xNorm[3];
}
else
for (i = 0; i < 4; i++) // 零角度段:四点重合(退化)
{
xNorm[i] = cos(angleStart);
yNorm[i] = sin(angleStart);
}
几何解释:
P0 = (cos θs, sin θs)、P3 = (cos θe, sin θe)是单位圆上的起点与终点(归一化坐标)。- 切向量为
(-sin θ, cos θ)(逆时针)与(sin θ, -cos θ)(顺时针)。P1 = P0 + a*(-y0, x0)即沿起点切线方向伸出a倍距离;P2 = P3 + a*(y3, -x3)即沿终点反切线方向伸出a倍距离。 - 半角极小(
|h| ≤ 1e-8)时sin h ≈ 0会使系数发散,故退化为四个重合点(等价于不产生可见弧段)。
归一化 → 设备坐标 :以上控制点都在单位圆(归一化坐标 -1,1)上,需映射回边界矩形。PATH_ScaleNormalizedPoint(path.c 第 360--391 行)完成线性映射:
x_dev = corners[0].x + (corners[1].x - corners[0].x) * (x_norm + 1) / 2
y_dev = corners[0].y + (corners[1].y - corners[0].y) * (y_norm + 1) / 2
(代码用 FLOATOBJ 定点运算模拟浮点,避免内核浮点寄存器使用。)
配套的 PATH_NormalizePoint(path.c 第 398--424 行)做逆变换,把矢径端点归一化后供 atan2 求角:
x_norm = (pt.x - corners[0].x) * 2 / (corners[1].x - corners[0].x) - 1
y_norm = (pt.y - corners[0].y) * 2 / (corners[1].y - corners[0].y) - 1
PATH_Arc 第 1032--1035 行正是这样求起止角的:
c
PATH_NormalizePoint(corners, &pointStart, &x, &y);
angleStart = atan2(*(FLOAT*)&y, *(FLOAT*)&x);
PATH_NormalizePoint(corners, &pointEnd, &x, &y);
angleEnd = atan2(*(FLOAT*)&y, *(FLOAT*)&x);
PATH_DoArcPart 最后把 4 个(或 3 个,若 start 已含首点)控制点以 PT_BEZIERTO 类型追加到路径(add_points,path.c 第 489--500 行),并把首点类型改写为调用者指定的 startEntryType:
c
start = !startEntryType; // startEntryType 非零 → 首点已由调用者保证
for (i = start; i < 4; i++) // 追加控制点
if (!PATH_ScaleNormalizedPoint(corners, *(FLOATL*)&xNorm[i], *(FLOATL*)&yNorm[i], &points[i]))
return FALSE;
if (!(type = add_points(pPath, points + start, 4 - start, PT_BEZIERTO))) return FALSE;
if (!start) type[0] = startEntryType; // 首点改为 PT_MOVETO / PT_LINETO
4.4 ARC / CHORD / PIE 三种类型的绘制差异
三种类型共享同一段椭圆弧,差别在于闭合方式:
| 类型 | 图形 | 填充区域 | 补画线(IntArc) | 路径模式(PATH_Arc) |
|---|---|---|---|---|
GdiTypeArc |
开放式弧线 | 无(不填充) | 无 | 只有弧段,不闭合 |
GdiTypeChord |
弧 + 弦 | 弧与弦围成的弓形 | PUTLINE(终点, 起点) |
IntGdiCloseFigure(pPath)(弦由闭合隐式产生) |
GdiTypePie |
弧 + 两条径向线 | 扇形 | PUTLINE(中心, 起点) + PUTLINE(终点, 中心) |
`PATH_AddEntry(center, PT_LINETO |
直接绘制模式(IntArc,第 134--174 行):
c
if ((arctype == GdiTypePie) || (arctype == GdiTypeChord))
{
ret = IntFillArc(dc, RectBounds.left, RectBounds.top,
abs(RectBounds.right-RectBounds.left), // Width
abs(RectBounds.bottom-RectBounds.top), // Height
AngleStart, AngleEnd, arctype);
}
if (ret)
{
ret = IntDrawArc(dc, ..., AngleStart, AngleEnd, arctype, pbrPen);
}
...
if (arctype == GdiTypePie)
{
PUTLINE(CenterX, CenterY, RectSEpts.left, RectSEpts.top, dc->eboLine);
PUTLINE(RectSEpts.right, RectSEpts.bottom, CenterX, CenterY, dc->eboLine);
}
if (arctype == GdiTypeChord)
PUTLINE(RectSEpts.right, RectSEpts.bottom, RectSEpts.left, RectSEpts.top, dc->eboLine);
即:只有 Chord/Pie 需要先填充(IntFillArc,GdiTypeArc 只描边不填充),随后统一描边(IntDrawArc),最后用 PUTLINE 补画弦线或两条径向线。注意填充和描边都用 abs(宽度/高度)------IntArc 开头已把矩形排序(见 5.3 节),宽度高度恒为非负。
路径模式(PATH_Arc 第 1118--1133 行):
c
if (lines == GdiTypeArcTo)
update_current_pos(pPath); // 当前点 = 弧终点
else if (lines == GdiTypeChord)
IntGdiCloseFigure(pPath); // 最后一条边置 PT_CLOSEFIGURE → 闭合为弦
else if (lines == GdiTypePie)
{
centre.x = (corners[0].x + corners[1].x) / 2;
centre.y = (corners[0].y + corners[1].y) / 2;
if (!PATH_AddEntry(pPath, ¢re, PT_LINETO | PT_CLOSEFIGURE))
Ret = FALSE;
}
- Chord:弧段结束后调用
IntGdiCloseFigure(path.c 第 106--119 行),把最后一条边置上PT_CLOSEFIGURE并置newStroke = TRUE------闭合成弦。 - Pie:显式把椭圆中心作为
PT_LINETO | PT_CLOSEFIGURE条目加入,形成两条径向线并闭合。 - Arc(
GdiTypeArc == 0):三个分支都不匹配,弧保持开放。
4.5 填充与描边算法概览
IntFillArc/IntDrawArc 最终委托给 drawing.c 的 app_fill_arc/app_draw_arc(详见 5.8 节),其核心是中点椭圆扫描算法(Bresenham 风格,纯整数):
e(x,y) = b²x² + a²y² - a²b² (a = 宽/2,b = 高/2,椭圆隐式方程)
app_fill_arc(drawing.c 第 783--960 行):
- 起止角相差 ≥ 360° → 等价于完整椭圆,直接
app_fill_ellipse。 - 把角度归一化到 [0, 360°);起止角相等 → 什么都不画。
- 用
app_boundary_point求两条径向线与椭圆边界的交点 p1、p2;Chord 时楔形顶点 p0 取弦中点,否则取椭圆中心。 - 用中点椭圆步进(
t = b² + a² - 2a²b判别式,逐步决定 moveout/movedown)逐行扫描,得到上下对称的矩形条 r1/r2。 - 每个矩形条调用
app_fill_arc_rect(drawing.c 第 367--... 行):计算当前扫描线与起/止径向线的交点,结合"起止点是否位于上半平面"(start_above/end_above)判断填充楔形内部还是外部,最后用app_fill_rect(扫描线填充)落笔。
app_draw_arc(drawing.c 第 962 行起):用"外椭圆(半径 a,b)+ 内椭圆(半径 A=a-笔宽, B=b-笔宽)"构造环形区域,逐段输出线段,实现带笔宽的弧描边。
5. 函数逐一分析
5.1 NtGdiArcInternal(系统调用入口,arc.c 第 345--408 行)
签名:
c
BOOL APIENTRY NtGdiArcInternal(
ARCTYPE arctype, // GdiTypeArc / GdiTypeArcTo / GdiTypeChord / GdiTypePie
HDC hDC,
int LeftRect, int TopRect, int RightRect, int BottomRect, // 边界矩形
int XStartArc, int YStartArc, int XEndArc, int YEndArc); // 起止矢径端点
系统调用绑定(win32ss/win32u/win32u.spec 第 19 行):
@ stdcall NtGdiArcInternal(long ptr long long long long long long long long)
gdi32 侧(win32ss/gdi/gdi32/objects/arc.c)的 Arc/ArcTo/Chord/Pie 四个 API 都只是参数透传:处理元文件(HANDLE_METADC/HANDLE_EMETAFDC 宏)后,以不同 ARCTYPE 调用本函数。
作用 :内核态入口,负责三件事------锁定 DC、刷新脏刷子、保护浮点上下文,然后委托 IntGdiArcInternal。
实现流程:
- 锁 DC (第 364--369 行):
DC_LockDc(hDC);失败(无效句柄)→EngSetLastError(ERROR_INVALID_HANDLE)返回 FALSE。 - 类型校验 (第 370--375 行):
arctype > GdiTypePie(即大于 3)→ 解锁并EngSetLastError(ERROR_INVALID_PARAMETER)。这保证了 ARCTYPE 只能取 0--3。 - 准备 DC (第 377--383 行):
DC_vPrepareDCsForBlit(dc, NULL, NULL, NULL)准备表面;若ulDirty_标记了填充刷/线条刷脏(DIRTY_FILL | DC_BRUSH_DIRTY、DIRTY_LINE | DC_PEN_DIRTY),分别调用DC_vUpdateFillBrush/DC_vUpdateLineBrush重建刷子(保证弧用上最新的画刷/画笔)。 - 浮点保护 (第 385--390 行):
KeSaveFloatingPointState(&FloatSave)保存浮点寄存器;失败则解锁返回。 - 委托 (第 392--402 行):
IntGdiArcInternal(arctype, dc, LeftRect, ..., YEndArc)。 - 收尾 (第 404--406 行):
KeRestoreFloatingPointState、DC_vFinishBlit(dc, NULL)、DC_UnlockDc(dc),返回结果。
注意事项:
- 浮点保存/恢复是必须的:
IntArc与PATH_Arc内部使用atan2/cos/sin,而 win32k 内核线程默认不保存浮点上下文,若被抢占切换线程会导致浮点状态错乱。 DC_vPrepareDCsForBlit返回的"脏"状态同时覆盖了IntGdiArcInternal内部可能用到的路径操作(路径模式也在这个入口之下),保证路径点数组的写入安全。
5.2 IntGdiArcInternal(内部入口,arc.c 第 183--250 行)
签名:
c
BOOL FASTCALL IntGdiArcInternal(
ARCTYPE arctype, // 类型
DC *dc, // 已锁定的 DC(内核对象指针,非句柄)
int LeftRect, int TopRect, int RightRect, int BottomRect,
int XStartArc, int YStartArc, int XEndArc, int YEndArc);
作用 :统一内部入口。两条执行路径的分叉点------路径已打开走 PATH_Arc;否则处理 ArcTo 的连线/移点语义后走 IntArc。
实现流程:
- 空矩形检查 (第 204 行):
(LeftRect == RightRect) || (TopRect == BottomRect)→ 直接返回 TRUE(无宽度或无高度,退化为空操作;Windows 语义下这是合法空弧)。 - 路径模式 (第 206--219 行):
PATH_IsPathOpen(dc->dclevel)为真时调用:
c
return PATH_Arc(dc, LeftRect, TopRect, RightRect, BottomRect,
XStartArc, YStartArc, XEndArc, YEndArc,
0, // direction=0 → 使用 DC 的 DCPATH_CLOCKWISE 标志
arctype); // lines=arctype
- ArcTo 前置连线 (第 223--229 行):仅
arctype == GdiTypeArcTo:
c
if (dc->dclevel.flPath & DCPATH_CLOCKWISE)
IntGdiLineTo(dc, XEndArc, YEndArc); // 顺时针:先连到"终点"
else
IntGdiLineTo(dc, XStartArc, YStartArc); // 逆时针:先连到"起点"
(IntGdiLineTo 定义于 line.c 第 147 行起,从当前点画线到目标点;路径未打开时直接光栅化,路径打开时走 PATH_LineTo。此处路径未打开,走光栅化分支。)
-
核心绘制 (第 231--240 行):调用
IntArc(dc, LeftRect, TopRect, RightRect, BottomRect, XStartArc, YStartArc, XEndArc, YEndArc, arctype)。 -
ArcTo 后置移点(第 242--248 行):
c
if (arctype == GdiTypeArcTo)
{
if (dc->dclevel.flPath & DCPATH_CLOCKWISE)
IntGdiMoveToEx(dc, XStartArc, YStartArc, NULL); // 顺时针:移到"起点"
else
IntGdiMoveToEx(dc, XEndArc, YEndArc, NULL); // 逆时针:移到"终点"
}
语义讨论(源码现状) :Windows 文档规定 ArcTo 从当前点向弧起点 画线,画完弧后当前点位于弧终点 ,方向由 SetArcDirection 决定(默认 AD_COUNTERCLOCKWISE)。ReactOS 代码在逆时针 分支的行为与之一致(连起点、移到终点);顺时针分支则表现为"连终点、移到起点"(第 225--226、244--245 行)。这可能是有意的 ReactOS 约定,也可能是对"方向标志与坐标语义"的未完全对齐;标注为源码现状,实现时需与 Wine/Windows 行为对照验证。
使用方式 :仅被 NtGdiArcInternal 与 IntGdiAngleArc 调用,是 win32k 内部 API,不暴露给用户态。
注意事项:
- 路径打开时
ArcTo的连线行为由PATH_Arc内的PT_LINETO首段处理(见 5.6 节),IntGdiArcInternal第 223--248 行的连线/移点逻辑只作用于直接绘制模式。 - 第 197 行有一个被注释掉的
PDC_ATTR pdcattr;声明------历史遗留,说明早期版本曾直接访问 DC 属性。
5.3 IntArc(静态核心实现,arc.c 第 25--180 行)
签名:
c
static BOOL FASTCALL IntArc(
DC *dc,
int Left, int Top, int Right, int Bottom, // 边界矩形
int XRadialStart, int YRadialStart, // 起点矢径
int XRadialEnd, int YRadialEnd, // 终点矢径
ARCTYPE arctype);
作用:直接绘制模式的核心:参数预处理(排序/收缩/笔宽)→ 坐标转换 → 角度计算 → 填充+描边 → 补线。
实现流程(按代码顺序):
第 1 步:矩形排序与空检查 (第 48--58 行)。Right < Left 时交换、Bottom < Top 时交换;(Left == Right) || (Top == Bottom) → 返回 TRUE(空弧)。注意这与 IntGdiArcInternal 的空检查重复,属于防御性冗余。
第 2 步:Chord/Pie 的 1 像素特例(第 60--65 行):
c
// FIXME: this needs to be verified
if ((arctype == GdiTypeChord ) || (arctype == GdiTypePie))
{
if ((Right - Left == 1) || (Bottom - Top == 1))
return TRUE;
}
宽度或高度为 1 像素的 Chord/Pie 直接跳过。代码带 FIXME 注释,说明该特例的正确性有待验证(可能是为了避免 1 像素矩形填充时出现退化楔形)。
第 3 步:锁定画笔 (第 68--76 行)。pdcattr = dc->pdcattr;PEN_ShareLockPen(pdcattr->hpen) 按句柄共享锁定当前画笔,失败则 EngSetLastError(ERROR_INTERNAL_ERROR)。画笔属性 lWidth(笔宽)与 ulPenStyle(笔样式)随后被读取。
第 4 步:笔宽处理(第 78--92 行):
c
PenOrigWidth = PenWidth = pbrPen->lWidth;
if (pbrPen->ulPenStyle == PS_NULL) PenWidth = 0; // 空笔 → 宽 0
if (pbrPen->ulPenStyle == PS_INSIDEFRAME) // 内框笔:受矩形约束
{
if (2*PenWidth > (Right - Left)) PenWidth = (Right -Left + 1)/2;
if (2*PenWidth > (Bottom - Top)) PenWidth = (Bottom -Top + 1)/2;
Left += PenWidth / 2; // 矩形向内收缩
Right -= (PenWidth - 1) / 2;
Top += PenWidth / 2;
Bottom -= (PenWidth - 1) / 2;
}
if (!PenWidth) PenWidth = 1; // 空笔也至少 1 宽
pbrPen->lWidth = PenWidth;
PS_NULL:笔宽先置 0,随后第 91 行统一提升为 1(避免 0 宽导致的退化绘制,尽管 PS_NULL 语义上不画)。PenOrigWidth保存原宽,函数结束前恢复(第 176 行)。PS_INSIDEFRAME:笔宽不得超过矩形半尺寸,且矩形按笔宽向内收缩(保证弧线全部落在矩形内)。pbrPen->lWidth = PenWidth是临时改写共享画笔,随后必须恢复------这是共享锁保护的临界区。
第 5 步:坐标转换(第 94--121 行)。构造两个"矩形":
c
RectBounds.left=Left; RectBounds.right=Right; RectBounds.top=Top; RectBounds.bottom=Bottom;
RectSEpts.left=XRadialStart; RectSEpts.top=YRadialStart; RectSEpts.right=XRadialEnd; RectSEpts.bottom=YRadialEnd;
IntLPtoDP(dc, (LPPOINT)&RectBounds, 2); // 逻辑 → 设备
IntLPtoDP(dc, (LPPOINT)&RectSEpts, 2);
RectBounds.left += dc->ptlDCOrig.x; // 加 DC 原点偏移
RectBounds.right += dc->ptlDCOrig.x;
RectBounds.top += dc->ptlDCOrig.y;
RectBounds.bottom += dc->ptlDCOrig.y;
RectSEpts.left += dc->ptlDCOrig.x;
RectSEpts.top += dc->ptlDCOrig.y;
RectSEpts.right += dc->ptlDCOrig.x;
RectSEpts.bottom += dc->ptlDCOrig.y;
设备坐标 = 逻辑坐标经 DC 变换(世界/视图/窗口变换)+ DC 原点(ptlDCOrig)。后续角度计算、填充、描边、补线全部使用设备坐标。
第 6 步:角度计算 (第 123--132 行)。中心点取矩形中心(整数除 2),atan2 求起止角度(度数),并处理"起止同向 → 整圆"边角(见 4.1、4.2 节)。
第 7 步:填充(仅 Chord/Pie) (第 134--144 行)。IntFillArc(dc, Left, Top, Width, Height, AngleStart, AngleEnd, arctype),其中 Width = abs(Right-Left)、Height = abs(Bottom-Top)。
第 8 步:描边 (第 146--157 行)。若填充成功(ret 为真),IntDrawArc(dc, ..., AngleStart, AngleEnd, arctype, pbrPen)。
第 9 步:表面检查 (第 159--166 行)。psurf = dc->dclevel.pSurface 为空 → "Arc Fail 2",解锁画笔、EngSetLastError(ERROR_INTERNAL_ERROR)。注意该检查放在绘制之后 ,属于收尾防御(实际绘制时表面早已由 DC_vPrepareDCsForBlit 保证)。
第 10 步:补线 (第 168--174 行)。Pie 画两条径向线、Chord 画一条弦(见 4.4 节),均用 PUTLINE 宏 + dc->eboLine(当前线条刷)。
第 11 步:恢复与返回 (第 176--179 行)。pbrPen->lWidth = PenOrigWidth 恢复笔宽,PEN_ShareUnlockPen(pbrPen) 解锁,返回 ret。
使用方式 :仅被 IntGdiArcInternal(第 231 行)调用一次;静态函数,模块私有。
注意事项:
IntArc内临时修改共享画笔lWidth的写法在并发场景依赖PEN_ShareLockPen的排他语义;若恢复路径提前 return(如第 163--165 行表面失败分支)也能正确解锁,但该分支不会恢复lWidth(因为原宽未变,仅在 INSIDEFRAME 收缩时才会改写------该分支发生在收尾,安全性依赖"提前 return 前 lWidth 未被改写"的隐含约定,值得留意)。atan2的 y 参数用的是RectSEpts.top - CenterY(起点)与RectSEpts.bottom - CenterY(终点),注意起终点与 left/top、right/bottom 的对应关系:left/top 属于起点 ,right/bottom 属于终点。- 空笔
PS_NULL被强制为宽 1(第 79、91 行)后,IntDrawArc仍会描边------这与"空笔不画"的 Windows 语义可能有出入,属于源码现状。
5.4 NtGdiAngleArc(系统调用入口,arc.c 第 299--343 行)
签名:
c
BOOL APIENTRY NtGdiAngleArc(
IN HDC hDC,
IN INT x, IN INT y, // 圆心
IN DWORD dwRadius, // 半径
IN DWORD dwStartAngle, // 起始绝对角度(FLOAT 位模式)
IN DWORD dwSweepAngle); // 扫过角(FLOAT 位模式,正=逆时针,负=顺时针)
系统调用绑定(win32u.spec 第 16 行):
@ stdcall NtGdiAngleArc(ptr long long long long long)
FLOAT 位模式传递 (关键设计):gdi32 的 AngleArc(objects/arc.c 第 47--75 行)接收 FLOAT eStartAngle, eSweepAngle,直接按位强转:
c
return NtGdiAngleArc(hdc, x, y, dwRadius,
RCAST(DWORD, eStartAngle),
RCAST(DWORD, eSweepAngle));
RCAST(DWORD, float) 保持 IEEE 754 位模式不变(等价于 memcpy),避免"FLOAT→double→DWORD"转换链的精度损失。内核侧用联合体恢复:
c
gxf_long worker, worker1; // 位模式转换联合体(DWORD l / FLOAT f)
...
worker.l = dwStartAngle;
worker1.l = dwSweepAngle;
...
Ret = IntGdiAngleArc(pDC, x, y, dwRadius, worker.f, worker1.f);
实现流程:
- 锁 DC (第 315--320 行):失败 →
ERROR_INVALID_HANDLE。 - 浮点保护 (第 322--327 行):
KeSaveFloatingPointState失败 → 解锁返回 FALSE。 - 位模式转换 (第 329--330 行):
worker.l = dwStartAngle; worker1.l = dwSweepAngle;(之后以.f读出 FLOAT)。 - 准备 DC 与刷新刷子 (第 331--335 行):
DC_vPrepareDCsForBlit+ 条件更新填充刷/线条刷(与NtGdiArcInternal相同的三段式)。 - 委托 (第 336 行):
IntGdiAngleArc(pDC, x, y, dwRadius, worker.f, worker1.f)。 - 收尾 (第 337--342 行):
DC_vFinishBlit、DC_UnlockDc、KeRestoreFloatingPointState。
注意事项:
- 参数名为
dwStartAngle/dwSweepAngle但实际承载 FLOAT 位模式,阅读时不要被 DWORD 类型误导。 - 半径
dwRadius是 DWORD(无符号),用户态传负半径会变成很大的正数,gdi32 未做钳制(Windows 行为是忽略该调用)。
5.5 IntGdiAngleArc(AngleArc 内部实现,arc.c 第 252--295 行)
签名:
c
BOOL FASTCALL IntGdiAngleArc(
PDC pDC,
INT x, INT y, // 圆心(逻辑坐标)
DWORD dwRadius, // 半径
FLOAT eStartAngle, // 起始绝对角度(度)
FLOAT eSweepAngle); // 扫过角(度,正=逆时针)
作用 :把 AngleArc 的"绝对角度+半径"参数化为"矩形+矢径端点",设置弧方向,再委托 IntGdiArcInternal(GdiTypeArcTo, ...) 完成"连线→画弧→移点"的完整语义。
实现流程:
- 计算弧终点(第 264--266 行):
c
x2 = x + (INT)(cos(((eStartAngle+eSweepAngle)/360)*(M_PI*2)) * dwRadius);
y2 = y - (INT)(sin(((eStartAngle+eSweepAngle)/360)*(M_PI*2)) * dwRadius);
弧终点方向角 = 起始角 + 扫过角;y 取负是因为屏幕坐标系 Y 向下(数学角的逆时针在屏幕上表现为顺时针,取负后角度增大方向变为屏幕顺时针,与 AngleArc 文档一致)。
-
计算弧起点 (第 268--269 行):同上,但用
eStartAngle。 -
设置弧方向(第 271--275 行):
c
arcdir = pDC->dclevel.flPath & DCPATH_CLOCKWISE; // 保存原方向
if (eSweepAngle >= 0)
pDC->dclevel.flPath &= ~DCPATH_CLOCKWISE; // 正扫 → 逆时针
else
pDC->dclevel.flPath |= DCPATH_CLOCKWISE; // 负扫 → 顺时针
这与 Windows 语义一致:eSweepAngle > 0 逆时针、< 0 顺时针、= 0 不画。
- 构造矩形并委托 (第 277--286 行):边界矩形为圆心 ± 半径(
x-dwRadius, y-dwRadius, x+dwRadius, y+dwRadius),矢径端点用计算出的 (x1,y1)/(x2,y2),类型为GdiTypeArcTo:
c
result = IntGdiArcInternal(GdiTypeArcTo, pDC,
x-dwRadius, y-dwRadius, x+dwRadius, y+dwRadius,
x1, y1, x2, y2);
进入 IntGdiArcInternal 后(路径未打开时):逆时针分支先 IntGdiLineTo(x1,y1)(当前点连到弧起点)→ IntArc 画弧 → IntGdiMoveToEx(x2,y2)(当前点移到弧终点)------完整复现 AngleArc 文档语义。
-
恢复方向标志 (第 288 行):
pDC->dclevel.flPath |= (arcdir & DCPATH_CLOCKWISE);。 -
最终移点 (第 290--293 行):成功时
IntGdiMoveToEx(pDC, x2, y2, NULL)(与第 4 步内层移点重复,幂等)。
注意事项:
- 端点计算用
(INT)截断浮点结果,半径较小或角度非整时存在 1 像素取整误差。 - 方向恢复用
|=而非赋值:若原方向为逆时针(无 CLOCKWISE 位)且本次为负扫角(置位),|= 0无法清回原位------DC 的 CLOCKWISE 位残留为"顺时针"。该残留会影响后续依赖该标志的弧/路径调用(源码现状,疑似边角缺陷,见第 8 节)。 - 该函数没有自己的浮点保护(
cos/sin调用);浮点保护由上层NtGdiAngleArc完成,因此只能作为 win32k 内部函数使用。
5.6 PATH_Arc(路径模式下的弧,path.c 第 963--1137 行)
签名:
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, // AD_CLOCKWISE(2)/AD_COUNTERCLOCKWISE(1)/0=默认
INT lines); // 实际为 ARCTYPE
作用 :路径已打开时,把一条椭圆弧拆成最多 5 段贝塞尔曲线写入路径,并按 lines(ARCTYPE)决定闭合/连线方式。
实现流程:
- 锁路径 (第 993--994 行):
PATH_LockPath(dc->dclevel.hPath),失败返回 FALSE。 - 方向判定 (第 996--999 行):
direction非 0 时按AD_CLOCKWISE判定;为 0 时读dc->dclevel.flPath & DCPATH_CLOCKWISE。arc.c 调用时传 0,即采用 DC 当前弧方向。 - 零宽高检查 (第 1002--1007 行):
x1 == x2 || y1 == y2→ 直接 ArcExit(返回 TRUE)。注释标注"FIXME: Only in GM_COMPATIBLE?"。 - 坐标转换 (第 1009--1015 行):
INTERNAL_LPTODP(dc, corners, 2)转换矩形两角点,pointStart/pointEnd各转一次。 - 角点排序 (第 1018--1029 行):保证
corners[0]为左上、corners[1]为右下。 - 求起止角 (第 1032--1035 行):
PATH_NormalizePoint+atan2(见 4.1/4.3 节)。 - 角度区间调整 (第 1038--1053 行):按方向把
angleEnd挪到起点的正确一侧(见 4.2 节)。 - GM_COMPATIBLE 修正 (第 1056--1060 行):兼容模式下
corners[1].x--; corners[1].y--;------排除 right/bottom 边界,使弧与 Win9x 行为一致。 - arcto 首点 (第 1063--1073 行):
lines == GdiTypeArcTo && pPath->newStroke时,读取 DC 当前点(IntGetCurrentPositionEx+CoordLPtoDP转设备坐标),以PT_MOVETO加入路径(保证"当前点→弧起点"连线的起点正确),并清除newStroke。 - 象限分段循环 (第 1077--1116 行):每段 ≤ 90°,调用
PATH_DoArcPart(见 4.2、5.7 节)。 - 闭合处理 (第 1118--1133 行):ArcTo →
update_current_pos(路径当前点 = 弧终点);Chord →IntGdiCloseFigure;Pie → 加中心点, PT_LINETO | PT_CLOSEFIGURE(见 4.4 节)。 - 解锁路径(第 1134--1136 行)。
使用方式 :仅被 IntGdiArcInternal(路径分支)调用;direction 固定传 0,lines 传 ARCTYPE。也可被其他内核模块直接调用(带显式 direction)。
注意事项:
x1,y1,x2,y2参数在排序前后语义变化:排序后corners[0]是左上角、corners[1]是右下角。- 第 988--989 行有两处 FIXME:未检查所有可能的错误返回、未处理
newStroke的某些状态。 - 若弧跨多个象限,最多 5 段贝塞尔(首段可能不足 90° + 4 个整象限)------与注释"up to five Bezier splines"吻合。
5.7 PATH_DoArcPart(单段贝塞尔弧段,path.c 第 898--951 行)
签名:
c
static BOOL PATH_DoArcPart(
PPATH pPath,
POINT corners[], // [左上, 右下] 边界矩形
double angleStart, // 起始角(弧度)
double angleEnd, // 结束角(弧度),|angleEnd - angleStart| ≤ π/2
BYTE startEntryType); // 非 0:首控制点改用该类型(PT_MOVETO/PT_LINETO);0:首点已存在
作用:生成逼近圆弧的一段三次贝塞尔样条并追加到路径(数学推导见 4.3 节)。
实现流程:
- 断言 (第 913 行):
ASSERT(fabs(angleEnd - angleStart) <= M_PI_2)------调用者保证每段不超过 90°。 - 计算控制点 (第 917--936 行):系数
a = 4/3·(1-cos h)/sin h;4 个归一化控制点xNorm[]/yNorm[];半角过小则四点重合。 - 追加点 (第 938--948 行):
start = !startEntryType;对i = start..3用PATH_ScaleNormalizedPoint映射到设备坐标;add_points(pPath, points+start, 4-start, PT_BEZIERTO)追加(贝塞尔段三点为一组:首点已在路径上时只需追加 3 个控制点);首点类型改写为startEntryType。 - 返回:成功 TRUE,内存不足 FALSE。
使用方式 :仅被 PATH_Arc 调用(第 1109 行),属于 path.c 内部辅助。
注意事项:
add_points追加的是设备坐标点(调用者已转换),不再做 LPtoDP。- 首段(
startEntryType == PT_MOVETO或PT_LINETO)追加 4 个点(MOVETO + 3 控制点);后续段只追加 3 个点(3 控制点,曲线自动延续)。 - 该函数是"弧→路径"精确性的保证:90° 分段 + 标准贝塞尔系数,误差远小于肉眼可辨(对四分之一圆,相对径向误差约 2.7e-4)。
5.8 IntFillArc / IntDrawArc(drawing.c 第 1304--1360 行)
这两个函数不在 arc.c 中,但由 IntArc 直接调用,是弧绘制的光栅化委托目标,声明于 intgdi.h 第 138--139 行:
c
BOOL FASTCALL IntFillArc( PDC dc, INT XLeft, INT YLeft, INT Width, INT Height,
double StartArc, double EndArc, ARCTYPE arctype);
BOOL FASTCALL IntDrawArc( PDC dc, INT XLeft, INT YLeft, INT Width, INT Height,
double StartArc, double EndArc, ARCTYPE arctype, PBRUSH pbrush);
IntFillArc(drawing.c 第 1304--1338 行):
- 锁定填充刷
BRUSH_ShareLockBrush(pdcattr->hbrush),失败 →ERROR_INTERNAL_ERROR。 Start = (int)ceil(StartArc); End = (int)ceil(EndArc);------度数向上取整为整数。Chord = (arctype == GdiTypeChord)。- 调用
app_fill_arc:
c
ret = app_fill_arc(dc, rect(XLeft, YLeft, Width, Height),
(dc->dclevel.flPath & DCPATH_CLOCKWISE) ? -End : -Start,
(dc->dclevel.flPath & DCPATH_CLOCKWISE) ? -Start : -End,
pbrush, Chord);
注意两点:一是角度取负(app 层以"顺时针为正"的度数解释,负号完成方向换算);二是按方向交换起止(保证区间方向与 app 层扫描方向一致)。
- 解锁刷子,返回。
IntDrawArc(drawing.c 第 1340--1360 行) :结构与 IntFillArc 完全相同,但使用 app_draw_arc 且画笔由调用者传入(pbrush),不自行锁定。app_draw_arc 用外椭圆(半径 a,b)+ 内椭圆(半径 a-笔宽, b-笔宽)的环形区域逐段输出带笔宽的弧线(见 4.5 节)。
注意事项 :StartArc/EndArc 是度数 double,ceil 取整意味着角度在 (n, n+1] 区间被当作 n+1,1 度级取整误差;注释 // FIXME: don't use floating point! 表明作者希望未来用定点数替代。
5.9 文件级宏 PUTPIXEL / PUTLINE(arc.c 第 6--23 行)
c
#define PUTPIXEL(x,y,BrushInst) \
ret = ret && IntEngLineTo(&psurf->SurfObj, \
(CLIPOBJ *)&dc->co, \
&BrushInst.BrushObject, \
x, y, (x)+1, y, \
&RectBounds, \
ROP2_TO_MIX(pdcattr->jROP2));
#define PUTLINE(x1,y1,x2,y2,BrushInst) \
ret = ret && IntEngLineTo(&psurf->SurfObj, \
(CLIPOBJ *)&dc->co, \
&BrushInst.BrushObject, \
x1, y1, x2, y2, \
&RectBounds, \
ROP2_TO_MIX(pdcattr->jROP2));
IntEngLineTo:win32k 内部画线原语(surface 对象 + 裁剪对象 + 画笔刷 + 端点 + 边界矩形 + ROP 混合模式)。PUTPIXEL:画 1×1 单位线段(x,y → x+1,y),相当于单像素点。PUTLINE:画任意端点线段。- 两者都用
ret = ret && ...累积结果:任一画线失败都会把整个IntArc的返回值置为 FALSE(但不会中断后续补线,这是"尽力而为"的容错风格)。 - 两个宏都依赖外围作用域中的
psurf、dc、RectBounds、pdcattr、ret变量,只能在IntArc内部使用(闭包式宏)。
6. 调用链(mermaid 时序)
以下 mermaid 图展示 Arc/Pie 与 AngleArc 两条完整调用链(含路径模式分支):
#mermaid-svg-1WavOVIW5y2nujL8{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-1WavOVIW5y2nujL8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1WavOVIW5y2nujL8 .error-icon{fill:#552222;}#mermaid-svg-1WavOVIW5y2nujL8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1WavOVIW5y2nujL8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1WavOVIW5y2nujL8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1WavOVIW5y2nujL8 .marker.cross{stroke:#333333;}#mermaid-svg-1WavOVIW5y2nujL8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1WavOVIW5y2nujL8 p{margin:0;}#mermaid-svg-1WavOVIW5y2nujL8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1WavOVIW5y2nujL8 .cluster-label text{fill:#333;}#mermaid-svg-1WavOVIW5y2nujL8 .cluster-label span{color:#333;}#mermaid-svg-1WavOVIW5y2nujL8 .cluster-label span p{background-color:transparent;}#mermaid-svg-1WavOVIW5y2nujL8 .label text,#mermaid-svg-1WavOVIW5y2nujL8 span{fill:#333;color:#333;}#mermaid-svg-1WavOVIW5y2nujL8 .node rect,#mermaid-svg-1WavOVIW5y2nujL8 .node circle,#mermaid-svg-1WavOVIW5y2nujL8 .node ellipse,#mermaid-svg-1WavOVIW5y2nujL8 .node polygon,#mermaid-svg-1WavOVIW5y2nujL8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1WavOVIW5y2nujL8 .rough-node .label text,#mermaid-svg-1WavOVIW5y2nujL8 .node .label text,#mermaid-svg-1WavOVIW5y2nujL8 .image-shape .label,#mermaid-svg-1WavOVIW5y2nujL8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1WavOVIW5y2nujL8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1WavOVIW5y2nujL8 .rough-node .label,#mermaid-svg-1WavOVIW5y2nujL8 .node .label,#mermaid-svg-1WavOVIW5y2nujL8 .image-shape .label,#mermaid-svg-1WavOVIW5y2nujL8 .icon-shape .label{text-align:center;}#mermaid-svg-1WavOVIW5y2nujL8 .node.clickable{cursor:pointer;}#mermaid-svg-1WavOVIW5y2nujL8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1WavOVIW5y2nujL8 .arrowheadPath{fill:#333333;}#mermaid-svg-1WavOVIW5y2nujL8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1WavOVIW5y2nujL8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1WavOVIW5y2nujL8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1WavOVIW5y2nujL8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1WavOVIW5y2nujL8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1WavOVIW5y2nujL8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1WavOVIW5y2nujL8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1WavOVIW5y2nujL8 .cluster text{fill:#333;}#mermaid-svg-1WavOVIW5y2nujL8 .cluster span{color:#333;}#mermaid-svg-1WavOVIW5y2nujL8 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-1WavOVIW5y2nujL8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1WavOVIW5y2nujL8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1WavOVIW5y2nujL8 .icon-shape,#mermaid-svg-1WavOVIW5y2nujL8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1WavOVIW5y2nujL8 .icon-shape p,#mermaid-svg-1WavOVIW5y2nujL8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1WavOVIW5y2nujL8 .icon-shape .label rect,#mermaid-svg-1WavOVIW5y2nujL8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1WavOVIW5y2nujL8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1WavOVIW5y2nujL8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1WavOVIW5y2nujL8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内核 win32k
用户态
NtGdiArcInternal syscall
NtGdiAngleArc syscall
worker.f 位模式还原
cos/sin 求端点 + 设方向
路径打开?
是
否
Chord/Pie 闭合
Chord/Pie
Pie/Chord 补线
ArcTo 连线
应用调用 Arc/Pie/Chord/ArcTo
gdi32 objects/arc.c
应用调用 AngleArc
gdi32 AngleArc
win32u.dll
NtGdiArcInternal(arc.c:345)
NtGdiAngleArc(arc.c:299)
IntGdiAngleArc(arc.c:252)
IntGdiArcInternal(arc.c:183)
PathIsPathOpen
PATH_Arc(path.c:963)
IntArc(arc.c:25)
PATH_DoArcPart(path.c:898)
PATH_ScaleNormalizedPoint / add_points
IntGdiCloseFigure / PATH_AddEntry
IntFillArc(drawing.c:1304)
IntDrawArc(drawing.c:1340)
app_fill_arc(drawing.c:785)
app_draw_arc(drawing.c:962)
PUTLINE → IntEngLineTo
IntGdiLineTo / IntGdiMoveToEx(line.c)
调用链要点:
- Arc 族(Arc/ArcTo/Chord/Pie) :gdi32 包装(
HANDLE_METADC/HANDLE_EMETAFDC元文件处理 +GdiConvertAndCheckDC校验)→NtGdiArcInternal(arctype, ...)(经 win32u)→IntGdiArcInternal→ 路径模式PATH_Arc或直接模式IntArc。 - AngleArc :gdi32 把两个 FLOAT 按位强转 DWORD →
NtGdiAngleArc→ 位模式还原为 FLOAT →IntGdiAngleArc(算端点、设方向)→IntGdiArcInternal(GdiTypeArcTo, ...),随后复用 Arc 族管线。 - 路径模式 下
PATH_Arc每 90° 一段调用PATH_DoArcPart,最终点写入路径后由上层StrokePath/FillPath统一渲染。 - 直接模式 下
IntArc依次完成:填充(仅 Chord/Pie)→ 描边 → 补线,全部在设备坐标上运行。
时序示例(Pie,直接绘制模式):
gdi32 Pie() → NtGdiArcInternal(GdiTypePie, hdc, rect, start, end)
→ DC_LockDc / DC_vPrepareDCsForBlit / KeSaveFloatingPointState
→ IntGdiArcInternal(GdiTypePie, dc, ...)
→ (路径未打开)
→ IntArc(dc, rect, start, end, GdiTypePie)
├─ 矩形排序 → 空检查 → 1px 特例 → PEN_ShareLockPen
├─ 笔宽处理(PS_NULL / PS_INSIDEFRAME)
├─ IntLPtoDP + ptlDCOrig → 设备坐标
├─ atan2 → AngleStart/AngleEnd → (+360 整圆边角)
├─ IntFillArc(app_fill_arc 填充扇形)
├─ IntDrawArc(app_draw_arc 描边)
├─ PUTLINE(中心,起点) + PUTLINE(终点,中心) ← 两条径向线
└─ 恢复笔宽 / PEN_ShareUnlockPen
→ KeRestoreFloatingPointState / DC_vFinishBlit / DC_UnlockDc
时序示例(AngleArc,正扫过角):
gdi32 AngleArc(x, y, r, start, sweep) // sweep > 0
→ NtGdiAngleArc(位模式)
→ IntGdiAngleArc: 计算 (x1,y1) 起点、(x2,y2) 终点;清 DCPATH_CLOCKWISE(逆时针)
→ IntGdiArcInternal(GdiTypeArcTo, ...)
→ IntGdiLineTo(x1, y1) // 当前点 → 弧起点
→ IntArc(画弧)
→ IntGdiMoveToEx(x2, y2) // 当前点 → 弧终点
→ IntGdiMoveToEx(x2, y2) // 再次确认(幂等)
→ 恢复方向标志
7. 与相关模块的关系
| 模块/文件 | 关系 | 说明 |
|---|---|---|
win32ss/gdi/ntgdi/path.c |
下游(路径模式) | PATH_Arc/PATH_DoArcPart 生成弧的贝塞尔路径;IntGdiCloseFigure 闭合 Chord;PATH_AddEntry 追加 Pie 中心点 |
win32ss/gdi/ntgdi/drawing.c |
下游(直接绘制) | IntFillArc/IntDrawArc 委托 app_fill_arc/app_draw_arc(中点椭圆扫描 + 楔形裁剪) |
win32ss/gdi/ntgdi/line.c |
旁路 | IntGdiLineTo/IntGdiMoveToEx/IntGetCurrentPositionEx 供 ArcTo 连线、AngleArc 移点、PATH_Arc 取当前点 |
win32ss/gdi/ntgdi/dcutil.c |
旁路 | DC_SetArcDirection 维护 DCPATH_CLOCKWISE 标志,弧方向的数据来源 |
win32ss/gdi/ntgdi/intgdi.h |
接口 | IntFillArc/IntDrawArc/IntGdiArcInternal 等内部 API 声明(138--139 行等) |
win32ss/include/ntgdityp.h |
类型 | ARCTYPE 枚举定义(18--24 行) |
win32ss/gdi/gdi32/objects/arc.c |
上游 | Arc/ArcTo/Chord/Pie/AngleArc 用户态包装,元文件处理与参数透传 |
win32ss/win32u/win32u.spec |
绑定 | NtGdiArcInternal/NtGdiAngleArc 系统调用签名(第 16、19 行) |
win32ss/gdi/ntgdi/fillshap.c |
姊妹模块 | 椭圆/圆角矩形实现(《分析_42》),与弧共用 app_* 光栅函数 |
数据流总览:
ARCTYPE + 矩形 + 矢径 ──→ 角度(atan2)──→ 填充(app_fill_arc)/ 描边(app_draw_arc)
└──→ 路径(PATH_Arc → PATH_DoArcPart → add_points)
与《分析_31》的关系:该分册 2.2 节"ntgdi 函数总表"中列出的 arc.c 各函数,即本文档第 5 节逐一展开的内容。
8. 注意事项与已知问题
以下问题均基于当前源码(2026 年 8 月)的观察,标注了疑点与改进方向,不代表全部已被确认。
-
ArcTo 顺时针分支的连线/移点方向 (arc.c 第 223--248 行):Windows 语义是"从当前点连到弧起点,画完移到弧终点"。ReactOS 的逆时针 分支符合该语义;顺时针分支反之为"连终点、移到起点"。若应用依赖顺时针 ArcTo 的标准行为,可能出现连线方向差异。建议对照 Wine/Windows 验证后决定是否调整。
-
IntGdiAngleArc 的方向标志恢复缺陷 (arc.c 第 288 行):
pDC->dclevel.flPath |= (arcdir & DCPATH_CLOCKWISE)用"或"恢复。当原方向为逆时针(CLOCKWISE 位为 0)而本次传负扫过角(置位)后,|= 0无法清除残留位,DC 方向被永久改为顺时针。同理,原方向为顺时针而本次正扫(清位)时同样无法恢复。只有"原为顺时针且本次也置位"或"原为逆时针且本次也清位"两种组合能正确恢复。建议改为"先清位再按原值或入"。 -
PS_NULL 空笔仍以宽 1 描边 (arc.c 第 79、91 行):空笔逻辑上不应绘制,但
PenWidth被强制提升为 1 后IntDrawArc仍会描边。这与"空笔不画"的直觉语义有出入,需与 Windows 行为核对。 -
IntArc 对共享画笔 lWidth 的临时改写 (arc.c 第 78--92、176 行):
pbrPen->lWidth = PenWidth临时改写共享画笔对象,靠PEN_ShareLockPen的锁保护;若未来并发访问路径变化(如多线程共享画笔),存在竞态隐患。此外,若第 9 步表面检查失败提前返回,lWidth未被恢复(该分支在绘制之后,实际改写已经发生------不过 INSIDEFRAME 的收缩只影响局部变量 Left/Right/Top/Bottom,lWidth改写本身在原宽基础上进行,提前返回时 lWidth 值已在绘制中定型,恢复与否影响后续绘制)。 -
角度取整 :
IntFillArc/IntDrawArc用(int)ceil(角度)取整(drawing.c 第 1317--1318、1352--1353 行),存在 1 度内的量化误差;IntGdiAngleArc端点用(INT)截断,有亚像素误差。两者均有 FIXME 注释计划改用定点数。 -
1 像素 Chord/Pie 直接跳过(arc.c 第 60--65 行):带 FIXME,需验证与 Windows 的一致性。
-
PATH_Arc 的 FIXME (path.c 第 988--989、1002 行):未检查所有可能的错误返回;
newStroke的某些状态未处理;零宽高检查是否仅限 GM_COMPATIBLE 未定。 -
注释过期 :PATH_Arc 的
lines参数注释("-1/1/2")与实际 ARCTYPE 值(0--3)不符;NtGdiAngleArc的DWORD参数名易误导(实际承载 FLOAT 位模式)。阅读源码时需以上下文为准。 -
浮点依赖 :整个模块依赖
atan2/cos/sin及M_PI常量;内核浮点上下文由两个 NtGdi 入口显式保护,任何新增内部调用路径(如未来从其他内核模块直接调用IntGdiAngleArc)都必须自带KeSaveFloatingPointState,否则可能产生随机浮点故障。 -
GM_COMPATIBLE 的边界排除 (path.c 第 1056--1060 行):路径模式下兼容图形模式排除右下边界(
corners[1].x--/corners[1].y--),直接模式(IntArc)未见对应处理------两条路径在 GM_COMPATIBLE 下对边界像素的取舍可能不一致,值得回归测试关注。
9. 源码索引
| 文件 | 关键内容 | 位置 |
|---|---|---|
win32ss/gdi/ntgdi/arc.c |
PUTPIXEL/PUTLINE 宏 | 9--23 |
win32ss/gdi/ntgdi/arc.c |
IntArc |
25--180 |
win32ss/gdi/ntgdi/arc.c |
IntGdiArcInternal |
183--250 |
win32ss/gdi/ntgdi/arc.c |
IntGdiAngleArc |
252--295 |
win32ss/gdi/ntgdi/arc.c |
NtGdiAngleArc |
299--343 |
win32ss/gdi/ntgdi/arc.c |
NtGdiArcInternal |
345--408 |
win32ss/gdi/ntgdi/path.c |
PATH_DoArcPart(贝塞尔弧段) |
898--951 |
win32ss/gdi/ntgdi/path.c |
PATH_Arc(路径弧) |
963--1137 |
win32ss/gdi/ntgdi/path.c |
PATH_ScaleNormalizedPoint |
360--391 |
win32ss/gdi/ntgdi/path.c |
PATH_NormalizePoint |
398--424 |
win32ss/gdi/ntgdi/path.c |
add_points / PATH_AddEntry |
489--500 / 260--286 |
win32ss/gdi/ntgdi/path.c |
IntGdiCloseFigure |
106--119 |
win32ss/gdi/ntgdi/path.h |
PATH 结构、DCPATH_* 标志、PATH_IsPathOpen |
4--72 |
win32ss/include/ntgdityp.h |
ARCTYPE 枚举 |
18--24 |
win32ss/gdi/ntgdi/drawing.c |
app_fill_arc / app_draw_arc |
783--960 / 962-- |
win32ss/gdi/ntgdi/drawing.c |
IntFillArc / IntDrawArc |
1304--1360 |
win32ss/gdi/ntgdi/intgdi.h |
内部 API 声明 | 138--139 |
win32ss/gdi/ntgdi/line.c |
IntGdiLineTo / IntGdiMoveToEx |
80--107 / 147-- |
win32ss/gdi/ntgdi/dcutil.c |
DC_SetArcDirection(AD_CLOCKWISE 维护) |
495--507、630--655 |
win32ss/gdi/gdi32/objects/arc.c |
Arc/ArcTo/Chord/Pie/AngleArc 包装 |
3--199 |
win32ss/win32u/win32u.spec |
系统调用签名 | 16、19 |
sdk/include/psdk/wingdi.h |
AD_CLOCKWISE=2 / AD_COUNTERCLOCKWISE=1 |
667--668 |
术语速查:
| 术语 | 含义 |
|---|---|
| 矢径(radial) | 从椭圆中心到端点的射线;弧的起止点由矢径方向与椭圆交点确定 |
| 扫过角(sweep angle) | AngleArc 中弧的角跨度;正 = 逆时针、负 = 顺时针 |
| 楔形(wedge) | Pie/Chord 填充区域(弧与弦/径向线围成) |
| 中点椭圆算法 | Bresenham 风格整数椭圆步进(e(x,y) = b²x²+a²y²-a²b²) |
| 贝塞尔近似 | 用三次贝塞尔曲线逼近圆弧,90° 分段,系数 a = 4/3·tan(h/2) |
| 位模式传递 | gdi32 与 win32k 之间用 DWORD 无损搬运 FLOAT(IEEE 754 位模式) |
结语
arc.c 以 408 行的体量完整覆盖了 Windows GDI 弧线绘制的五种 API,其核心设计可概括为三句话:
- 一个入口 :
NtGdiArcInternal(ARCTYPE, ...)统一 Arc/ArcTo/Chord/Pie,用枚举区分闭合语义; - 两条路径 :路径打开走贝塞尔弧段(
PATH_Arc),否则走填充+描边直接光栅化(IntArc); - 一个复用 :
AngleArc通过GdiTypeArcTo分支复用"连线+画弧+移点"的完整管线,只额外负责角度参数化与方向设置。
弧的几何本质------"矩形 + 起止矢径 → atan2 角度 → 按方向扫过 → 90° 分段贝塞尔或扫描线光栅化"------是理解本模块(以及 path.c、drawing.c 相关部分)的钥匙。文中标注的若干 FIXME 与源码现状疑点(ArcTo 顺时针方向、方向标志恢复、空笔宽 1 等)可作为后续修复与测试的切入点。
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/arc.c及关联模块分析(2026 年 8 月)