ReactOS 图形系统分析(42):形状填充 — fillshap.c

ReactOS 图形系统分析(42):形状填充 --- fillshap.c

1. 概述

fillshap.cwin32ss/gdi/ntgdi/fillshap.c,约 1250 行)是 ntgdi(内核态 GDI,即 win32k.sys 中的 GDI 部分)的形状绘制与填充子系统 。它实现了用户态 GDI32 层形状类 API 的内核入口:椭圆(NtGdiEllipse)、矩形(NtGdiRectangle)、圆角矩形(NtGdiRoundRect)、渐变填充(NtGdiGradientFill)、洪水填充(NtGdiExtFloodFill)、多边形/复合绘制(NtGdiPolyPolyDraw),以及若干仅供内核内部调用的辅助函数(IntGdiPolygonIntGdiPolyPolygonIntRectangleIntRoundRectGreGradientFillIntPolygon 等)。

本文件的核心设计思想是:形状先被转换成"路径(PATH)"或"区域(REGION)",再经由通用的填充/描边引擎输出 。这与 ReactOS 图形栈"少即是多"的分层思路一致------椭圆、圆角矩形这类复杂形状,其几何轮廓被分解为贝塞尔曲线或扫描线可处理的边,从而复用已有的路径填充、线段绘制、位块传输等基础设施,而不必为每种形状单独实现光栅化器。不过,ReactOS 在非路径模式下对椭圆/圆角矩形还保留了一套中点(midpoint)光栅化算法 (位于 drawing.capp_fill_ellipse/app_draw_ellipse 等),这是历史遗留的双轨实现,下文会详细对比。

1.1 架构总览

复制代码
应用: Ellipse / Rectangle / RoundRect / GradientFill / ExtFloodFill / PolyPolyDraw / Polygon
    │  (GDI32 用户态调用 → 系统服务入口)
    ↓
fillshap.c(本文件,NtGdi* 系统服务入口 + 内核内部 Int* 函数)
    │
    ├─ 形状 → 路径:PATH_Ellipse / PATH_Rectangle / PATH_RoundRect / PATH_PolyPolygon
    │                (ntgdi/path.c,见《分析_37》)
    │
    ├─ 填充/描边:
    │      ├─ IntFillPolygon(ntgdi/polyfill.c,扫描线边缘表填充)
    │      ├─ IntFillEllipse / IntDrawEllipse(ntgdi/drawing.c,中点算法)
    │      ├─ IntFillRoundRect / IntDrawRoundRect(ntgdi/drawing.c,圆弧+矩形组合)
    │      ├─ IntEngBitBlt(PATCOPY 位块填充,eng 层)
    │      ├─ IntEngLineTo(逐边描边,eng 层)
    │      └─ PATH_StrokePath(宽笔描边,ntgdi/path.c)
    │
    ├─ 渐变:GreGradientFill → IntEngGradientFill(win32ss/gdi/eng/gradient.c,见《分析_26》)
    │
    └─ 洪水:NtGdiExtFloodFill → DIB_XXBPP_FloodFillSolid(win32ss/gdi/dib/floodfill.c,见《分析_29》)

1.2 设计动机

  1. 统一入口、复用引擎NtGdiPolyPolyDraw 一个系统服务就承载了 PolyPolygon、PolyPolyline、PolyBezier、PolylineTo、PolyBezierTo、PolyPolygonRgn 六种 GDI 对象(iFunc 参数分发),把参数校验、SEH 探测、内核缓冲分配集中在一处,减少重复代码与攻击面。
  2. 路径优先 :凡 DC 处于"路径接收"状态(PATH_IsPathOpen(dc->dclevel)),所有形状 API 一律转向 PATH_* 系列,把轮廓追加到当前路径中------这正是 BeginPath() 的实现基础;只有路径未打开时才真正执行绘制。
  3. 双轨光栅化:椭圆/圆角矩形在没有路径的情况下,既可以用贝塞尔路径(宽笔描边场景),也可以用中点算法直接逐矩形填充(普通场景),以保证性能与兼容性。
  4. 颜色/画刷经 EXLATEOBJ 转换 :渐变与洪水填充在进入引擎前都把 COLORREF/顶点颜色通过 EXLATEOBJ(调色板翻译对象)转成目标表面格式的颜色,屏蔽了 1bpp~32bpp 的差异。
  5. 全部走 DC 脏标记体系 :填充画笔(fill brush)与线条画笔(line pen)通过 ulDirty_DIRTY_FILL/DC_BRUSH_DIRTYDIRTY_LINE/DC_PEN_DIRTY 位延迟更新(DC_vUpdateFillBrush/DC_vUpdateLineBrush),避免每次绘制都重建画刷对象。

2. 核心数据结构

2.1 TRIVERTEX / GRADIENT_RECT / GRADIENT_TRIANGLE

渐变填充的三个核心结构定义在 sdk/include/psdk/wingdi.h(用户态与内核态共用):

c 复制代码
typedef USHORT COLOR16;

typedef struct _TRIVERTEX {
    LONG    x;          // 顶点逻辑坐标 X
    LONG    y;          // 顶点逻辑坐标 Y
    COLOR16 Red;        // 红色分量(0x0000 ~ 0xff00,高 8 位有效)
    COLOR16 Green;      // 绿色分量
    COLOR16 Blue;       // 蓝色分量
    COLOR16 Alpha;      // Alpha 分量(当前渐变填充中不使用)
} TRIVERTEX, *PTRIVERTEX, *LPTRIVERTEX;

typedef struct _GRADIENT_TRIANGLE {
    ULONG Vertex1;      // 第一个顶点在 TRIVERTEX 数组中的索引
    ULONG Vertex2;      // 第二个顶点索引
    ULONG Vertex3;      // 第三个顶点索引
} GRADIENT_TRIANGLE, *PGRADIENT_TRIANGLE, *LPTRIGRADIENT_TRIANGLE;

typedef struct _GRADIENT_RECT {
    ULONG UpperLeft;    // 左上角顶点索引
    ULONG LowerRight;   // 右下角顶点索引
} GRADIENT_RECT, *PGRADIENT_RECT, *LPGRADIENT_RECT;

要点:

  • COLOR16 是 16 位颜色分量,取值范围 0x0000~0xff00,只有高 8 位有意义eng/gradient.c 中的插值代码大量使用 >> 8 把 COLOR16 归一化到 0~255 再做线性插值,输出前再经 XLATEOBJ_iXlate 转换。
  • 网格(mesh)是一组索引UpperLeft/LowerRight(矩形模式)或 Vertex1/2/3(三角形模式)指向 pVertex 数组中的顶点,这是典型的"顶点缓冲 + 索引缓冲"布局,允许多个网格共享顶点。
  • 模式常量(wingdi.h):
c 复制代码
#define GRADIENT_FILL_RECT_H  0x00   // 水平渐变矩形(左右两顶点间水平插值)
#define GRADIENT_FILL_RECT_V  0x01   // 垂直渐变矩形(上下两顶点间垂直插值)
#define GRADIENT_FILL_TRIANGLE 0x02  // 渐变三角形

2.2 填充模式:WINDING / ALTERNATE

GDI 的多边形填充有两种模式,由 DC 的 pdcattr->jFillMode 决定(SetPolyFillMode 设置):

  • ALTERNATE(交替/奇偶规则,值 1) :对每条扫描线,按边与扫描线的交点从左到右排序,第 1、2 条边之间填充,第 3、4 条边之间填充,依此类推 。简单自交多边形中,"奇偶"自然产生镂空效果。polyfill.c 文件头部注释给出了官方语义:"When the fill mode is ALTERNATE, GDI fills the area between odd-numbered and even-numbered polygon sides on each scan line."
  • WINDING(环绕数规则,值 2) :为每条边赋予方向(+1/-1),对扫描线上的点计算环绕数(多边形边界绕该点多少圈),环绕数非零的区域被填充。边的方向(顺时针/逆时针)在此规则下有意义,因此可区分"洞"与"岛"。

ReactOS 中 FillPolygonpolyfill.c)根据 jFillMode 选择两条扫描线填充函数:

c 复制代码
if (WINDING == pdcattr->jFillMode)
    FillScanLine = POLYGONFILL_FillScanLineWinding;
else /* Default */
    FillScanLine = POLYGONFILL_FillScanLineAlternate;

IntFillPolygonfillshap.cIntGdiPolygon 实际调用者)没有jFillMode 分支,直接按交替方式以 IntEngBitBlt 填充------这一点与 Windows 存在差异,也说明 ReactOS 对填充模式的支持尚不完整(见第 3.4 节分析)。

2.3 与本文件相关的 DC 内部结构

  • dc->pdcattrPDC_ATTR):持有 hpenhbrushjROP2(光栅操作)、jBkMode(背景模式)、jFillMode(填充模式)、iGraphicsMode(GM_COMPATIBLE/GM_ADVANCED)、ptlCurrentmxWorldToDevice(世界变换矩阵)等。
  • dc->dclevel:持有绘制会话级对象------pbrFill(当前填充画刷)、pbrLine(当前线条画笔)、pSurface(目标表面)、hPath(当前路径句柄)、flPath(路径状态标志,含 DCPATH_ACTIVE/DCPATH_CLOCKWISE)。
  • dc->eboFill/dc->eboLineEBRUSHOBJ):已实例化的画刷/画笔对象,直接挂在 IntEngBitBlt/IntEngLineTo 上。
  • dc->ptlDCOrig:DC 原点,所有设备坐标结果都要加上它(IntLPtoDP 之后 += ptlDCOrig)。
  • dc->coCLIPOBJ)、dc->prgnVis/dc->prgnRao:裁剪对象与可视/RAO 区域。
  • dc->fs 中的 DC_ACCUM_APP/DC_ACCUM_WMGR:脏矩形累积(IntUpdateBoundsRect),用于窗口管理器区域刷新。

2.4 形状绘制通用预处理流水线

几乎所有入口(NtGdiEllipse/Rectangle/RoundRectIntGdiPolygonGreGradientFillNtGdiExtFloodFill)共享同一套预处理步骤,可以提炼为固定流水线:

复制代码
1. DC_LockDc(hDC) ──────────────────── 句柄→对象,失败置 ERROR_INVALID_HANDLE
2. 路径检查 PATH_IsPathOpen? ────────── 是 → PATH_*(仅形状入口)
3. 参数规范化(min/max 交换、abs、INSIDEFRAME 收缩、退化形状短路)
4. IntLPtoDP + ptlDCOrig 偏移 ───────── 逻辑→设备坐标
5. RECTL_vMakeWellOrdered ──────────── RTL 反矩形修正
6. 脏标记检查 → DC_vUpdateFillBrush / DC_vUpdateLineBrush
7. DC_vPrepareDCsForBlit(开启绘制会话)
8. 填充(IntFillPolygon / IntFillEllipse / IntEngBitBlt / IntEngGradientFill / FloodFillSolid)
9. 描边(IntEngLineTo / PATH_StrokePath,按 IntIsEffectiveWidePen 分支)
10. DC_vFinishBlit(提交绘制会话)
11. 解锁(画刷/画笔/DC)

其中第 3、4、5 步是形状正确性的关键:坐标不规范化会导致负宽矩形,不补偿 DC 原点会导致图案错位,不修正反矩形会导致引擎负宽度访问。

3. 多边形系列

3.1 IntGdiPolygon ------ 单多边形填充 + 描边

c 复制代码
BOOL FASTCALL
IntGdiPolygon(PDC    dc,
              PPOINT Points,
              int    Count)
  • 参数dc 已加锁的 DC;Points 顶点数组(逻辑坐标,会被就地改写为设备坐标 );Count 顶点个数。
  • 作用 :绘制一个闭合多边形:先用填充画刷填充内部,再用线条画笔描边。这是 Polygon() API 的内核实现主干,也被 IntRectangle 的旋转/剪切分支(NtGdiRectangle)复用。
  • 实现流程
  1. 参数校验ASSERT(dc);若 Points == NULL || Count < 2EngSetLastError(ERROR_INVALID_PARAMETER) 返回 FALSE。
  2. 坐标变换IntLPtoDP(dc, Points, Count) 把顶点从逻辑坐标转设备坐标,再逐点累加 dc->ptlDCOrig 偏移。
  3. 计算包围盒DestRect 取所有顶点的 min/max,作为后续填充/描边的边界矩形(Bounding rectangle),供引擎裁剪优化。
  4. 刷子/画笔更新 :按脏标记调用 DC_vUpdateFillBrush/DC_vUpdateLineBrush;随后按特殊加锁顺序 取出 pbrFillpbrLinepsurf = dc->dclevel.pSurface(注释说明该顺序是为了避免死锁)。
  5. 空表面处理 :若 psurf == NULL(内存 DC 未选位图),直接返回 TRUE(无操作成功)。
  6. 填充 :若填充画刷非 NULL 画刷(!(pbrFill->flAttrs & BR_IS_NULL)),计算画刷原点(ptOrigin + ptlDCOrig),调用 IntFillPolygon(dc, psurf, &dc->eboFill.BrushObject, Points, Count, DestRect, &BrushOrigin) 完成扫描线填充。
  7. 描边 :若线条画笔非 NULL:
    • 宽笔IntIsEffectiveWidePen(pbrLine),见 3.4):删除旧路径 → PATH_CreatePath(Count+1) 创建新路径 → 设 DCPATH_ACTIVEPATH_MoveTo 起点 → 对每个顶点 PATH_LineTo → 最后 PATH_LineTo(Points[0]) 闭合 → pPath->state = PATH_Closed、清除 DCPATH_ACTIVEPATH_StrokePath(dc, pPath) 描边 → 解锁并删除路径。宽笔必须走路径描边,因为逐边 IntEngLineTo 无法正确处理线帽/线接合。
    • 细笔 :循环 IntEngLineTo 依次画 Points[i]→Points[i+1],最后补一条 Points[Count-1]→Points[0] 的闭合边;ROP 由 ROP2_TO_MIX(pdcattr->jROP2) 转换。
  8. 返回:填充与描边任一失败则 FALSE。
  • 注意事项Points 数组被原地修改(LPtoDP),调用方需可写缓冲;填充走 IntFillPolygon(交替扫描线),与 jFillMode 无关。

3.2 IntGdiPolyPolygon ------ 复合多边形

c 复制代码
BOOL FASTCALL
IntGdiPolyPolygon(DC      *dc,
                  LPPOINT Points,
                  PULONG  PolyCounts,
                  int     Count)
  • 参数PolyCounts 是每个子多边形顶点数的数组;Count 是子多边形个数。
  • 实现流程 :若 DC 处于路径打开状态,直接委托 PATH_PolyPolygon(dc, Points, (PINT)PolyCounts, Count)(把多段轮廓追加进路径,见 3.5);否则 while (--Count >= 0) 逐个子多边形调用 IntGdiPolygon,每处理一个子多边形就把 Points 指针后移 *PolyCounts++ 个点。任一子多边形失败即返回 FALSE。
  • 使用方式NtGdiPolyPolyDrawGdiPolyPolygon 分支调用;NtGdiRectangle 在 XFORM_SCALE 加速条件下也用它把矩形当作 4 顶点单多边形处理。

3.3 IntPolygon ------ HDC 级包装

c 复制代码
BOOL FASTCALL
IntPolygon(HDC hdc, POINT *Point, int Count)

加锁 DC_LockDc(hdc)IntGdiPolygonDC_UnlockDc。失败时 EngSetLastError(ERROR_INVALID_HANDLE)。它供内核内部以 HDC 句柄调用 Polygon 的场合使用(例如其他子系统按句柄绘制时)。

3.4 IntFillPolygon ------ 扫描线边缘表填充(polyfill.c)

IntFillPolygon 定义在 ntgdi/polyfill.c(第 590 行),是 IntGdiPolygon 的填充引擎:

c 复制代码
BOOL FASTCALL
IntFillPolygon(PDC dc, SURFACE *psurf, BRUSHOBJ *BrushObj,
               CONST PPOINT Points, int Count,
               RECTL DestRect, POINTL *BrushOrigin)

实现要点:

  1. POLYGONFILL_MakeEdgeList(Points, Count):为每条边(含最后一条闭合边 Points[Count-1]→Points[0])创建 FILL_EDGEPOLYGONFILL_MakeEdge 会把边规范化为 FromY <= ToY(反向边交换端点并记 YDirection = -1),计算 dx/dy/absdx/absdy/xmajor/XDirection,并用 Bresenham 风格的 Error/ErrorMax 初值(Error = ErrorMax/2,上行边再 -1 以像素级对齐)。水平边(absdy==0)被直接丢弃
  2. 对每条扫描线 ScanLine ∈ [DestRect.top, DestRect.bottom)
    • POLYGONFILL_BuildActiveList 收集满足 FromY <= ScanLine < ToY 的边,POLYGONFILL_UpdateScanline 用类 Bresenham 递推计算边与该扫描线的 X 截距区间 XIntercept[0..1]POLYGONFILL_ActiveListInsert 按截距排序插入活动边表。
    • 活动边成对取 (pLeft->XIntercept[0], pRight->XIntercept[1]),若 x2 > x1 则构造一条 1 像素高的 LineRect,用 IntEngBitBlt(..., ROP4_FROM_INDEX(R3_OPINDEX_PATCOPY)) 以画刷 PATCOPY 填充------即每条扫描线段一次位块传输。
  3. POLYGONFILL_DestroyEdgeList 释放内存。

同文件另有 FillPolygon(第 536 行)与 POLYGONFILL_FillScanLineAlternate/POLYGONFILL_FillScanLineWinding 两个逐行填充器(用 IntEngLineTo 画线而非 BitBlt),并依据 jFillMode 选择------但 fillshap.c 中该函数被 #if 0 注释掉的 extern 声明表明:当前 IntGdiPolygon 走的是 IntFillPolygon 交替路径,WINDING 模式的完整支持在填充路径上尚未接入。

3.5 PATH_PolyPolygon ------ 复合多边形路径化(path.c)

c 复制代码
BOOL FASTCALL
PATH_PolyPolygon(PDC dc, const POINT* pts, const INT* counts, UINT polygons)
  • 校验每个子多边形 counts[poly] >= 2,累计总点数。
  • add_log_points 把全部顶点一次性追加进路径(类型 PT_LINETO,自动做 LPtoDP)。
  • 对每个子多边形修正标志:type[0] = PT_MOVETO(子多边形起点),type[counts[poly]-1] = PT_LINETO | PT_CLOSEFIGURE(闭合图形)。
  • 结果是路径中容纳多个闭合图形,之后可由 EndPathFillPath/StrokePath 统一处理。

3.6 宽笔描边:临时路径 + PATH_StrokePath

IntGdiPolygonIntRectangle 在检测到"有效宽笔"时,都会走同一套临时路径描边流程,其步骤可以归纳为一个"构造-描边-销毁"三段式模式:

  1. 构造PATH_Delete(dc->dclevel.hPath) 清掉可能残留的旧路径;PATH_CreatePath(n) 预分配 n 个槽位的路径(多边形用 Count+1,矩形固定 5);置 dc->dclevel.flPath |= DCPATH_ACTIVE 并把新路径句柄挂到 dc->dclevel.hPathPATH_MoveTo 设置起点(矩形分支先把起点 pPath->posIntLPtoDP)。
  2. 描边 :逐点 PATH_LineTo 追加(多边形最后补回 Points[0] 闭合),然后 pPath->state = PATH_Closed 并清除 DCPATH_ACTIVEPATH_StrokePath(dc, pPath) 用当前画笔把闭合路径描成像素。描边引擎负责线帽(PS_ENDCAP_)、线接合(PS_JOIN_ )与几何笔宽度的处理,这是逐边 IntEngLineTo 无法提供的。
  3. 销毁PATH_UnlockPath 解锁、PATH_Delete(dc->dclevel.hPath) 删除、dc->dclevel.hPath = NULL 复位。

选择路径描边而不是逐边画线的原因在于:普通 IntEngLineTo 对多边形的顶点接缝处理不完整------两条独立线段的端帽会互相叠印或留缝,而路径描边把整个轮廓当作单一图形统一处理接合处。

3.7 扫描线填充的数学细节(FILL_EDGE 结构)

FILL_EDGE(polyfill.c:19)是扫描线填充的核心数据结构,字段含义如下:

字段 含义
FromX/FromY/ToX/ToY 规范化后的边端点(FromY <= ToY
dx/dy/absdx/absdy 边向量与绝对值
xmajor absdx > absdy:x 主导边(近水平)
x/y 当前步进位置(像素级)
XIntercept[2] 边与当前扫描线交叠的 x 区间(x 主导边会产生 2 像素宽的过渡带)
Error/ErrorMax Bresenham 误差项与模
XDirection/YDirection x/y 步进方向;YDirection 同时是环绕数(winding)累加单位
pNext 活动边表链表指针

POLYGONFILL_UpdateScanline(polyfill.c:294)实现从当前扫描线推进到下一扫描线的递推:

  • x 主导边xmajor):先计算"大步跳到下一条扫描线"所需的步数 steps = (ErrorMax-Error-1)/absdy,把这 steps 步内扫过的 x 区间记入 XIntercept[0..1](min/max),再精确走 1 步(x += XDirection; Error += absdy; Error -= absdx; y++)落到下一条扫描线。它用 ASSERT 保证每轮恰好跨过一条扫描线((ErrorMax-Error-1)/absdy == 0)。
  • y 主导边XIntercept[0]=XIntercept[1]=x(每行只占一个 x),Error += absdx,溢出时 x += XDirection

初值 Error = ErrorMax/2(半进位舍入)加上上行边 Error = -1 的修正,保证正反方向边在同一像素网格上对齐------这正是注释所说 "Lines that go up get walked backwards, so need to be offset by -1 in order to make the walk identically on a pixel-level"。

3.8 两种扫描线填充器的实现对照

POLYGONFILL_FillScanLineAlternate(polyfill.c:389)与 POLYGONFILL_FillScanLineWinding(polyfill.c:437)接受相同的活动边表,但填充规则不同:

步骤 Alternate(奇偶) Winding(环绕数)
遍历 活动边按 XIntercept 排序后两两成对 边按序进入,累加 winding += pLeft->YDirection
判定 无判定,直接取第 1、2 条之间,第 3、4 条之间...... winding != 0 时把新区间并入当前线段;winding == 0 时结束当前线段
相邻合并 新截距区间与当前区间重叠/相接时合并(newx1/x2x1/x2 相交判断)
输出 每对区间一次 IntEngLineTo 每段连续区间一次 IntEngLineTo
适用 简单多边形、自交镂空 需要方向语义的复杂多边形(洞/岛)

Winding 版本对"环绕数为 0 的区间"不填充,因此自交多边形的"缠绕"区域会被正确填实,而 Alternate 版本会按奇偶产生交替镂空。YDirectionPOLYGONFILL_MakeEdge 按边的上下方向给出(To.y < From.y 时为 -1),这正是环绕数算法的核心输入。

4. NtGdiPolyPolyDraw ------ 六合一复合绘制入口

c 复制代码
ULONG_PTR APIENTRY
NtGdiPolyPolyDraw(IN HDC hDC,
                  IN PPOINT UnsafePoints,
                  IN PULONG UnsafeCounts,
                  IN ULONG Count,
                  IN INT iFunc)
  • 作用 :一个系统服务同时支撑 GDI32 的 PolyPolygonPolyPolylinePolyBezierPolylineToPolyBezierToPolyPolygon(Rgn 变体)。iFunc 取值来自 GdiPolyPolygon/GdiPolyPolyLine/GdiPolyBezier/GdiPolyLineTo/GdiPolyBezierTo/GdiPolyPolyRgn 枚举。
  • 实现流程
  1. 快速失败!UnsafePoints || !UnsafeCounts || Count == 0 || iFunc == 0 || iFunc > GdiPolyPolyRgn 直接返回 FALSE(注释明确:Windows 对这些情况不设置 last error)。
  2. 第一段 SEHProbeForRead 探测两个用户缓冲;同时统计 nPoints(总点数)、nMaxPoints(最大子多边形点数)、nInvalid(点数 < 2 的子多边形数)。异常 → _SEH2_GetExceptionCode()
  3. 溢出与合法性nPoints == 0 || nPoints < nMaxPoints(说明求和溢出)→ FALSE 不设错误;nInvalid != 0(存在 0 或 1 点的子多边形)→ EngSetLastError(ERROR_INVALID_PARAMETER)
  4. 一次分配ExAllocatePoolWithTag(PagedPool, Count*sizeof(ULONG) + nPoints*sizeof(POINT), TAG_SHAPE),前半段放 counts(SafeCounts),后半段放 points(SafePoints)。
  5. 第二段 SEHRtlCopyMemory 把两个用户缓冲拷入内核缓冲(指针已探测,无需再探)。
  6. GdiPolyPolyRgn 特例iFunc == GdiPolyPolyRgn 时,hDC 参数被复用为填充模式 iMode = (INT)(UINT_PTR)hDC,调用 GreCreatePolyPolygonRgn(SafePoints, SafeCounts, Count, iMode) 生成区域句柄返回(返回 HRGN 而非 BOOL,故函数返回类型是 ULONG_PTR)。
  7. 加锁与准备DC_LockDc(hDC)DC_vPrepareDCsForBlit;按脏标记更新填充/线条画刷。
  8. 分发
c 复制代码
switch (iFunc)
{
    case GdiPolyPolygon:  Ret = IntGdiPolyPolygon(dc, SafePoints, SafeCounts, Count); break;
    case GdiPolyPolyLine: Ret = IntGdiPolyPolyline(dc, SafePoints, SafeCounts, Count); break;
    case GdiPolyBezier:   Ret = IntGdiPolyBezier(dc, SafePoints, *SafeCounts); break;
    case GdiPolyLineTo:   Ret = IntGdiPolylineTo(dc, SafePoints, *SafeCounts); break;
    case GdiPolyBezierTo:
        /* 来自 Wine 10.0 dlls/win32u/painting.c 的参考实现:
         * UnsafeCounts[0] 必须为 3*n+1(n>=1) */
        if (Count == 1 && UnsafeCounts[0] != 1 && UnsafeCounts[0] % 3 == 1)
        {
            SafeCounts[0]--;
            Ret = IntGdiPolyBezierTo(dc, SafePoints, *SafeCounts);
        }
        else { EngSetLastError(ERROR_INVALID_PARAMETER); Ret = FALSE; }
        break;
    default: EngSetLastError(ERROR_INVALID_PARAMETER); Ret = FALSE;
}
  1. 清理DC_vFinishBlitDC_UnlockDc → 释放内核缓冲,返回 (ULONG_PTR)Ret
  • 子函数 (均定义在 ntgdi/line.c,与本文件密切配合):
    • IntGdiPolyPolyline(line.c:464):复合折线,路径打开时走 PATH_PolyPolyline
    • IntGdiPolyBezier(line.c:262):路径打开 → PATH_PolyBezier;否则 GDI_Bezier 把贝塞尔曲线离散为折线段,再 IntGdiPolyline 绘制(贝塞尔转折线化)。
    • IntGdiPolylineTo(line.c:425):从 ptlCurrent 开始连线并更新当前位置。
    • IntGdiPolyBezierTo(line.c:290):路径打开 → PATH_PolyBezierTo;否则把当前点 ptlCurrent 前置进顶点数组再 IntGdiPolyBezier,成功后更新 ptlCurrent/ptfxCurrent(同步清 DIRTY_PTLCURRENT 等脏位)。
  • 注意事项GdiPolyBezierTo 要求 Count == 1 且点数满足 3n+1(一组贝塞尔参数 = 3 控制点 + 1 起点,多组则加 3),SafeCounts[0]-- 去掉首点后再交给 IntGdiPolyBezierTo(该函数内部再补当前点);这是对 Wine 行为的对齐。

4.1 iFunc 分支与 GDI32 API 映射

iFunc 枚举 对应 GDI32 API 内部实现 备注
GdiPolyPolygon PolyPolygon IntGdiPolyPolygon 每个子多边形独立闭合填充+描边
GdiPolyPolyLine PolyPolyline IntGdiPolyPolyline 折线不闭合
GdiPolyBezier PolyBezier IntGdiPolyBezier 贝塞尔离散化为折线
GdiPolyLineTo PolylineTo IntGdiPolylineTo 从当前点开始,更新当前位置
GdiPolyBezierTo PolyBezierTo IntGdiPolyBezierTo 点数须为 3n+1,更新当前位置
GdiPolyPolyRgn CreatePolyPolygonRgn GreCreatePolyPolygonRgn 不绘制,返回 HRGN;hDC 复用为填充模式

4.2 错误处理对照

错误场景 行为 last error
空指针 / Count==0 / 非法 iFunc 返回 FALSE 不设置(对齐 Windows)
用户缓冲不可读(SEH 捕获) 返回 FALSE 不设置(对齐 Windows)
点数求和溢出(nPoints < nMaxPoints 返回 FALSE 不设置
存在点数 <2 的子多边形(nInvalid != 0 返回 FALSE ERROR_INVALID_PARAMETER
内核缓冲分配失败 返回 FALSE ERROR_NOT_ENOUGH_MEMORY
DC 句柄无效 返回 FALSE ERROR_INVALID_HANDLE
GdiPolyBezierTo 点数不满足 3n+1 返回 FALSE ERROR_INVALID_PARAMETER

这个表同时体现了内核系统服务编程的两个惯例:能静默失败的尽量静默 (不泄漏内核信息),明确参数错误才设置 last error

4.3 GDI_Bezier 转折线化

IntGdiPolyBezier 在非路径模式下调用 GDI_Bezier(pt, Count, &nOut) 把一条或多条三次贝塞尔曲线离散为折线段(输出点数组以 TAG_BEZIER 标签分配),再交给 IntGdiPolyline 绘制。其意义在于:折线绘制引擎 IntGdiPolyline 成熟且支持裁剪/画刷/ROP,而贝塞尔本身只是几何描述。离散化精度由 GDI_Bezier 内部的细分算法保证(递归细分直到曲线段足够平直)。IntGdiPolyBezierTo 则在数组头部先插入 ptlCurrent(当前画笔位置),使曲线从当前位置开始,绘制成功后同步更新 ptlCurrent/ptfxCurrent 并清除对应脏位------PolyBezierToPolyBezier 的差别就在"是否使用并更新当前点"。

4.4 SEH 两段式拷贝模式

NtGdiPolyPolyDrawNtGdiGradientFill 都采用"先探测、后拷贝"的两段式 SEH 模式,这是 ReactOS 内核系统服务处理用户缓冲的标准范式:

复制代码
第一段 SEH(只读探测 + 统计):
    ProbeForRead(ptr, size, 1)  →  校验指针与长度可读
    顺带完成参数统计/校验(点数求和、溢出检测、模式字节数)
    异常 → 记录状态码,跳出

第二段 SEH(实际拷贝):
    RtlCopyMemory(内核缓冲, 用户缓冲, size)
    异常 → 释放缓冲,SetLastNtError(异常码),返回 FALSE

第一段用 ProbeForRead 的异常机制代替 逐字节校验(性能),第二段利用"已探测"前提直接拷贝(安全)。nPoints < nMaxPoints 检测则抓住无符号整数求和溢出的场景------nMaxPoints 必然 ≤ 某个子计数 ≤ 总和,若总和回绕变小则说明溢出。这套模式在 NtGdiPolyPolyDraw(Counts+Points 合并单块分配)与 NtGdiGradientFill(Vertex+Mesh 合并单块分配)中表现完全一致,只是后者还额外校验了网格索引范围。

5. NtGdiEllipse ------ 椭圆

5.1 主流程

c 复制代码
BOOL APIENTRY
NtGdiEllipse(HDC hDC, int Left, int Top, int Right, int Bottom)
  1. DC_LockDc(hDC),失败 ERROR_INVALID_HANDLE
  2. 路径分支PATH_IsPathOpen(dc->dclevel)PATH_Ellipse(dc, Left, Top, Right, Bottom)(见 5.4)直接返回。
  3. 退化处理Left == Right || Top == Bottom 返回 TRUE(零面积椭圆不绘制)。
  4. 坐标规范化Right < Left 交换、Bottom < Top 交换,保证 Left<=Right、Top<=Bottom。
  5. 画笔准备PEN_ShareLockPen(pdcattr->hpen)PenOrigWidth = PenWidth = pbrush->lWidthPS_NULL 笔 → PenWidth = 0
  6. PS_INSIDEFRAME 收缩:内框笔样式要求图形整体在包围矩形内,故按笔宽收缩矩形:
c 复制代码
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; 并写回 pbrush->lWidth(用完恢复 PenOrigWidth)。

  1. 坐标变换RectBounds 装入四值后 IntLPtoDP(dc, &RectBounds, 2)(两个 POINT:左上、右下),再加 ptlDCOrig

  2. 计算椭圆参数RadiusX = max((right-left)/2, 2)RadiusY = max((bottom-top)/2, 2)(至少 2,给描边留空间),CenterX/CenterY 取中点。

  3. 填充画刷BRUSH_ShareLockBrush(pdcattr->hbrush);拷贝到临时 BRUSH tmpFillBrushObj(副本),ptOrigin += ptlDCOrigDC_vPrepareDCsForBlit;调 IntFillEllipse(dc, CenterX-RadiusX, CenterY-RadiusY, RadiusX*2, RadiusY*2, &tmpFillBrushObj)

  4. 描边 :填充成功后 IntDrawEllipse(dc, ..., pbrush)

  5. DC_vFinishBlit;恢复 pbrush->lWidth = PenOrigWidth;解锁画刷/画笔/DC。

  • 使用方式 :GDI32 的 Ellipse(hdc, x1, y1, x2, y2) 经系统调用直达。矩形参数界定椭圆的外接矩形。

5.2 IntFillEllipse → app_fill_ellipse(中点算法)

c 复制代码
BOOL FASTCALL
IntFillEllipse(PDC dc, INT XLeft, INT YLeft, INT Width, INT Height, PBRUSH pbrush)
{ return (BOOL)app_fill_ellipse(dc, rect(XLeft, YLeft, Width, Height), pbrush); }

app_fill_ellipsentgdi/drawing.c:617)是经典中点椭圆算法

  • 隐式方程 e(x,y) = b²x² + a²y² - a²b²(a=宽/2,b=高/2),从 (0, b) 出发在第一象限沿椭圆弧推进,用判别式 t = e(x+1, y-1) 与临界值 xcrit = 3a²/4 + 1ycrit = 3b²/4 + 1 决定"向外(x+1)/向下(y-1)/对角"三种步进,二阶差分 dxt/dyt/d2xt/d2yt 增量更新。
  • 关键优化 :不是逐像素写,而是把同一扫描带内等宽的像素列累积成水平矩形 r1(上半)与 r2(下半,镜像),用 app_fill_rect(即 IntFillRect)批量填充,仅在对角步进且矩形有尺寸时才落盘;最后处理中缝重叠/交叉线的补矩形。这是注释所述"accumulates rectangles on adjacent lines which have the same width into a single larger rectangle"。
  • 宽度或高度 <= 2 时直接填充整个矩形。

5.3 IntDrawEllipse → app_draw_ellipse(双椭圆描边)

c 复制代码
BOOL FASTCALL
IntDrawEllipse(PDC dc, INT XLeft, INT YLeft, INT Width, INT Height, PBRUSH pbrush)
{ return (BOOL)app_draw_ellipse(dc, rect(XLeft, YLeft, Width, Height), pbrush); }

app_draw_ellipse(drawing.c:124)用内外两个椭圆 实现带宽度描边:外椭圆半径 a/b,内椭圆半径 A=a-w、B=b-w(w 为笔宽,不足则取 0);两条椭圆弧用同一套中点算法并行推进,填充两者之间的环带矩形(r1/r2),并处理"内椭圆几乎与外椭圆同大"的病态情形(W 取 max(W, w) 保证环带至少 w 宽)。宽 > 半径一半时退化为填充整圆。

5.4 PATH_Ellipse ------ 贝塞尔近似转路径(path.c)

c 复制代码
BOOL PATH_Ellipse(PDC dc, INT x1, INT y1, INT x2, INT y2)
  • PATH_CheckRect:LPtoDP、RECTL_vMakeWellOrdered(RTL 布局可能产生 left>right 的"反矩形",需规范化)、GM_COMPATIBLE 下右/下边界减 1 且零宽/零高返回 FALSE。
  • xRadius = width/2yRadius = height/2;取四个象限点 left/top/right/bottom。
  • 控制点偏移:xOffset = EngMulDiv(width, 55428475, 200000000)(即 width * 0.277142375,等于 k/2 * width,其中 k = 4/3·(1-cos45°)/sin45° ≈ 0.55228475 是三次贝塞尔拟合单位圆弧的经典系数;因为半径 = width/2,k·radius = k·width/2)。注释附有 Stack Overflow 链接"How to create circle with Bézier curves"。
  • 生成 13 个点、4 段三次贝塞尔 :起点 right,然后 right→top(第一段)、top→left(第二段)、left→bottom(第三段)、bottom→right(第四段),每段 3 个控制点(含段终点)。add_points(..., PT_BEZIERTO) 追加,type[0] = PT_MOVETO;若 DCPATH_CLOCKWISEreverse_points 反转 13 点。最后 IntGdiCloseFigure 闭合。
  • 因此路径中的椭圆是贝塞尔近似 (误差极小但非精确椭圆),后续 EndPath+FillPath/StrokePathPathToRegion 都基于这 13 点轮廓。

5.5 中点椭圆算法数学原理

app_fill_ellipse 的每一步决策都对应隐式方程 e(x,y) = b²x² + a²y² - a²b² 在候选像素中点的符号判断:

  • 判别式t = e(x+1, y-1) 是"向外并向下"候选点 (x+1, y-1) 处的函数值;xcrit = 3a²/4 + 1ycrit = 3b²/4 + 1 是椭圆弧上斜率从 -1 过渡到 -∞ 的临界带判定值(对应 45° 分界)。
  • 三种步进:
    • t + a²y < xcrite(x+1, y-1/2) <= 0,椭圆边界在候选点下方,取"向外" x+1
    • t - b²x >= ycrite(x+1/2, y-1) > 0,边界在上方,取"向下" y-1
    • 否则取"对角" x+1, y-1
  • 增量更新t += dxt(向外)、t += dyt(向下)、t += dxt + dyt(对角),其中 dxt = b²(3+2x)dyt = a²(3-2y) 又各自按二阶差分 d2xt = 2b²d2yt = 2a² 递增------全部是整数加法,无乘法、无浮点。
  • 对称性利用:只算第一象限(或第一八分圆),r1(从顶部向下长)与 r2(从底部向上长,镜像)两个矩形同时推进,一次迭代覆盖上下两个像素带。

app_draw_ellipse 在此基础上并行跑两套 同样的算法(外椭圆 a/b 与内椭圆 A/B),并把每行的"外宽 - 内宽"区间 W 夹在 [w, prevx-...] 范围内,填充内外椭圆之间的环带,从而实现任意笔宽的椭圆边框;当内椭圆收缩为 0(笔宽 >= 半径)时直接填充实心圆。

5.6 椭圆绘制中的临时画刷副本

NtGdiEllipse 填充前把共享画刷 pFillBrushObj 拷贝为局部 tmpFillBrushObj 再修改 ptOrigin(加 ptlDCOrig),而不是直接改共享对象------这是内核对象共享安全的典型做法:BRUSH_ShareLockBrush 得到的指针可能被多个线程/进程引用,直接改它的 ptOrigin 会破坏其他使用者的画刷相位。IntRoundRectbrushTemp 的处理同理,只是其原点补偿量是 (RectBounds.left - Left, RectBounds.top - Top)(坐标变换造成的漂移),而非 DC 原点。两处都体现了"共享对象只读、私有副本可写"的并发原则。

6. NtGdiRectangle ------ 矩形

6.1 主流程与旋转分支

c 复制代码
BOOL APIENTRY
NtGdiRectangle(HDC hDC, int LeftRect, int TopRect, int RightRect, int BottomRect)
  • 加锁后判断是否旋转/剪切!(dc->pdcattr->mxWorldToDevice.flAccel & XFORM_SCALE) 意味着世界变换只是平移+缩放(有 XFORM_SCALE 加速标志),此时矩形仍然是轴对齐的,可用多边形捷径:
    • 构造 DestCoords[4]({Left,Top}、{Right,Top}、{Right,Bottom}、{Left,Bottom}),PolyCounts = 4,调 IntGdiPolyPolygon(dc, DestCoords, &PolyCounts, 1)。注释:"Use IntGdiPolyPolygon so to support PATH."------路径打开时自动转入 PATH_PolyPolygon
  • 否则(有旋转/剪切)调用 IntRectangle 走专用矩形路径(矩形 4 条边可以分别画线,无需多边形扫描转换)。
  • 使用方式 :GDI32 Rectangle(hdc, x1, y1, x2, y2)

6.2 IntRectangle

c 复制代码
BOOL FASTCALL
IntRectangle(PDC dc, int LeftRect, int TopRect, int RightRect, int BottomRect)
  1. 路径分支PATH_IsPathOpenPATH_Rectangle(dc, ...)
  2. 规范化 DestRect(min/max),IntLPtoDP,加 ptlDCOrig,再 RECTL_vMakeWellOrdered(RTL 反矩形修正)。
  3. DC_ACCUM_APP|DC_ACCUM_WMGRIntUpdateBoundsRect 累积脏矩形。
  4. GM_COMPATIBLE 语义iGraphicsMode == GM_COMPATIBLEDestRect.right--; DestRect.bottom--;(兼容模式下矩形不含右/下边,与 Windows 兼容)。
  5. DC_vPrepareDCsForBlit;更新填充/线条画刷;取 pbrFill/pbrLine/psurf;空笔/空表面分别处理(空表面返回 TRUE)。
  6. 填充 :非 NULL 填充画刷时,用 IntEngBitBlt(&psurf->SurfObj, NULL, NULL, (CLIPOBJ*)&dc->co, NULL, &DestRect, NULL, NULL, &dc->eboFill.BrushObject, &BrushOrigin, ROP4_FROM_INDEX(R3_OPINDEX_PATCOPY)) 整块 PATCOPY------矩形填充不需要扫描转换,一次位块传输即完成。
  7. 描边
    • 宽笔:创建 5 槽路径,PATH_MoveTo(Left,Top) 后四条 PATH_LineTo 回到起点,state = PATH_ClosedPATH_StrokePath 描边。
    • 细笔:Mix = ROP2_TO_MIX(pdcattr->jROP2),四条 IntEngLineTo 画上、右、下、左边(坐标取自设备坐标 DestRect),逐条 ret = ret && ... 累积结果。
  8. DC_vFinishBlit。MSDN 注释提醒:Rectangle 不使用也不更新当前光标位置

6.3 PATH_Rectangle(path.c)

  • PATH_CheckRect 校验后,构造 4 点:(right,top)、(left,top)、(left,bottom)、(right,bottom)(注意逆时针走向),DCPATH_CLOCKWISE 时反转。
  • add_points(..., PT_LINETO) 追加,type[0] = PT_MOVETOIntGdiCloseFigure 闭合。

6.4 IntGdiFillRgn ------ 区域填充路径(bitblt.c)

虽然 IntRectangle 走 PATCOPY 快速路径,但其他形状(如 FrameRectFillRgn 等 API)最终会汇聚到 IntGdiFillRgn(bitblt.c:1228),此处一并展开,因为它与本文件的多边形填充在概念上是姊妹机制:

c 复制代码
BOOL
IntGdiFillRgn(_In_ PDC pdc, _In_ PREGION prgn, _In_opt_ PBRUSH pbrFill)
  1. 无表面 → TRUE。
  2. 创建临时区域 prgnClipREGION_LPTODP(pdc, prgnClip, prgn) 把区域从逻辑坐标变换到设备坐标。
  3. 与可见性区域求交pdc->prgnRao 存在则 RGN_AND 其与 RAO 区域,否则与 prgnVis 求交(注释指出这两个区域目前不含 DC 原点偏移)。
  4. REGION_bOffsetRgn(prgnClip, ptlDCOrig.x, ptlDCOrig.y) 补上 DC 原点。
  5. 脏矩形累积(DC_ACCUM_APP|DC_ACCUM_WMGR 时)。
  6. IntEngInitClipObj + IntEngUpdateClipRegion 把区域转成 XCLIPOBJ
  7. 生成 MIXrop2Fg = FIXUP_ROP2(jROP2)mix = rop2Fg | (jBkMode == OPAQUE ? rop2Fg : R2_NOP) << 8------前景按 ROP2,背景模式为 OPAQUE 时背景同前景,否则背景为 NOP(透明)。
  8. 画刷:显式传入 pbrFillEBRUSHOBJ_vInit 初始化私有画刷对象;否则用 DC 当前填充画刷(按脏标记更新)。
  9. IntEngPaint(&SurfObj, &xcoClip, pbo, &ptlFillOrigin, mix) 完成实际填充。
  10. DC_vFinishBlit、删除临时区域、释放裁剪资源。

IntFillPolygon 的差异:多边形是先扫描转换出像素级线段 再逐段 PATCOPY;区域填充则是把区域矩形带rdh.nCountRECTBuffer)直接作为裁剪源,由引擎一次性刷涂。

7. NtGdiRoundRect ------ 圆角矩形

7.1 IntRoundRect 主流程

c 复制代码
BOOL FASTCALL
IntRoundRect(PDC dc, int Left, int Top, int Right, int Bottom,
             int xCurveDiameter, int yCurveDiameter)
  • 路径分支PATH_IsPathOpenPATH_RoundRect(dc, Left, Top, Right, Bottom, xCurveDiameter, yCurveDiameter)
  • 退化:Left == Right || Top == Bottom 返回 TRUE。
  • 曲线直径规范化xCurveDiameter = max(abs(xCurveDiameter), 1)(同理 y)------直径取绝对值且至少 1,避免除零/负值。
  • 坐标规范化(交换 Left/Right、Top/Bottom)。
  • 画笔准备与 PS_INSIDEFRAME 收缩逻辑与椭圆完全一致。
  • RectBounds LPtoDP + ptlDCOrig 偏移。
  • 填充BRUSH_ShareLockBrush(hbrush),拷贝到 brushTempbrushTemp.ptOrigin += (RectBounds.left - Left, RectBounds.top - Top)(补偿坐标变换引起的画刷原点漂移),调 IntFillRoundRect(dc, RectBounds.left, RectBounds.top, RectBounds.right, RectBounds.bottom, xCurveDiameter, yCurveDiameter, &brushTemp)
  • 描边 :成功后 IntDrawRoundRect(dc, ..., pbrLine)
  • 恢复笔宽、解锁。

NtGdiRoundRect 只是加锁/解锁包装(失败 ERROR_INVALID_HANDLE)。

7.2 IntFillRoundRect / IntDrawRoundRect(drawing.c)

IntFillRoundRect(drawing.c:1388)把圆角矩形拆成四条圆角弧(app_fill_arc)+ 三个矩形(app_fill_rect) 。设 r 为外接矩形、rx = Wellipse/2ry = Hellipse/2

  • Wellipse > r.width && Hellipse > r.height:曲线直径超出矩形,直接 app_fill_ellipse 填满。
  • Wellipse > r.width(仅宽超):上下两个半椭圆弧 app_fill_arc(..., 0,180)(..., 180,360)
  • Hellipse > r.height(仅高超):左右两个半椭圆弧 (..., 90,270)(..., 270,90)
  • 正常情形:四个 90° 圆角弧(左上 90,180、左下 180,270、右下 270,360、右上 0,90),再加三段矩形:上/下水平条 rect(r.x+rx, r.y, r.width-rx-rx, ry+1)rect(r.x+rx, r.y+r.height-ry+1, r.width-rx-rx, ry-1)、中央竖条 rect(r.x, r.y+ry+1, r.width, r.height-ry-ry)
  • IntDrawRoundRect(drawing.c:1454)同构,只是把填充弧换成 app_draw_arc、填充矩形换成 app_fill_rect(..., TRUE)(以笔宽 w 画边框条)。

7.3 PATH_RoundRect(path.c)------ 16 点贝塞尔

  • 椭圆直径先 LPtoDP 再 RECTL_vMakeWellOrdered,取 ell_width = min(ellipse宽, rect宽)ell_height = min(ellipse高, rect高);直径为零回退 PATH_Rectangle
  • 控制点偏移:xOffset = EngMulDiv(ell_width, 44771525, 200000000),即 ell_width·(1-0.5522847)/2------圆角处贝塞尔控制点相对 90° 角点的内缩距离((1-k)·半径,k≈0.55228475)。
  • 16 个点按顺序:右上弧(3 点)→ 上边中点 → 左上弧(3 点)→ 左边中点 → 左下弧(3 点)→ 下边中点 → 右下弧(3 点)→ 回到起点右侧。type[0]=PT_MOVETOtype[4]=type[8]=type[12]=PT_LINETO(中间的水平/垂直直线段),其余 PT_BEZIERTODCPATH_CLOCKWISE 时反转;IntGdiCloseFigure 闭合。

7.4 app_fill_arc / app_draw_arc ------ 圆弧光栅化

IntFillRoundRect/IntDrawRoundRect 中的圆角弧最终落在 app_fill_arc(drawing.c:785)与 app_draw_arc(同文件)上。这两个函数以 start_angle/end_angle(单位:度,0°=右侧、逆时针)界定椭圆弧范围:

  • app_boundary_point(r, angle)(drawing.c:741):计算椭圆边界上给定角度的点。对 8 个特殊角度(0/45/90/135/180/225/270/315)直接返回轴对齐/对角点(避免 tan 的奇点);其余角度用参数方程 cx ± height/2·(1/tan) cy ± width/2·tan(依据角度所在象限选择水平/垂直投影,防止 tan 爆炸)。
  • app_fill_arc:把椭圆弧当作"从 start 到 end 的扇形"扫描------从弧的起点出发,用中点算法沿弧推进到终点,同时填充弧与圆心之间的区域(与 app_fill_ellipse 的矩形累加不同,弧填充按角度裁剪,只处理角内的矩形带)。
  • app_draw_arc:类似 app_draw_ellipse 的内外双椭圆方案,但只渲染角度范围内的环带,用于圆角矩形的边框弧。

由于 IntFillRoundRect 只使用 0/90/180/270 这四个整 90° 弧段,app_boundary_point 在这些调用中都命中特例分支,不会触碰浮点 tan 路径------浮点分支仅服务于通用的 Arc/Pie/Chord API(drawing.cIntDrawArc/IntFillArc 的入口,见 GdiTypeArc/Pie/Chord 相关代码)。

7.5 圆角矩形的画刷原点补偿

IntRoundRect 中一个容易被忽略的细节:填充前 brushTemp.ptOrigin += (RectBounds.left - Left, RectBounds.top - Top)。由于 IntLPtoDP 之后 RectBounds 与原始 Left/Top 之间存在变换差,若不补偿,画刷图案(如棋盘格)在圆角矩形内部会出现相位错位 ------圆角弧与矩形条的填充边界上图案无法对齐。椭圆路径没有此问题,因为它的画刷副本只加 DC 原点(ptlDCOrig),而矩形偏移恰好为零(RectBounds.left - Left == ptlDCOrig.x 的语义差异取决于变换)。

8. 渐变填充:NtGdiGradientFill → GreGradientFill → IntEngGradientFill

8.1 NtGdiGradientFill ------ SEH 探测与缓冲

c 复制代码
BOOL APIENTRY
NtGdiGradientFill(HDC hdc, PTRIVERTEX pVertex, ULONG nVertex,
                  PVOID pMesh, ULONG nMesh, ULONG ulMode)
  1. 快速校验:!pVertex || !nVertex || !pMesh || !nMeshERROR_INVALID_PARAMETER
  2. 按模式计算网格字节数:RECT_H/RECT_V → nMesh * sizeof(GRADIENT_RECT);TRIANGLE → nMesh * sizeof(GRADIENT_TRIANGLE);未知模式 → ERROR_INVALID_PARAMETER
  3. 溢出检查cbVertex + cbMesh <= cbVertex 判定加法回绕 → FALSE(不设 last error)。
  4. 单块分配 cbVertex + cbMesh 字节(TAG_SHAPE),SafeMesh = SafeVertex + cbVertex
  5. SEH:ProbeForRead 两段后 RtlCopyMemory 拷入;异常 → 释放缓冲、SetLastNtError、返回 FALSE。
  6. GreGradientFill(hdc, SafeVertex, nVertex, SafeMesh, nMesh, ulMode),释放缓冲返回结果。

8.2 GreGradientFill ------ 参数校验与准备

c 复制代码
BOOL NTAPI
GreGradientFill(HDC hdc, PTRIVERTEX pVertex, ULONG nVertex,
                PVOID pMesh, ULONG nMesh, ULONG ulMode)
  1. 顶点索引校验 :TRIANGLE 模式逐网格检查 Vertex1/2/3 < nVertex;矩形模式检查 UpperLeft/LowerRight < nVertex;越界 → ERROR_INVALID_PARAMETER。(注意:此处访问的是内核缓冲 pMesh,故无需 SEH。)
  2. DC_LockDc;无表面(内存 DC 未选位图)→ 解锁返回 TRUE(注释 CHECKME)。
  3. 计算整体包围盒 rclExtent:遍历所有顶点取 min/max x、y;IntLPtoDP 转设备坐标;加 ptlDCOrigRECTL_vMakeWellOrdered 规范化;空矩形直接返回 TRUE。
  4. 矩形模式顶点变换ulMode == RECT_H || RECT_V 时,把每个顶点 IntLPtoDP 并加 ptlDCOrig(三角形模式的顶点不在这里变换------它们由引擎内按边插值时处理?实际上 IntEngGradientFillTriangle 直接使用顶点坐标,说明三角形顶点在调用前未统一转换,这是一个值得注意的差异点;矩形顶点因引擎按矩形区域做水平/垂直逐行插值,需要精确设备坐标)。
  5. 抖动原点ptlDitherOrg 置 (0,0) 后 LPtoDP 加偏移------渐变在平移变换下保持抖动相位一致。
  6. 脏矩形累积、DC_vPrepareDCsForBlit
  7. EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0, 0):以 RGB 调色板为源、目标表面调色板为目标的颜色翻译对象。
  8. IntEngGradientFill(&psurf->SurfObj, (CLIPOBJ*)&pdc->co, &exlo.xlo, pVertex, nVertex, pMesh, nMesh, &rclExtent, &ptlDitherOrg, ulMode)
  9. EXLATEOBJ_vCleanupDC_vFinishBlit、解锁 DC。

8.3 引擎侧:IntEngGradientFill → EngGradientFill(eng/gradient.c)

IntEngGradientFill(gradient.c:557)是引擎包装:若目标表面设置 HOOK_GRADIENTFILL(显示驱动实现了 DrvGradientFill),则调用 GDIDEVFUNCS(psoDest).GradientFill(...) 交给驱动;否则走软件实现 EngGradientFill(gradient.c:479)。

EngGradientFillulMode 分发:

  • RECT_H / RECT_V :遍历每个 GRADIENT_RECT,调 IntEngGradientFillRect(..., (ulMode == GRADIENT_FILL_RECT_H))
  • TRIANGLEIntEngIsNULLTriangle 检测任意两顶点重合的退化三角形(COMPAREVERTEX 比较 x/y),跳过;否则 IntEngGradientFillTriangle

8.4 链一:IntEngGradientFillRect(矩形水平/垂直渐变)

IntEngGradientFillRect(gradient.c:46):

  • v1 = pVertex[UpperLeft]v2 = pVertex[LowerRight];渐变矩形 rcGradient 由两顶点 min/max 构成;插值跨度 dy:水平 = |right-left|,垂直 = |bottom-top|
  • IntEngEnter 进入表面(含平移 Translate)。
  • 双色不同且跨度 > 1 时逐像素插值:
    • 颜色插值用 Bresenham 增量式(宏 HVINITCOL/HVSTEPCOL):c[id] = v1->Col>>8dc[id] = |(v2->Col>>8) - c[id]|ec[id] = -(dy>>1)(误差项半初始化),ic[id] = ±1 步进;HVSTEPCOLec += dc; while(ec>0){c+=ic; ec-=dy;} 推进------每条线的起点颜色线性插值,且用误差扩散避免累加漂移
    • 水平渐变 (RECT_H):对每个裁剪矩形,按列 y ∈ [left, right)DIB_VLine 画竖线(每列颜色不同)。
    • 垂直渐变 (RECT_V):按行 y ∈ [top, bottom)DIB_HLine 画横线。
    • 裁剪:CLIPOBJ_cEnumStart/bEnum 枚举 CT_RECTANGLES,与 rcSG 求交得 FillRect
  • 单色v1v2 的 RGB 全相同或 dy <= 1):不插值,直接以 v1 颜色对每个裁剪矩形逐行 DIB_HLine

8.5 链二:IntEngGradientFillTriangle(三角形渐变)

IntEngGradientFillTriangle(gradient.c:315):

  • 三个顶点按 SMALLER(先 y 后 x)冒泡排序为 v1≤v2≤v3。
  • 三顶点 RGB 不全相同(VCMPCLRS 为真)时走渐变路径:
    • 三条边 DDA 初始化:INITLINE(a,b,line) 记录 x/sx/dx/dy/incx/ex/destx,INITCOL 初始化边两端颜色插值(ec = -(dy>>1) 误差项)。
    • sy = v1->y + ditherYbt = min(v3->y + ditherY, FillRect.bottom) 逐扫描线:DOLINE 步进左边 v1→v3 与上边 v1→v2 的 x 和颜色;FILLLINE(0,1) 从左边到上边填充当前行,FILLLINE 内部用 FINITCOL/FDOCOL行内 再做一次 Bresenham 颜色插值(gx = |sx[lineto]-sx[linefrom]|),逐像素 DIB_PutPixel;跨越 v2 后改用 FILLLINE(0,2) 从左边到下边。
    • 每像素的颜色 = 先沿两条边各插值一次、再沿行内插值一次(双线性插值的扫描线实现)。
  • 纯色三角形 (三顶点同色):用 S_* 宏系列(S_INITLINE/S_DOLINE/S_FILLLINE)同样逐行推进,但行内直接 DIB_HLine 画实线,不逐像素插值。

8.6 三条链汇总

复制代码
NtGdiGradientFill(SEH 探测+拷贝)
  └─ GreGradientFill(索引校验+包围盒+EXLATEOBJ)
       └─ IntEngGradientFill(gradient.c)
            ├─ surface 钩子 HOOK_GRADIENTFILL → 驱动 GradientFill
            └─ EngGradientFill
                 ├─ GRADIENT_FILL_RECT_H/V → IntEngGradientFillRect
                 │     ├─ 水平:逐列 DIB_VLine(HVINITCOL/HVSTEPCOL 插值)
                 │     └─ 垂直:逐行 DIB_HLine(同上)
                 └─ GRADIENT_FILL_TRIANGLE → IntEngGradientFillTriangle
                       ├─ 渐变:三边 DDA + 行内 FILLLINE 插值 + DIB_PutPixel
                       └─ 纯色:S_* 宏 + DIB_HLine

8.7 Bresenham 式颜色插值的误差控制

渐变引擎在两处使用"误差项 + 增量比较"的插值(而非直接 color = a + (b-a)*t 的浮点乘加):

  1. HVINITCOL/HVSTEPCOL (矩形渐变,gradient.c:29):误差 ec 初始 -(dy>>1),每步 ec += dc[id]ec > 0c += icec -= dy。这等价于用分数 dc/dy 累加逼近线性函数,每次只在误差越过阈值时进 1------避免了浮点累加误差,也避免了每像素的乘除法。所有运算为整数。
  2. FILLLINE/FDOCOL (三角形行内插值,gradient.c:237):对每一条水平扫描线,在左右边界色之间再用同样手法插值:FINITCOL 初始化 gc/gd/ge/gige = -(gx>>1)),FDOCOL 每像素推进。注意它在每条线上重新初始化误差项,因此每行都是独立、无跨行误差累积的插值。

此外 INITLINE/INITCOL(三角形边)的 ex[line] = -(dy[line]>>1) 与多边形扫描线 POLYGONFILL_MakeEdgeError = ErrorMax/2 是同一思想:半进位舍入保证插值结果对称(正向与反向遍历得到一致结果)。

8.8 EXLATEOBJ 颜色翻译机制

GreGradientFillNtGdiExtFloodFill 都依赖 EXLATEOBJ 完成"逻辑颜色 → 表面像素值"的翻译:

  • EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0, 0):源调色板为 gpalRGB(内置 RGB 调色板),目标为表面调色板 psurf->ppal,无特殊标志。之后 XLATEOBJ_iXlate(&exlo.xlo, rgb) 返回目标格式下的像素值。
  • 洪水填充使用掩码 0xffffffEXLATEOBJ_vInitialize(..., 0, 0xffffff, 0)):只翻译 RGB 低 24 位,Alpha 高位忽略。
  • 对 32bpp 表面,翻译通常是恒等映射;对 8bpp 调色板表面则查表取最近颜色;对 1/4/16bpp 同样由翻译表处理。渐变/洪水代码因此与位深解耦 ------引擎只与 XLATEOBJ 交互,所有格式差异被隔离在翻译表构建逻辑里。

EXLATEOBJ 还承担"抖动/调色板匹配"职责(iXlate 后可附加抖动),这与 ptlDitherOrg 参数配合:ptlDitherOrg 使抖动相位随 DC 平移而平移,保证相邻两次渐变填充的抖动图案连续。

8.9 与显示驱动的交互:HOOK_GRADIENTFILL

IntEngGradientFill 的分派逻辑:

c 复制代码
if (psurf->flags & HOOK_GRADIENTFILL)
    Ret = GDIDEVFUNCS(psoDest).GradientFill(psoDest, pco, pxlo, pVertex,
                                            nVertex, pMesh, nMesh,
                                            prclExtents, pptlDitherOrg, ulMode);
else
    Ret = EngGradientFill(...);   /* 软件实现 */
  • HOOK_GRADIENTFILL 是 win32k 内部对 SURFACE.flags 的钩子标志(对应驱动 DrvGradientFill 的支持声明)。支持硬件加速渐变的显示驱动(如带 GPU 的驱动)会在表面标志中设置它,从而让渐变在驱动内完成;大多数 DIB 表面不设该标志,落入软件实现。
  • GDIDEVFUNCS(psoDest) 从表面的设备函数表取出 GradientFill 入口------这是 ReactOS 驱动模型(DRVFN/EngDeviceFunctions 机制)的一部分。
  • 软件路径本身又分两层:EngGradientFill(按模式分发)→ IntEngGradientFillRect/IntEngGradientFillTriangle(逐像素光栅化)。IntEngEnter/IntEngLeave 负责表面的锁定/平移/裁剪进入退出。

9. NtGdiExtFloodFill ------ 洪水填充

9.1 内核入口

c 复制代码
BOOL APIENTRY
NtGdiExtFloodFill(HDC hDC, INT XStart, INT YStart, COLORREF Color, UINT FillType)
  1. DC_LockDc;无表面 → 返回 TRUE。
  2. 起点 Pt(x, y)IntLPtoDP 转设备坐标(XStart/YStart 是逻辑坐标)。
  3. DC_vPrepareDCsForBlit
  4. 裁剪区域起点检查prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis;若区域存在,REGION_PtInRegion(prgn, Pt) 判断起点是否在可见/RAO 区域内,不在则直接结束(返回 FALSE);在则取区域包围盒 DestRect 作为洪水填充范围;区域为空则用整个位图尺寸。
  5. 脏矩形累积。
  6. EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0xffffff, 0)(掩码 0xffffff 只翻译 RGB)。
  7. ConvColor = XLATEOBJ_iXlate(&exlo.xlo, Color) 把用户 COLORREF 翻译为目标表面像素值。
  8. DIB_XXBPP_FloodFillSolid(&psurf->SurfObj, &dc->eboFill.BrushObject, &DestRect, &Pt, ConvColor, FillType)
  9. DC_vFinishBlitEXLATEOBJ_vCleanup、解锁。
  • 注释说明了设计取舍:"Only solid fills supported for now"------图案画刷与无 DIB 函数的非常规表面的支持方案(逐像素 DrvPatBlt / MaskBlt 掩码)尚未实现。
  • 使用方式 :GDI32 ExtFloodFill(hdc, x, y, color, FLOODFILLBORDER|FLOODFILLSURFACE)FillType 语义:FLOODFILLBORDER 以 Color 为边界色、填充边界内部;FLOODFILLSURFACE 以 Color 为表面色、填充所有同色连通区域。

9.2 DIB_XXBPP_FloodFillSolid ------ 迭代四邻域洪水填充(dib/floodfill.c)

数据结构:

c 复制代码
typedef struct _floodItem { ULONG x; ULONG y; } FLOODITEM;
typedef struct _floodInfo {
    ULONG floodLen;
    FLOODITEM *floodStart;   // 栈顶指针
    FLOODITEM *floodData;    // 栈底
} FLOODINFO;
  • 注释明示这是"iterative four neighbors version"(迭代四邻域版),用内部栈式数组initFlood 一次性按表面宽×高分配 FLOODITEM 数组(最坏情况=全部像素),floodStart 从数组末尾开头 生长(--floodStart 入栈、++floodStart 出栈),避免频繁分配/释放;栈溢出时(floodStart - 4 < floodData)打印 "Can't finish flooding!" 并失败返回。
  • addItemFlood 入栈前做三重检查:
    1. RECTL_bPointInRect:越出填充范围(裁剪区域包围盒)不入栈;
    2. pixel == BrushColor已经涂成填充色的像素跳过 ------这正是防重复的关键(出栈后先 DIB_PutPixel 再入栈四邻域,邻域再次遇到该像素时颜色已变,自动排除,栈大小有保证);
    3. isSurf 区分规则:FLOODFILLSURFACE 要求 pixel == SurfaceColor;FLOODFILLBORDER 要求 pixel != SurfaceColor(SurfaceColor 此时即边界色 ConvColor)。
  • 主循环(以 FLOODFILLBORDER 为例):检查起点像素不是边界色(是则 FALSE)→ initFlood → 起点入栈 → 循环:出栈 → DIB_PutPixel(x,y,BrushColor) → 上下左右四邻域 addItemFlood
  • DIB_GetPixel/DIB_PutPixelDibFunctionsForBitmapFormat[iBitmapFormat] 函数表,天然支持 1/4/8/16/24/32bpp。
  • 返回值:完成 TRUE;分配失败/栈溢出/起点即边界(或非表面色)FALSE。

9.3 内部栈的增长方向

FLOODINFO 的栈从 floodData(低地址)向 floodStart = floodData + width*height(高地址)反向生长,出栈则指针向高地址回移。图示如下:

复制代码
floodData                                   floodStart(=数组末尾)
   │                                            │
   ▼                                            ▼
   ┌──────┬──────┬──────┬──────┬──────┬──────┬──┐
   │ 未用 │ 未用 │ 未用 │ 未用 │itemN │ ...  │item1│  ← 已入栈项从末尾向左生长
   └──────┴──────┴──────┴──────┴──────┴──────┴──┘
   ←─── 入栈方向 ───        ↑floodStart(当前栈顶,向低地址移动)

设计动机:数组按最坏情况(整个表面全部入栈)一次性分配,避免逐像素 EngAllocMem 的开销;栈溢出检测 floodStart - 4 < floodData 预留 4 个槽位余量(一次入栈最多 4 个邻域)。该算法本质是 DFS(深度优先),顺序为"下、上、右、左",与 BFS 相比栈空间需求最大但实现最简。

9.4 与 Windows ExtFloodFill 语义对照

Windows 行为 ReactOS 实现
FLOODFILLBORDER 以 Color 为边界色,填充被边界包围的连通区域(起点不能是边界色) 一致:起点为边界色即失败;addItemFloodisSurf=FALSE(pixel != 边界色)
FLOODFILLSURFACE 以 Color 为表面色,填充所有同色连通区域(起点必须是表面色) 一致:起点非表面色即失败;isSurf=TRUE(pixel == 表面色)
画刷 用当前画刷填充(支持图案) Brush->iSolidColor 纯色("Only solid fills supported for now"
裁剪 受设备裁剪区约束 prgnRao/prgnVis 包围盒 DestRect 约束
边界 填充到画刷/表面边界 DstRect(区域包围盒)为硬边界

由此可见洪水填充在语义上对齐 Windows,但图案画刷支持与"裁剪区逐矩形精确定界"仍是 TODO。

9.5 洪水填充的复杂度与内存行为

  • 空间复杂度initFloodwidth × heightDestRect 包围盒尺寸)分配 FLOODITEM(8 字节)。对于全屏 1920×1080 表面即约 16.6MB 的 PagedPool 一次性分配------这是"最坏情况"策略的代价。floodStart - 4 < floodData 检测理论上几乎不会触发(因为每个已入栈像素都已满足填充规则且被涂色排除,栈深不可能超过可填充像素数),它更像防御性检查。
  • 时间复杂度:每个被填充像素恰好访问一次(出栈 → PutPixel → 四邻域检查),O(填充面积)。四邻域 DFS 可能产生"蛇形"长栈,但内存已预分配,无动态扩容。
  • 表面类型DIB_GetPixel/DIB_PutPixelDibFunctionsForBitmapFormat 取函数指针,对 1bpp 表面是位操作、对 24bpp 是字节三元组、对 32bpp 是 DWORD 写------洪水填充在任意位深上运行同一套逻辑。
  • 已知缺陷DestRect 只是区域包围盒而非精确裁剪区,理论上会把包围盒内、裁剪区外的像素也填充(引擎层未做逐矩形裁剪)。对窗口场景,这依赖 prgnRao/prgnVis 一般接近矩形的事实,绝大多数情况下无感知。

10. 通用机制专题

10.1 填充模式判定(WINDING/ALTERNATE)

位置 判定依据 行为
polyfill.c FillPolygon pdcattr->jFillMode == WINDING 选择 POLYGONFILL_FillScanLineWinding(环绕数非零填充)或 POLYGONFILL_FillScanLineAlternate(奇偶填充)
polyfill.c IntFillPolygon 无(固定交替) IntGdiPolygon 的填充路径,成对截距用 IntEngBitBlt PATCOPY
POLYGONFILL_MakeEdge 边方向 YDirection = ±1(环绕数计算依据),Error = -1(上行边)或 0
PATH_RoundRect/Ellipse/Rectangle dc->dclevel.flPath & DCPATH_CLOCKWISE 反转点序,使路径轮廓方向与"顺时针路径"约定一致

POLYGONFILL_FillScanLineWinding 的具体做法:维护 winding 累加量(每条活动边的 YDirection),当前累加值非零时把新截距区间并入当前线段,否则结束当前线段并绘制------实现"环绕数≠0 的区域被填充"。

10.2 椭圆/圆角矩形的双轨实现对比

场景 路径模式(BeginPath 中) 直接绘制模式
椭圆 PATH_Ellipse:4 段三次贝塞尔、13 点,控制点偏移 k·radius(k≈0.55228475,常量 55428475/2e8) IntFillEllipse(中点算法,矩形累加)+ IntDrawEllipse(双椭圆环带)
圆角矩形 PATH_RoundRect:4 段贝塞尔 + 4 条直线、16 点,控制点偏移 (1-k)·radius(常量 44771525/2e8) IntFillRoundRect/IntDrawRoundRect:4 个 90° 弧 + 矩形条
矩形 PATH_Rectangle:4 点 LINETO IntRectangle:PATCOPY 整块填充 + 4 条 IntEngLineTo

路径模式输出的是可继续编辑的轮廓 (后续 EndPath/FillPath/StrokePath/PathToRegion/GetPath 都可用),直接绘制模式输出的是像素

10.3 坐标与兼容性约定

  • 所有形状先 IntLPtoDP+= ptlDCOrig(DC 原点平移),统一到设备坐标后交给引擎。
  • RECTL_vMakeWellOrdered 处理 RTL 布局产生的"反矩形"(left>right)。
  • GM_COMPATIBLE 下右/下边界不包含(right--; bottom--),兼容 Windows 兼容模式的矩形/路径语义。
  • PS_INSIDEFRAME 笔按笔宽收缩外接矩形,保证边框全部落在矩形内部。
  • IntIsEffectiveWidePen(pen.h):lWidth > 1 && (BR_IS_OLDSTYLEPEN || (ulPenStyle & PS_TYPE_MASK) == PS_GEOMETRIC)------只有"旧式加宽笔或几何笔"才需要路径描边,普通笔逐边画线即可。

10.4 文件内遗留代码

  • Rsin(d)/Rcos(d) 宏(fillshap.c 顶部):定义了但全文件未使用,疑似早期三角函数椭圆算法的残留。
  • #if 0 块:FillPolygon 的 extern 声明与 ALTERNATE/WINDING 语义注释(polyfill.c 有实现但 fillshap.c 未启用)。
  • NtGdiPolyPolyDrawGdiPolyBezierTo 分支注释引用 "From Wine 10.0 dlls/win32u/painting.c"------说明该分支的 3n+1 校验规则移植自 Wine。

10.5 性能与正确性评注

  1. 椭圆/圆角矩形选择路径还是中点算法 :路径模式(BeginPath 内)必然产生 13/16 点的贝塞尔轮廓,后续 FillPath 还要再经路径填充引擎做一次扫描转换;直接模式一次完成。因此非路径场景下,ReactOS 刻意保留中点算法以获得更好的性能与像素精确性。
  2. 多边形填充的双引擎IntFillPolygon(BitBlt 填充)与 FillPolygon(LineTo 填充)并存------前者每扫描线段一次 IntEngBitBlt(适合画刷 PATCOPY,但每行一次调用开销不小),后者每线段一次 IntEngLineTo。两者都没有使用区域(REGION)批量技术,扫描线极多时性能是 O(行数 × 调用开销)。
  3. 整数插值的可复现性:渐变与多边形扫描都用"误差项 + 半进位"的整数算法,保证同样输入在任何平台产生完全一致的像素(不依赖 FPU/编译器优化),这对图形兼容性测试至关重要。
  4. RECTL_vMakeWellOrdered 的必要性 :RTL(从右到左)布局下,逻辑坐标经 IntLPtoDP 后可能产生 left > right 的矩形。若不规范化,IntEngBitBlt/DIB_HLine 会得到负宽度而崩溃或画错。所有形状入口都做了这道防护。

10.6 与 Windows/Wine 的行为差异

差异点 Windows ReactOS 现状
Polygon 填充模式 严格遵循 SetPolyFillMode(WINDING/ALTERNATE) IntGdiPolygon 固定交替填充;WINDING 仅存在于未被启用的 FillPolygon 路径
GdiPolyBezierTo 参数校验 点数须 3n+1 同 Wine 10.0:Count==1 && n%3==1 才接受,SafeCounts[0]-- 处理首点
NtGdiPolyPolyDraw 静默失败 部分参数错误不设 last error 已对齐(空指针/溢出/异常均静默)
渐变三角形顶点坐标 统一走世界变换 矩形模式顶点在 GreGradientFill 内 LPtoDP,三角形模式顶点未统一转换(潜在不一致)
ExtFloodFill 画刷 支持图案画刷 仅纯色

这些差异多为"已知 TODO"而非缺陷,体现了 ReactOS 以最小实现覆盖主流用例、逐步对齐的演进策略。

11. 调用链(mermaid)

#mermaid-svg-gSXmSjrSabP13GbE{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-gSXmSjrSabP13GbE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gSXmSjrSabP13GbE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gSXmSjrSabP13GbE .error-icon{fill:#552222;}#mermaid-svg-gSXmSjrSabP13GbE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gSXmSjrSabP13GbE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gSXmSjrSabP13GbE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gSXmSjrSabP13GbE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gSXmSjrSabP13GbE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gSXmSjrSabP13GbE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gSXmSjrSabP13GbE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gSXmSjrSabP13GbE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gSXmSjrSabP13GbE .marker.cross{stroke:#333333;}#mermaid-svg-gSXmSjrSabP13GbE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gSXmSjrSabP13GbE p{margin:0;}#mermaid-svg-gSXmSjrSabP13GbE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gSXmSjrSabP13GbE .cluster-label text{fill:#333;}#mermaid-svg-gSXmSjrSabP13GbE .cluster-label span{color:#333;}#mermaid-svg-gSXmSjrSabP13GbE .cluster-label span p{background-color:transparent;}#mermaid-svg-gSXmSjrSabP13GbE .label text,#mermaid-svg-gSXmSjrSabP13GbE span{fill:#333;color:#333;}#mermaid-svg-gSXmSjrSabP13GbE .node rect,#mermaid-svg-gSXmSjrSabP13GbE .node circle,#mermaid-svg-gSXmSjrSabP13GbE .node ellipse,#mermaid-svg-gSXmSjrSabP13GbE .node polygon,#mermaid-svg-gSXmSjrSabP13GbE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gSXmSjrSabP13GbE .rough-node .label text,#mermaid-svg-gSXmSjrSabP13GbE .node .label text,#mermaid-svg-gSXmSjrSabP13GbE .image-shape .label,#mermaid-svg-gSXmSjrSabP13GbE .icon-shape .label{text-anchor:middle;}#mermaid-svg-gSXmSjrSabP13GbE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gSXmSjrSabP13GbE .rough-node .label,#mermaid-svg-gSXmSjrSabP13GbE .node .label,#mermaid-svg-gSXmSjrSabP13GbE .image-shape .label,#mermaid-svg-gSXmSjrSabP13GbE .icon-shape .label{text-align:center;}#mermaid-svg-gSXmSjrSabP13GbE .node.clickable{cursor:pointer;}#mermaid-svg-gSXmSjrSabP13GbE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gSXmSjrSabP13GbE .arrowheadPath{fill:#333333;}#mermaid-svg-gSXmSjrSabP13GbE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gSXmSjrSabP13GbE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gSXmSjrSabP13GbE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSXmSjrSabP13GbE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gSXmSjrSabP13GbE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSXmSjrSabP13GbE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gSXmSjrSabP13GbE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gSXmSjrSabP13GbE .cluster text{fill:#333;}#mermaid-svg-gSXmSjrSabP13GbE .cluster span{color:#333;}#mermaid-svg-gSXmSjrSabP13GbE 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-gSXmSjrSabP13GbE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gSXmSjrSabP13GbE rect.text{fill:none;stroke-width:0;}#mermaid-svg-gSXmSjrSabP13GbE .icon-shape,#mermaid-svg-gSXmSjrSabP13GbE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSXmSjrSabP13GbE .icon-shape p,#mermaid-svg-gSXmSjrSabP13GbE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gSXmSjrSabP13GbE .icon-shape .label rect,#mermaid-svg-gSXmSjrSabP13GbE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSXmSjrSabP13GbE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gSXmSjrSabP13GbE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gSXmSjrSabP13GbE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NtGdiPolyPolyDraw
GdiPolyPolygon
GdiPolyPolyLine
GdiPolyBezier
GdiPolyLineTo
GdiPolyBezierTo
GdiPolyPolyRgn
路径打开?
逐个子多边形
填充
宽笔描边
细笔描边
NtGdiEllipse
路径打开
NtGdiRectangle
XFORM_SCALE
填充
路径打开
描边
NtGdiRoundRect
路径打开
NtGdiGradientFill
驱动钩子
RECT_H/V
TRIANGLE
NtGdiExtFloodFill
GDI32 用户态: Polygon/PolyPolygon
NtGdiPolyPolyDraw
IntGdiPolyPolygon
IntGdiPolyPolyline
IntGdiPolyBezier
IntGdiPolylineTo
IntGdiPolyBezierTo
GreCreatePolyPolygonRgn
PATH_PolyPolygon
IntGdiPolygon
IntFillPolygon
PATH_StrokePath
IntEngLineTo
POLYGONFILL_MakeEdgeList
逐扫描线 BuildActiveList
IntEngBitBlt PATCOPY
GDI32: Ellipse
NtGdiEllipse
PATH_Ellipse
IntFillEllipse
app_fill_ellipse 中点算法
IntDrawEllipse
app_draw_ellipse 双椭圆
GDI32: Rectangle
NtGdiRectangle
IntRectangle
IntEngBitBlt
PATH_Rectangle
GDI32: RoundRect
IntRoundRect
PATH_RoundRect
IntFillRoundRect
app_fill_arc + app_fill_rect
IntDrawRoundRect
GDI32: GradientFill
GreGradientFill
IntEngGradientFill
DrvGradientFill
EngGradientFill
IntEngGradientFillRect
DIB_HLine / DIB_VLine 插值
IntEngGradientFillTriangle
三边 DDA + FILLLINE
GDI32: ExtFloodFill
EXLATEOBJ 颜色翻译
DIB_XXBPP_FloodFillSolid
迭代四邻域 + 内部栈

12. 源码索引

文件 关键内容
win32ss/gdi/ntgdi/fillshap.c 本文主角:NtGdiEllipse/NtGdiRectangle/NtGdiRoundRect/NtGdiGradientFill/NtGdiExtFloodFill/NtGdiPolyPolyDraw、IntGdiPolygon/IntGdiPolyPolygon/IntRectangle/IntRoundRect/GreGradientFill/IntPolygon
win32ss/gdi/ntgdi/polyfill.c IntFillPolygon、FillPolygon(WINDING/ALTERNATE)、FILL_EDGE 边缘表、POLYGONFILL_* 扫描线填充器
win32ss/gdi/ntgdi/drawing.c app_fill_ellipse/app_draw_ellipse/app_fill_arc/app_draw_arc/app_fill_rect、IntFillEllipse/IntDrawEllipse/IntFillRoundRect/IntDrawRoundRect
win32ss/gdi/ntgdi/path.c PATH_Ellipse(13 点贝塞尔)、PATH_RoundRect(16 点)、PATH_Rectangle、PATH_PolyPolygon、PATH_CheckRect、PATH_StrokePath
win32ss/gdi/ntgdi/line.c IntGdiPolyBezier/IntGdiPolyBezierTo/IntGdiPolylineTo/IntGdiPolyPolyline、GDI_Bezier 转折线化
win32ss/gdi/ntgdi/bitblt.c IntGdiFillRgn(区域填充,IntEngPaint)
win32ss/gdi/ntgdi/pen.h IntIsEffectiveWidePen 宏
win32ss/gdi/eng/gradient.c IntEngGradientFill/EngGradientFill/IntEngGradientFillRect/IntEngGradientFillTriangle
win32ss/gdi/dib/floodfill.c DIB_XXBPP_FloodFillSolid
sdk/include/psdk/wingdi.h TRIVERTEX/GRADIENT_RECT/GRADIENT_TRIANGLE/GRADIENT_FILL_* 常量
sdk/include/psdk/winddi.h EngGradientFill 导出原型

关联模块: path.c(《分析_37》)、eng/gradient.c(《分析_26》)、dib/floodfill.c(《分析_29》)、bitblt.c 区域填充(《分析_36》)、coord.c(坐标变换 IntLPtoDP)、EXLATEOBJ(颜色翻译)。


13. 附录

13.1 fillshap.c 函数清单

函数 类型 签名 职责
IntGdiPolygon FASTCALL (PDC, PPOINT, int) 单多边形填充 + 描边(含宽笔路径描边)
IntGdiPolyPolygon FASTCALL (DC*, LPPOINT, PULONG, int) 复合多边形;路径打开转 PATH_PolyPolygon
IntPolygon FASTCALL (HDC, POINT*, int) Polygon 的 HDC 级加锁包装
NtGdiEllipse APIENTRY (HDC, int×4) 椭圆系统服务;路径/中点双轨
NtGdiPolyPolyDraw APIENTRY (HDC, PPOINT, PULONG, ULONG, INT) 六合一复合绘制系统服务
IntRectangle FASTCALL (PDC, int×4) 矩形填充 + 描边
NtGdiRectangle APIENTRY (HDC, int×4) 矩形系统服务;XFORM_SCALE 分支
IntRoundRect FASTCALL (PDC, int×6) 圆角矩形填充 + 描边
NtGdiRoundRect APIENTRY (HDC, int×6) 圆角矩形系统服务
GreGradientFill NTAPI (HDC, PTRIVERTEX, ULONG, PVOID, ULONG, ULONG) 渐变填充内部实现(含索引校验)
NtGdiGradientFill APIENTRY (HDC, PTRIVERTEX, ULONG, PVOID, ULONG, ULONG) 渐变填充系统服务(SEH+拷贝)
NtGdiExtFloodFill APIENTRY (HDC, INT, INT, COLORREF, UINT) 洪水填充系统服务

被调用但定义于其他文件的关键函数: IntFillPolygon(polyfill.c)、IntFillEllipse/IntDrawEllipse/IntFillRoundRect/IntDrawRoundRect(drawing.c)、PATH_Ellipse/PATH_Rectangle/PATH_RoundRect/PATH_PolyPolygon/PATH_StrokePath(path.c)、IntGdiPolyBezier/IntGdiPolylineTo/IntGdiPolyPolyline(line.c)、IntEngGradientFill(eng/gradient.c)、DIB_XXBPP_FloodFillSolid(dib/floodfill.c)。

13.2 关键宏与常量速查

宏/常量 值/定义位置 用途
Rsin/Rcos fillshap.c:14-15 三角函数宏,当前未使用(遗留)
ROP2_TO_MIX(jROP2) 引擎头文件 ROP2 光栅操作 → MIX(前景/背景混色)
ROP4_FROM_INDEX(R3_OPINDEX_PATCOPY) 引擎头文件 生成 PATCOPY 的 ROP4(图案拷贝)
BR_IS_NULL brush 头文件 画刷为"空画刷"标志(跳过填充)
DIRTY_FILL/DC_BRUSH_DIRTY dc 头文件 填充画刷脏标记
DIRTY_LINE/DC_PEN_DIRTY dc 头文件 线条画笔脏标记
DCPATH_ACTIVE/DCPATH_CLOCKWISE path 头文件 路径活动/顺时针方向标志
TAG_SHAPE 内核池标签 形状数据的内存池标签
GRADIENT_FILL_RECT_H/V/TRIANGLE wingdi.h:1387-1389 渐变模式常量
FLOODFILLBORDER/FLOODFILLSURFACE wingdi.h 洪水填充规则
GdiPolyPolygon...GdiPolyPolyRgn 内部枚举 NtGdiPolyPolyDraw 的 iFunc 取值
SMALLER/SWAP/LINC gradient.c 三角形顶点排序/交换/方向数组 {-1,1}
NLINES gradient.c:311 三角形三条边的索引维度(=3)

13.3 路径状态标志与绘制会话

  • PATH_IsPathOpen(dc->dclevel) 检查 dclevel.flPathDCPATH_ACTIVE 位------BeginPath 置位、EndPath/AbortPath 清除。本文件所有形状入口的第一步都是它,决定"追加路径"还是"直接绘制"。
  • DCPATH_CLOCKWISESetArcDirection(AD_CLOCKWISE/AD_COUNTERCLOCKWISE) 控制,路径生成(PATH_Ellipse 等)据此反转点序,保证按方向的填充/描边语义(WINDING 模式依赖方向)。
  • DC_vPrepareDCsForBlit(dc, prclBounds, ...)DC_vFinishBlit(dc, ...) 构成一个绘制会话 :前者锁定表面、同步可见性区域/裁剪并建立引擎上下文;后者释放并提交脏矩形。两者必须成对调用,本文件所有绘制路径都严格遵守(包括 goto cleanup 分支)。
  • IntUpdateBoundsRect(dc, &rcl)DC_ACCUM_APP|DC_ACCUM_WMGR 时把绘制区域并入 DC 的累积边界,供窗口管理器做区域更新。

13.4 一次 NtGdiRectangle 调用的完整生命周期

以"无旋转、普通画刷、细笔"为例,走 XFORM_SCALE 加速分支:

  1. NtGdiRectangle 加锁 DC,检查 mxWorldToDevice.flAccel & XFORM_SCALE
  2. 构造 4 顶点 DestCoords,调 IntGdiPolyPolygon(dc, DestCoords, &PolyCounts=4, 1)
  3. 路径未打开 → 进入 IntGdiPolygon
  4. IntLPtoDP 变换 4 顶点并加 ptlDCOrig;计算 DestRect 包围盒。
  5. 更新填充画刷与线条画笔(脏标记检查)。
  6. 填充:IntFillPolygon 建边缘表(4 条边 + 闭合边,水平边丢弃)→ 逐扫描线求截距 → IntEngBitBlt PATCOPY 刷出矩形内部。
  7. 描边:细笔分支四条 IntEngLineTo(含闭合边),ROP 由 ROP2_TO_MIX 转换。
  8. 返回后 DC_UnlockDc

若 DC 处于 BeginPath 状态,第 3 步改为 PATH_PolyPolygon 追加 4 点轮廓(第一个点 PT_MOVETO,最后一点 PT_CLOSEFIGURE),不产生任何像素。

14. 总结

fillshap.c 是 ntgdi 形状绘制的总调度台:

  • 六条系统服务 :椭圆、矩形、圆角矩形走"路径或直接光栅化"双轨;NtGdiPolyPolyDraw 以 iFunc 聚合六种多边形类操作;NtGdiGradientFill/NtGdiExtFloodFill 分别下放到 eng 层渐变引擎与 DIB 层洪水填充器。
  • 两条光栅化哲学:路径贝塞尔近似(可编辑、宽笔友好)与中点扫描转换(快速、直接像素输出)并存,体现了 ReactOS 在兼容性与实现代价之间的务实取舍。
  • 安全与健壮性:所有用户指针都经 SEH 探测+内核缓冲拷贝;顶点索引、点计数溢出、退化形状、空表面、栈溢出均有显式检查。
  • 已知差距IntGdiPolygon 填充未按 jFillMode 区分 WINDING/ALTERNATE(IntFillPolygon 固定交替);渐变三角形顶点未像矩形那样统一做 LPtoDP;洪水填充仅支持纯色画刷。

本文档基于 ReactOS 源代码 win32ss/gdi/ntgdi/fillshap.c 及关联模块(polyfill.c、drawing.c、path.c、line.c、bitblt.c、eng/gradient.c、dib/floodfill.c)逐行分析(2026 年 8 月)

相关推荐
caimouse11 小时前
ReactOS 图形系统分析(36):位块传输 — bitblt.c
c语言·reactos
Tyler_TXZ11 小时前
C++C语言之——树
c语言·c++·算法·
小羊先生car11 小时前
F429-HAL-Can总线(回环及双机实验)(2026/8/15)
c语言·单片机·嵌入式硬件·软件构建
caimouse12 小时前
ReactOS 图形系统分析(38):区域子系统 — region.c
c语言
嵌入式阿蔡12 小时前
STM32时钟树详解:配错一个全乱套
c语言·stm32·单片机·嵌入式硬件
caimouse12 小时前
ReactOS 图形系统分析(37):路径子系统 — path.c
c语言·reactos
CarIise13 小时前
C语言基础课程笔记:硬件原理、数据类型、结构体与格式化输入输出详解
c语言·开发语言·笔记
GeekZHR13 小时前
C语言指针2:数组名、二级指针、指针数组,一次把“指针和数组“讲透
c语言·数据结构·算法·指针
Dr.kangder13 小时前
嵌入式面试总结(二十一)——C语言关键字
c语言·开发语言·面试·职场和发展·架构·虚拟化
caimouse14 小时前
ReactOS 图形系统分析(31):内核 GDI — ntgdi
性能优化·reactos