ReactOS 图形系统分析(48):弧线绘制 — arc.c

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(系统调用签名)。


目录

  1. 概述(架构图)
  2. 设计动机
  3. 核心数据结构
    • 3.1 ARCTYPE 枚举
    • 3.2 弧的参数化:矩形 + 起止矢径 + 方向
    • 3.3 DC 路径状态与路径弧段
  4. 弧几何算法详解
    • 4.1 起点矢径 → 角度(atan2)
    • 4.2 角度区间处理(整圆边角 / 象限分段)
    • 4.3 贝塞尔曲线近似圆弧
    • 4.4 ARC / CHORD / PIE 三种类型的绘制差异
    • 4.5 填充与描边算法概览
  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
  6. 调用链(mermaid 时序)
  7. 与相关模块的关系
  8. 注意事项与已知问题
  9. 源码索引

1. 概述(架构图)

arc.c 是 win32k(内核态 GDI)中负责弧线绘制 的模块,实现了 Windows 的 ArcArcToChordPieAngleArc 五个 API 的内核部分。整个模块只包含两个系统调用入口(NtGdiArcInternalNtGdiAngleArc)和三个内部函数(IntGdiArcInternalIntGdiAngleArc、静态的 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)

从图中可以看出核心设计:弧线绘制分两条路线

  1. 路径模式BeginPath 已打开路径):Arc/ArcTo/Chord/Pie 全部转成路径操作 PATH_Arc,把椭圆弧分解为若干个贝塞尔样条段写入路径,之后由统一的路径描边/填充管线输出。
  2. 直接绘制模式 (路径未打开):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 在本版本源码中并不存在 (已在全仓库检索确认无此标识符);角度计算内联在 IntArcatan2)与 PATH_Arcatan2 + PATH_NormalizePoint)中,属于纯函数式辅助而非独立函数。


2. 设计动机

理解 arc.c 的设计,需要先回顾 Windows GDI 对"弧"的定义方式与 ReactOS 的实现取舍:

  1. API 几何语义的一致性 。Windows 的 Arc/ArcTo/Chord/Pie 共用同一套参数:一个边界矩形(Left, Top, Right, Bottom)加上两个"径向端点"(XStartArc, YStartArc, XEndArc, YEndArc)。弧本身是矩形的内切椭圆的一部分,由"椭圆中心到两个径向端点的射线"夹出角度区间。因此把四个 API 合并为一个内核入口 NtGdiArcInternal(ARCTYPE, ...) 并用枚举区分行为,是最自然的设计------这也正是 arc.c 的结构。

  2. 两条执行路径的复用 。路径(path)是 GDI 的核心抽象:BeginPath 之后,所有绘制图元都记录为路径点序列,可以统一描边(StrokePath)或填充(FillPath)。ReactOS 的做法是:路径打开时把弧翻译成贝塞尔弧段(PATH_Arc),路径未打开时走"填充+描边"直接光栅化(IntArc)。这样同一个 NtGdiArcInternal 在不同上下文中给出一致结果,代码只分叉一次。

  3. 浮点只在需要的地方出现 。弧的角度计算离不开 atan2/cos/sin(椭圆参数方程),而 win32k 是内核模块:内核默认不保存浮点状态,使用浮点寄存器前必须 KeSaveFloatingPointState/KeRestoreFloatingPointState。因此两个系统调用入口都显式包住浮点区域,内部函数则尽量用整数运算(如 IntArc 中矩形中心、PATH_ScaleNormalizedPoint 的 FLOATOBJ 定点模拟浮点)。

  4. AngleArc 的特殊语义AngleArc("角弧")与 Arc 族不同:它不依赖"径向端点",而是直接给圆心、半径、起始绝对角度和扫过角,且绘制前先画一条"当前点到弧起点"的线段,绘制后当前点位于弧终点。ReactOS 通过复用 GdiTypeArcTo 分支自然获得"连线 + 移点"的行为,再补上角度参数化转换(cos/sin 求端点)与方向设置。

  5. 效率与正确的平衡 。填充弧楔形(Pie/Chord 的面)由 drawing.c 的 app_fill_arc 用"中点椭圆扫描 + 扫描线楔形裁剪"完成,纯整数 Bresenham 风格,避免逐点调用昂贵的光栅接口;描边则用内外两个椭圆(笔宽差)求环形区域。这些算法与 drawing.c 中的椭圆(IntFillEllipse/IntDrawEllipse)共用,体现了 ntgdi 光栅代码的模块化复用。

  6. 与 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 ArcToAngleArc(内部) 从当前点连线到弧起点,画弧,当前点移到弧终点
GdiTypeChord 2 Chord 弧 + 连接弧两端点的弦(封闭图形)
GdiTypePie 3 Pie 弧 + 圆心到两端点的两条径向线(扇形封闭图形)

几点说明:

  • 该枚举同时也被当作 PATH_Arc 的 lines 参数语义使用(见 5.6 节)。PATH_Arc 的注释里写着 "lines 为 1 时加一条线得弦、为 2 时加两条线得饼、为 -1 时先连线再画弧(arcto)",这是从 Wine 移植时遗留的旧数值注释;实际调用中传入的就是 ARCTYPE 枚举值(GdiTypeArcTo == 1GdiTypeChord == 2GdiTypePie == 3),代码内部用枚举常量比较,与注释的绝对值不再一一对应。这是"注释过期"的典型例子,阅读时不要被注释误导。
  • NtGdiArcInternalarctype > GdiTypePie 做合法性校验(见 5.1 节),说明该参数只接受 0--3。
  • IntFillArc/IntDrawArc 只关心 arctype == GdiTypeChord 这一位(BOOL Chord = (arctype == GdiTypeChord)),因为填充/描边函数只区分"是否画弦",Pie 的径向线由 IntArcPUTLINE 另行补画。

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,默认逆时针)决定。

因此一条弧由五个几何要素确定:

  1. 椭圆中心 C = ((Left+Right)/2, (Top+Bottom)/2)
  2. 半轴长 a = |Right-Left|/2b = |Bottom-Top|/2
  3. 起点方向角 θs = atan2(YStart-Cy, XStart-Cx)
  4. 终点方向角 θe = atan2(YEnd-Cy, XEnd-Cx)
  5. 方向(顺时针 / 逆时针)。

在 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 行)。IntArcPATH_ArcIntFillArcIntDrawArcIntGdiArcInternalIntGdiAngleArc 全都读取/修改这个位。值得注意的是,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/pFlagsPATH_DoArcPartPATH_ScaleNormalizedPoint 把单位圆上的归一化控制点映射回矩形坐标。这些数据结构是弧→路径转换的载体。


4. 弧几何算法详解

4.1 起点矢径 → 角度(atan2)

标准平面几何中,给定椭圆中心 C 与一个矢径端点 P,方向角为:

复制代码
θ = atan2(Py - Cy, Px - Cx)         单位:弧度([-π, π])

arc.c 中把弧度换算成度数(* 360.0 / (2π)),因为后续 IntFillArc/IntDrawArcapp_* 函数以"整数度数"为接口。度数取值落在 (-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) 得到的角度在视觉上是"顺时针为正"。这解释了为何 IntArcAngleStart = 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;      // 逆指针扫过:终点角必须小于起点角
}

这样 angleStartangleEnd 之间的有向区间就是弧的实际扫过范围,跨度可以是任意值(包括超过 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);
  • 首段的端点类型:GdiTypeArcToPT_LINETO(从当前点连线到弧起点),其他类型用 PT_MOVETO(新子路径)。
  • 之后各段 startEntryType == 0PATH_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 需要先填充(IntFillArcGdiTypeArc 只描边不填充),随后统一描边(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, &centre, 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 行):

  1. 起止角相差 ≥ 360° → 等价于完整椭圆,直接 app_fill_ellipse
  2. 把角度归一化到 [0, 360°);起止角相等 → 什么都不画。
  3. app_boundary_point 求两条径向线与椭圆边界的交点 p1、p2;Chord 时楔形顶点 p0 取弦中点,否则取椭圆中心。
  4. 用中点椭圆步进(t = b² + a² - 2a²b 判别式,逐步决定 moveout/movedown)逐行扫描,得到上下对称的矩形条 r1/r2。
  5. 每个矩形条调用 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

实现流程

  1. 锁 DC (第 364--369 行):DC_LockDc(hDC);失败(无效句柄)→ EngSetLastError(ERROR_INVALID_HANDLE) 返回 FALSE。
  2. 类型校验 (第 370--375 行):arctype > GdiTypePie(即大于 3)→ 解锁并 EngSetLastError(ERROR_INVALID_PARAMETER)。这保证了 ARCTYPE 只能取 0--3。
  3. 准备 DC (第 377--383 行):DC_vPrepareDCsForBlit(dc, NULL, NULL, NULL) 准备表面;若 ulDirty_ 标记了填充刷/线条刷脏(DIRTY_FILL | DC_BRUSH_DIRTYDIRTY_LINE | DC_PEN_DIRTY),分别调用 DC_vUpdateFillBrush/DC_vUpdateLineBrush 重建刷子(保证弧用上最新的画刷/画笔)。
  4. 浮点保护 (第 385--390 行):KeSaveFloatingPointState(&FloatSave) 保存浮点寄存器;失败则解锁返回。
  5. 委托 (第 392--402 行):IntGdiArcInternal(arctype, dc, LeftRect, ..., YEndArc)
  6. 收尾 (第 404--406 行):KeRestoreFloatingPointStateDC_vFinishBlit(dc, NULL)DC_UnlockDc(dc),返回结果。

注意事项

  • 浮点保存/恢复是必须的:IntArcPATH_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

实现流程

  1. 空矩形检查 (第 204 行):(LeftRect == RightRect) || (TopRect == BottomRect) → 直接返回 TRUE(无宽度或无高度,退化为空操作;Windows 语义下这是合法空弧)。
  2. 路径模式 (第 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
  1. 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。此处路径未打开,走光栅化分支。)

  1. 核心绘制 (第 231--240 行):调用 IntArc(dc, LeftRect, TopRect, RightRect, BottomRect, XStartArc, YStartArc, XEndArc, YEndArc, arctype)

  2. 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 行为对照验证。

使用方式 :仅被 NtGdiArcInternalIntGdiAngleArc 调用,是 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->pdcattrPEN_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);

实现流程

  1. 锁 DC (第 315--320 行):失败 → ERROR_INVALID_HANDLE
  2. 浮点保护 (第 322--327 行):KeSaveFloatingPointState 失败 → 解锁返回 FALSE。
  3. 位模式转换 (第 329--330 行):worker.l = dwStartAngle; worker1.l = dwSweepAngle;(之后以 .f 读出 FLOAT)。
  4. 准备 DC 与刷新刷子 (第 331--335 行):DC_vPrepareDCsForBlit + 条件更新填充刷/线条刷(与 NtGdiArcInternal 相同的三段式)。
  5. 委托 (第 336 行):IntGdiAngleArc(pDC, x, y, dwRadius, worker.f, worker1.f)
  6. 收尾 (第 337--342 行):DC_vFinishBlitDC_UnlockDcKeRestoreFloatingPointState

注意事项

  • 参数名为 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, ...) 完成"连线→画弧→移点"的完整语义。

实现流程

  1. 计算弧终点(第 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 文档一致)。

  1. 计算弧起点 (第 268--269 行):同上,但用 eStartAngle

  2. 设置弧方向(第 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 不画。

  1. 构造矩形并委托 (第 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 文档语义。

  1. 恢复方向标志 (第 288 行):pDC->dclevel.flPath |= (arcdir & DCPATH_CLOCKWISE);

  2. 最终移点 (第 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)决定闭合/连线方式。

实现流程

  1. 锁路径 (第 993--994 行):PATH_LockPath(dc->dclevel.hPath),失败返回 FALSE。
  2. 方向判定 (第 996--999 行):direction 非 0 时按 AD_CLOCKWISE 判定;为 0 时读 dc->dclevel.flPath & DCPATH_CLOCKWISE。arc.c 调用时传 0,即采用 DC 当前弧方向。
  3. 零宽高检查 (第 1002--1007 行):x1 == x2 || y1 == y2 → 直接 ArcExit(返回 TRUE)。注释标注"FIXME: Only in GM_COMPATIBLE?"。
  4. 坐标转换 (第 1009--1015 行):INTERNAL_LPTODP(dc, corners, 2) 转换矩形两角点,pointStart/pointEnd 各转一次。
  5. 角点排序 (第 1018--1029 行):保证 corners[0] 为左上、corners[1] 为右下。
  6. 求起止角 (第 1032--1035 行):PATH_NormalizePoint + atan2(见 4.1/4.3 节)。
  7. 角度区间调整 (第 1038--1053 行):按方向把 angleEnd 挪到起点的正确一侧(见 4.2 节)。
  8. GM_COMPATIBLE 修正 (第 1056--1060 行):兼容模式下 corners[1].x--; corners[1].y--;------排除 right/bottom 边界,使弧与 Win9x 行为一致。
  9. arcto 首点 (第 1063--1073 行):lines == GdiTypeArcTo && pPath->newStroke 时,读取 DC 当前点(IntGetCurrentPositionEx + CoordLPtoDP 转设备坐标),以 PT_MOVETO 加入路径(保证"当前点→弧起点"连线的起点正确),并清除 newStroke
  10. 象限分段循环 (第 1077--1116 行):每段 ≤ 90°,调用 PATH_DoArcPart(见 4.2、5.7 节)。
  11. 闭合处理 (第 1118--1133 行):ArcTo → update_current_pos(路径当前点 = 弧终点);Chord → IntGdiCloseFigure;Pie → 加 中心点, PT_LINETO | PT_CLOSEFIGURE(见 4.4 节)。
  12. 解锁路径(第 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 节)。

实现流程

  1. 断言 (第 913 行):ASSERT(fabs(angleEnd - angleStart) <= M_PI_2)------调用者保证每段不超过 90°。
  2. 计算控制点 (第 917--936 行):系数 a = 4/3·(1-cos h)/sin h;4 个归一化控制点 xNorm[]/yNorm[];半角过小则四点重合。
  3. 追加点 (第 938--948 行):start = !startEntryType;对 i = start..3PATH_ScaleNormalizedPoint 映射到设备坐标;add_points(pPath, points+start, 4-start, PT_BEZIERTO) 追加(贝塞尔段三点为一组:首点已在路径上时只需追加 3 个控制点);首点类型改写为 startEntryType
  4. 返回:成功 TRUE,内存不足 FALSE。

使用方式 :仅被 PATH_Arc 调用(第 1109 行),属于 path.c 内部辅助。

注意事项

  • add_points 追加的是设备坐标点(调用者已转换),不再做 LPtoDP。
  • 首段(startEntryType == PT_MOVETOPT_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 行)

  1. 锁定填充刷 BRUSH_ShareLockBrush(pdcattr->hbrush),失败 → ERROR_INTERNAL_ERROR
  2. Start = (int)ceil(StartArc); End = (int)ceil(EndArc);------度数向上取整为整数。
  3. Chord = (arctype == GdiTypeChord)
  4. 调用 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 层扫描方向一致)。

  1. 解锁刷子,返回。

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(但不会中断后续补线,这是"尽力而为"的容错风格)。
  • 两个宏都依赖外围作用域中的 psurfdcRectBoundspdcattrret 变量,只能在 IntArc 内部使用(闭包式宏)。

6. 调用链(mermaid 时序)

以下 mermaid 图展示 Arc/PieAngleArc 两条完整调用链(含路径模式分支):
#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)

调用链要点:

  1. Arc 族(Arc/ArcTo/Chord/Pie) :gdi32 包装(HANDLE_METADC/HANDLE_EMETAFDC 元文件处理 + GdiConvertAndCheckDC 校验)→ NtGdiArcInternal(arctype, ...)(经 win32u)→ IntGdiArcInternal → 路径模式 PATH_Arc 或直接模式 IntArc
  2. AngleArc :gdi32 把两个 FLOAT 按位强转 DWORD → NtGdiAngleArc → 位模式还原为 FLOAT → IntGdiAngleArc(算端点、设方向)→ IntGdiArcInternal(GdiTypeArcTo, ...),随后复用 Arc 族管线。
  3. 路径模式PATH_Arc 每 90° 一段调用 PATH_DoArcPart,最终点写入路径后由上层 StrokePath/FillPath 统一渲染。
  4. 直接模式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 月)的观察,标注了疑点与改进方向,不代表全部已被确认。

  1. ArcTo 顺时针分支的连线/移点方向 (arc.c 第 223--248 行):Windows 语义是"从当前点连到弧起点,画完移到弧终点"。ReactOS 的逆时针 分支符合该语义;顺时针分支反之为"连终点、移到起点"。若应用依赖顺时针 ArcTo 的标准行为,可能出现连线方向差异。建议对照 Wine/Windows 验证后决定是否调整。

  2. IntGdiAngleArc 的方向标志恢复缺陷 (arc.c 第 288 行):pDC->dclevel.flPath |= (arcdir & DCPATH_CLOCKWISE) 用"或"恢复。当原方向为逆时针(CLOCKWISE 位为 0)而本次传负扫过角(置位)后,|= 0 无法清除残留位,DC 方向被永久改为顺时针。同理,原方向为顺时针而本次正扫(清位)时同样无法恢复。只有"原为顺时针且本次也置位"或"原为逆时针且本次也清位"两种组合能正确恢复。建议改为"先清位再按原值或入"。

  3. PS_NULL 空笔仍以宽 1 描边 (arc.c 第 79、91 行):空笔逻辑上不应绘制,但 PenWidth 被强制提升为 1 后 IntDrawArc 仍会描边。这与"空笔不画"的直觉语义有出入,需与 Windows 行为核对。

  4. IntArc 对共享画笔 lWidth 的临时改写 (arc.c 第 78--92、176 行):pbrPen->lWidth = PenWidth 临时改写共享画笔对象,靠 PEN_ShareLockPen 的锁保护;若未来并发访问路径变化(如多线程共享画笔),存在竞态隐患。此外,若第 9 步表面检查失败提前返回,lWidth 未被恢复(该分支在绘制之后,实际改写已经发生------不过 INSIDEFRAME 的收缩只影响局部变量 Left/Right/Top/Bottom,lWidth 改写本身在原宽基础上进行,提前返回时 lWidth 值已在绘制中定型,恢复与否影响后续绘制)。

  5. 角度取整IntFillArc/IntDrawArc(int)ceil(角度) 取整(drawing.c 第 1317--1318、1352--1353 行),存在 1 度内的量化误差;IntGdiAngleArc 端点用 (INT) 截断,有亚像素误差。两者均有 FIXME 注释计划改用定点数。

  6. 1 像素 Chord/Pie 直接跳过(arc.c 第 60--65 行):带 FIXME,需验证与 Windows 的一致性。

  7. PATH_Arc 的 FIXME (path.c 第 988--989、1002 行):未检查所有可能的错误返回;newStroke 的某些状态未处理;零宽高检查是否仅限 GM_COMPATIBLE 未定。

  8. 注释过期 :PATH_Arc 的 lines 参数注释("-1/1/2")与实际 ARCTYPE 值(0--3)不符;NtGdiAngleArcDWORD 参数名易误导(实际承载 FLOAT 位模式)。阅读源码时需以上下文为准。

  9. 浮点依赖 :整个模块依赖 atan2/cos/sinM_PI 常量;内核浮点上下文由两个 NtGdi 入口显式保护,任何新增内部调用路径(如未来从其他内核模块直接调用 IntGdiAngleArc)都必须自带 KeSaveFloatingPointState,否则可能产生随机浮点故障。

  10. 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,其核心设计可概括为三句话:

  1. 一个入口NtGdiArcInternal(ARCTYPE, ...) 统一 Arc/ArcTo/Chord/Pie,用枚举区分闭合语义;
  2. 两条路径 :路径打开走贝塞尔弧段(PATH_Arc),否则走填充+描边直接光栅化(IntArc);
  3. 一个复用AngleArc 通过 GdiTypeArcTo 分支复用"连线+画弧+移点"的完整管线,只额外负责角度参数化与方向设置。

弧的几何本质------"矩形 + 起止矢径 → atan2 角度 → 按方向扫过 → 90° 分段贝塞尔或扫描线光栅化"------是理解本模块(以及 path.c、drawing.c 相关部分)的钥匙。文中标注的若干 FIXME 与源码现状疑点(ArcTo 顺时针方向、方向标志恢复、空笔宽 1 等)可作为后续修复与测试的切入点。


本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/arc.c 及关联模块分析(2026 年 8 月)

相关推荐
cypking2 小时前
Objective-C 语法完整学习手册(小白自学 + 开发备查)
c语言·开发语言·学习·objective-c
ambition202424 小时前
操作系统同步:读者-写者问题与读写公平法详解(附每个 PV 操作含义)
linux·开发语言·数据结构·unix
chuan.bai7 小时前
Java RAG 实战(第 11 篇):RAG 知识工作台网页
java·开发语言·人工智能
离陌在学C#9 小时前
C# 事件(Event)详解:从概念到实战
开发语言·c#
Terra.K9 小时前
Java异常学习[特殊字符]
java·开发语言·学习
葡萄城技术团队11 小时前
InfluxDB 2\.x 深度解析:核心架构、Flux 函数与制造业落地指南(三)
java·开发语言·架构
counting money11 小时前
Java IO流详解:从InputStream到文件操作实战
java·开发语言·python
wuyk55511 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法
峥无12 小时前
从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)
开发语言·c++·笔记·算法·stl