ReactOS 图形系统分析(42):形状填充 --- fillshap.c
1. 概述
fillshap.c(win32ss/gdi/ntgdi/fillshap.c,约 1250 行)是 ntgdi(内核态 GDI,即 win32k.sys 中的 GDI 部分)的形状绘制与填充子系统 。它实现了用户态 GDI32 层形状类 API 的内核入口:椭圆(NtGdiEllipse)、矩形(NtGdiRectangle)、圆角矩形(NtGdiRoundRect)、渐变填充(NtGdiGradientFill)、洪水填充(NtGdiExtFloodFill)、多边形/复合绘制(NtGdiPolyPolyDraw),以及若干仅供内核内部调用的辅助函数(IntGdiPolygon、IntGdiPolyPolygon、IntRectangle、IntRoundRect、GreGradientFill、IntPolygon 等)。
本文件的核心设计思想是:形状先被转换成"路径(PATH)"或"区域(REGION)",再经由通用的填充/描边引擎输出 。这与 ReactOS 图形栈"少即是多"的分层思路一致------椭圆、圆角矩形这类复杂形状,其几何轮廓被分解为贝塞尔曲线或扫描线可处理的边,从而复用已有的路径填充、线段绘制、位块传输等基础设施,而不必为每种形状单独实现光栅化器。不过,ReactOS 在非路径模式下对椭圆/圆角矩形还保留了一套中点(midpoint)光栅化算法 (位于 drawing.c 的 app_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 设计动机
- 统一入口、复用引擎 :
NtGdiPolyPolyDraw一个系统服务就承载了 PolyPolygon、PolyPolyline、PolyBezier、PolylineTo、PolyBezierTo、PolyPolygonRgn 六种 GDI 对象(iFunc参数分发),把参数校验、SEH 探测、内核缓冲分配集中在一处,减少重复代码与攻击面。 - 路径优先 :凡 DC 处于"路径接收"状态(
PATH_IsPathOpen(dc->dclevel)),所有形状 API 一律转向PATH_*系列,把轮廓追加到当前路径中------这正是BeginPath()的实现基础;只有路径未打开时才真正执行绘制。 - 双轨光栅化:椭圆/圆角矩形在没有路径的情况下,既可以用贝塞尔路径(宽笔描边场景),也可以用中点算法直接逐矩形填充(普通场景),以保证性能与兼容性。
- 颜色/画刷经 EXLATEOBJ 转换 :渐变与洪水填充在进入引擎前都把 COLORREF/顶点颜色通过
EXLATEOBJ(调色板翻译对象)转成目标表面格式的颜色,屏蔽了 1bpp~32bpp 的差异。 - 全部走 DC 脏标记体系 :填充画笔(fill brush)与线条画笔(line pen)通过
ulDirty_的DIRTY_FILL/DC_BRUSH_DIRTY、DIRTY_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 中 FillPolygon(polyfill.c)根据 jFillMode 选择两条扫描线填充函数:
c
if (WINDING == pdcattr->jFillMode)
FillScanLine = POLYGONFILL_FillScanLineWinding;
else /* Default */
FillScanLine = POLYGONFILL_FillScanLineAlternate;
而 IntFillPolygon(fillshap.c 的 IntGdiPolygon 实际调用者)没有 按 jFillMode 分支,直接按交替方式以 IntEngBitBlt 填充------这一点与 Windows 存在差异,也说明 ReactOS 对填充模式的支持尚不完整(见第 3.4 节分析)。
2.3 与本文件相关的 DC 内部结构
dc->pdcattr(PDC_ATTR):持有hpen、hbrush、jROP2(光栅操作)、jBkMode(背景模式)、jFillMode(填充模式)、iGraphicsMode(GM_COMPATIBLE/GM_ADVANCED)、ptlCurrent、mxWorldToDevice(世界变换矩阵)等。dc->dclevel:持有绘制会话级对象------pbrFill(当前填充画刷)、pbrLine(当前线条画笔)、pSurface(目标表面)、hPath(当前路径句柄)、flPath(路径状态标志,含DCPATH_ACTIVE/DCPATH_CLOCKWISE)。dc->eboFill/dc->eboLine(EBRUSHOBJ):已实例化的画刷/画笔对象,直接挂在IntEngBitBlt/IntEngLineTo上。dc->ptlDCOrig:DC 原点,所有设备坐标结果都要加上它(IntLPtoDP之后+= ptlDCOrig)。dc->co(CLIPOBJ)、dc->prgnVis/dc->prgnRao:裁剪对象与可视/RAO 区域。dc->fs中的DC_ACCUM_APP/DC_ACCUM_WMGR:脏矩形累积(IntUpdateBoundsRect),用于窗口管理器区域刷新。
2.4 形状绘制通用预处理流水线
几乎所有入口(NtGdiEllipse/Rectangle/RoundRect、IntGdiPolygon、GreGradientFill、NtGdiExtFloodFill)共享同一套预处理步骤,可以提炼为固定流水线:
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)复用。 - 实现流程:
- 参数校验 :
ASSERT(dc);若Points == NULL || Count < 2,EngSetLastError(ERROR_INVALID_PARAMETER)返回 FALSE。 - 坐标变换 :
IntLPtoDP(dc, Points, Count)把顶点从逻辑坐标转设备坐标,再逐点累加dc->ptlDCOrig偏移。 - 计算包围盒 :
DestRect取所有顶点的 min/max,作为后续填充/描边的边界矩形(Bounding rectangle),供引擎裁剪优化。 - 刷子/画笔更新 :按脏标记调用
DC_vUpdateFillBrush/DC_vUpdateLineBrush;随后按特殊加锁顺序 取出pbrFill、pbrLine、psurf = dc->dclevel.pSurface(注释说明该顺序是为了避免死锁)。 - 空表面处理 :若
psurf == NULL(内存 DC 未选位图),直接返回 TRUE(无操作成功)。 - 填充 :若填充画刷非 NULL 画刷(
!(pbrFill->flAttrs & BR_IS_NULL)),计算画刷原点(ptOrigin + ptlDCOrig),调用IntFillPolygon(dc, psurf, &dc->eboFill.BrushObject, Points, Count, DestRect, &BrushOrigin)完成扫描线填充。 - 描边 :若线条画笔非 NULL:
- 宽笔 (
IntIsEffectiveWidePen(pbrLine),见 3.4):删除旧路径 →PATH_CreatePath(Count+1)创建新路径 → 设DCPATH_ACTIVE→PATH_MoveTo起点 → 对每个顶点PATH_LineTo→ 最后PATH_LineTo(Points[0])闭合 →pPath->state = PATH_Closed、清除DCPATH_ACTIVE→PATH_StrokePath(dc, pPath)描边 → 解锁并删除路径。宽笔必须走路径描边,因为逐边IntEngLineTo无法正确处理线帽/线接合。 - 细笔 :循环
IntEngLineTo依次画Points[i]→Points[i+1],最后补一条Points[Count-1]→Points[0]的闭合边;ROP 由ROP2_TO_MIX(pdcattr->jROP2)转换。
- 宽笔 (
- 返回:填充与描边任一失败则 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。 - 使用方式 :
NtGdiPolyPolyDraw的GdiPolyPolygon分支调用;NtGdiRectangle在 XFORM_SCALE 加速条件下也用它把矩形当作 4 顶点单多边形处理。
3.3 IntPolygon ------ HDC 级包装
c
BOOL FASTCALL
IntPolygon(HDC hdc, POINT *Point, int Count)
加锁 DC_LockDc(hdc) → IntGdiPolygon → DC_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)
实现要点:
POLYGONFILL_MakeEdgeList(Points, Count):为每条边(含最后一条闭合边Points[Count-1]→Points[0])创建FILL_EDGE。POLYGONFILL_MakeEdge会把边规范化为FromY <= ToY(反向边交换端点并记YDirection = -1),计算dx/dy/absdx/absdy/xmajor/XDirection,并用 Bresenham 风格的Error/ErrorMax初值(Error = ErrorMax/2,上行边再 -1 以像素级对齐)。水平边(absdy==0)被直接丢弃。- 对每条扫描线
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 填充------即每条扫描线段一次位块传输。
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(闭合图形)。 - 结果是路径中容纳多个闭合图形,之后可由
EndPath→FillPath/StrokePath统一处理。
3.6 宽笔描边:临时路径 + PATH_StrokePath
IntGdiPolygon 与 IntRectangle 在检测到"有效宽笔"时,都会走同一套临时路径描边流程,其步骤可以归纳为一个"构造-描边-销毁"三段式模式:
- 构造 :
PATH_Delete(dc->dclevel.hPath)清掉可能残留的旧路径;PATH_CreatePath(n)预分配 n 个槽位的路径(多边形用Count+1,矩形固定 5);置dc->dclevel.flPath |= DCPATH_ACTIVE并把新路径句柄挂到dc->dclevel.hPath;PATH_MoveTo设置起点(矩形分支先把起点pPath->pos做IntLPtoDP)。 - 描边 :逐点
PATH_LineTo追加(多边形最后补回Points[0]闭合),然后pPath->state = PATH_Closed并清除DCPATH_ACTIVE;PATH_StrokePath(dc, pPath)用当前画笔把闭合路径描成像素。描边引擎负责线帽(PS_ENDCAP_)、线接合(PS_JOIN_ )与几何笔宽度的处理,这是逐边IntEngLineTo无法提供的。 - 销毁 :
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/x2 与 x1/x2 相交判断) |
| 输出 | 每对区间一次 IntEngLineTo |
每段连续区间一次 IntEngLineTo |
| 适用 | 简单多边形、自交镂空 | 需要方向语义的复杂多边形(洞/岛) |
Winding 版本对"环绕数为 0 的区间"不填充,因此自交多边形的"缠绕"区域会被正确填实,而 Alternate 版本会按奇偶产生交替镂空。YDirection 由 POLYGONFILL_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 的
PolyPolygon、PolyPolyline、PolyBezier、PolylineTo、PolyBezierTo、PolyPolygon(Rgn 变体)。iFunc取值来自GdiPolyPolygon/GdiPolyPolyLine/GdiPolyBezier/GdiPolyLineTo/GdiPolyBezierTo/GdiPolyPolyRgn枚举。 - 实现流程:
- 快速失败 :
!UnsafePoints || !UnsafeCounts || Count == 0 || iFunc == 0 || iFunc > GdiPolyPolyRgn直接返回 FALSE(注释明确:Windows 对这些情况不设置 last error)。 - 第一段 SEH :
ProbeForRead探测两个用户缓冲;同时统计nPoints(总点数)、nMaxPoints(最大子多边形点数)、nInvalid(点数 < 2 的子多边形数)。异常 →_SEH2_GetExceptionCode()。 - 溢出与合法性 :
nPoints == 0 || nPoints < nMaxPoints(说明求和溢出)→ FALSE 不设错误;nInvalid != 0(存在 0 或 1 点的子多边形)→EngSetLastError(ERROR_INVALID_PARAMETER)。 - 一次分配 :
ExAllocatePoolWithTag(PagedPool, Count*sizeof(ULONG) + nPoints*sizeof(POINT), TAG_SHAPE),前半段放 counts(SafeCounts),后半段放 points(SafePoints)。 - 第二段 SEH :
RtlCopyMemory把两个用户缓冲拷入内核缓冲(指针已探测,无需再探)。 - GdiPolyPolyRgn 特例 :
iFunc == GdiPolyPolyRgn时,hDC参数被复用为填充模式iMode = (INT)(UINT_PTR)hDC,调用GreCreatePolyPolygonRgn(SafePoints, SafeCounts, Count, iMode)生成区域句柄返回(返回HRGN而非 BOOL,故函数返回类型是ULONG_PTR)。 - 加锁与准备 :
DC_LockDc(hDC);DC_vPrepareDCsForBlit;按脏标记更新填充/线条画刷。 - 分发:
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;
}
- 清理 :
DC_vFinishBlit→DC_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 并清除对应脏位------PolyBezierTo 与 PolyBezier 的差别就在"是否使用并更新当前点"。
4.4 SEH 两段式拷贝模式
NtGdiPolyPolyDraw 与 NtGdiGradientFill 都采用"先探测、后拷贝"的两段式 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)
DC_LockDc(hDC),失败ERROR_INVALID_HANDLE。- 路径分支 :
PATH_IsPathOpen(dc->dclevel)→PATH_Ellipse(dc, Left, Top, Right, Bottom)(见 5.4)直接返回。 - 退化处理 :
Left == Right || Top == Bottom返回 TRUE(零面积椭圆不绘制)。 - 坐标规范化 :
Right < Left交换、Bottom < Top交换,保证 Left<=Right、Top<=Bottom。 - 画笔准备 :
PEN_ShareLockPen(pdcattr->hpen);PenOrigWidth = PenWidth = pbrush->lWidth;PS_NULL笔 →PenWidth = 0。 - 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)。
-
坐标变换 :
RectBounds装入四值后IntLPtoDP(dc, &RectBounds, 2)(两个 POINT:左上、右下),再加ptlDCOrig。 -
计算椭圆参数 :
RadiusX = max((right-left)/2, 2)、RadiusY = max((bottom-top)/2, 2)(至少 2,给描边留空间),CenterX/CenterY取中点。 -
填充画刷 :
BRUSH_ShareLockBrush(pdcattr->hbrush);拷贝到临时BRUSH tmpFillBrushObj(副本),ptOrigin += ptlDCOrig;DC_vPrepareDCsForBlit;调IntFillEllipse(dc, CenterX-RadiusX, CenterY-RadiusY, RadiusX*2, RadiusY*2, &tmpFillBrushObj)。 -
描边 :填充成功后
IntDrawEllipse(dc, ..., pbrush)。 -
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_ellipse(ntgdi/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 + 1、ycrit = 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/2、yRadius = 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_CLOCKWISE则reverse_points反转 13 点。最后IntGdiCloseFigure闭合。 - 因此路径中的椭圆是贝塞尔近似 (误差极小但非精确椭圆),后续
EndPath+FillPath/StrokePath或PathToRegion都基于这 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 + 1与ycrit = 3b²/4 + 1是椭圆弧上斜率从 -1 过渡到 -∞ 的临界带判定值(对应 45° 分界)。 - 三种步进:
t + a²y < xcrit:e(x+1, y-1/2) <= 0,椭圆边界在候选点下方,取"向外"x+1;t - b²x >= ycrit:e(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 会破坏其他使用者的画刷相位。IntRoundRect 对 brushTemp 的处理同理,只是其原点补偿量是 (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)
- 路径分支 :
PATH_IsPathOpen→PATH_Rectangle(dc, ...)。 - 规范化
DestRect(min/max),IntLPtoDP,加ptlDCOrig,再RECTL_vMakeWellOrdered(RTL 反矩形修正)。 DC_ACCUM_APP|DC_ACCUM_WMGR时IntUpdateBoundsRect累积脏矩形。- GM_COMPATIBLE 语义 :
iGraphicsMode == GM_COMPATIBLE时DestRect.right--; DestRect.bottom--;(兼容模式下矩形不含右/下边,与 Windows 兼容)。 DC_vPrepareDCsForBlit;更新填充/线条画刷;取pbrFill/pbrLine/psurf;空笔/空表面分别处理(空表面返回 TRUE)。- 填充 :非 NULL 填充画刷时,用
IntEngBitBlt(&psurf->SurfObj, NULL, NULL, (CLIPOBJ*)&dc->co, NULL, &DestRect, NULL, NULL, &dc->eboFill.BrushObject, &BrushOrigin, ROP4_FROM_INDEX(R3_OPINDEX_PATCOPY))整块 PATCOPY------矩形填充不需要扫描转换,一次位块传输即完成。 - 描边 :
- 宽笔:创建 5 槽路径,
PATH_MoveTo(Left,Top)后四条PATH_LineTo回到起点,state = PATH_Closed,PATH_StrokePath描边。 - 细笔:
Mix = ROP2_TO_MIX(pdcattr->jROP2),四条IntEngLineTo画上、右、下、左边(坐标取自设备坐标DestRect),逐条ret = ret && ...累积结果。
- 宽笔:创建 5 槽路径,
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_MOVETO,IntGdiCloseFigure闭合。
6.4 IntGdiFillRgn ------ 区域填充路径(bitblt.c)
虽然 IntRectangle 走 PATCOPY 快速路径,但其他形状(如 FrameRect、FillRgn 等 API)最终会汇聚到 IntGdiFillRgn(bitblt.c:1228),此处一并展开,因为它与本文件的多边形填充在概念上是姊妹机制:
c
BOOL
IntGdiFillRgn(_In_ PDC pdc, _In_ PREGION prgn, _In_opt_ PBRUSH pbrFill)
- 无表面 → TRUE。
- 创建临时区域
prgnClip,REGION_LPTODP(pdc, prgnClip, prgn)把区域从逻辑坐标变换到设备坐标。 - 与可见性区域求交 :
pdc->prgnRao存在则RGN_AND其与 RAO 区域,否则与prgnVis求交(注释指出这两个区域目前不含 DC 原点偏移)。 REGION_bOffsetRgn(prgnClip, ptlDCOrig.x, ptlDCOrig.y)补上 DC 原点。- 脏矩形累积(
DC_ACCUM_APP|DC_ACCUM_WMGR时)。 IntEngInitClipObj+IntEngUpdateClipRegion把区域转成XCLIPOBJ。- 生成 MIX :
rop2Fg = FIXUP_ROP2(jROP2);mix = rop2Fg | (jBkMode == OPAQUE ? rop2Fg : R2_NOP) << 8------前景按 ROP2,背景模式为 OPAQUE 时背景同前景,否则背景为 NOP(透明)。 - 画刷:显式传入
pbrFill则EBRUSHOBJ_vInit初始化私有画刷对象;否则用 DC 当前填充画刷(按脏标记更新)。 IntEngPaint(&SurfObj, &xcoClip, pbo, &ptlFillOrigin, mix)完成实际填充。DC_vFinishBlit、删除临时区域、释放裁剪资源。
与 IntFillPolygon 的差异:多边形是先扫描转换出像素级线段 再逐段 PATCOPY;区域填充则是把区域矩形带 (rdh.nCount 个 RECT 的 Buffer)直接作为裁剪源,由引擎一次性刷涂。
7. NtGdiRoundRect ------ 圆角矩形
7.1 IntRoundRect 主流程
c
BOOL FASTCALL
IntRoundRect(PDC dc, int Left, int Top, int Right, int Bottom,
int xCurveDiameter, int yCurveDiameter)
- 路径分支 :
PATH_IsPathOpen→PATH_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 收缩逻辑与椭圆完全一致。
RectBoundsLPtoDP + ptlDCOrig 偏移。- 填充 :
BRUSH_ShareLockBrush(hbrush),拷贝到brushTemp且brushTemp.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/2、ry = 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_MOVETO,type[4]=type[8]=type[12]=PT_LINETO(中间的水平/垂直直线段),其余PT_BEZIERTO;DCPATH_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.c 中 IntDrawArc/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)
- 快速校验:
!pVertex || !nVertex || !pMesh || !nMesh→ERROR_INVALID_PARAMETER。 - 按模式计算网格字节数:RECT_H/RECT_V →
nMesh * sizeof(GRADIENT_RECT);TRIANGLE →nMesh * sizeof(GRADIENT_TRIANGLE);未知模式 →ERROR_INVALID_PARAMETER。 - 溢出检查 :
cbVertex + cbMesh <= cbVertex判定加法回绕 → FALSE(不设 last error)。 - 单块分配
cbVertex + cbMesh字节(TAG_SHAPE),SafeMesh = SafeVertex + cbVertex。 - SEH:
ProbeForRead两段后RtlCopyMemory拷入;异常 → 释放缓冲、SetLastNtError、返回 FALSE。 - 调
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)
- 顶点索引校验 :TRIANGLE 模式逐网格检查
Vertex1/2/3 < nVertex;矩形模式检查UpperLeft/LowerRight < nVertex;越界 →ERROR_INVALID_PARAMETER。(注意:此处访问的是内核缓冲pMesh,故无需 SEH。) DC_LockDc;无表面(内存 DC 未选位图)→ 解锁返回 TRUE(注释 CHECKME)。- 计算整体包围盒
rclExtent:遍历所有顶点取 min/max x、y;IntLPtoDP转设备坐标;加ptlDCOrig;RECTL_vMakeWellOrdered规范化;空矩形直接返回 TRUE。 - 矩形模式顶点变换 :
ulMode == RECT_H || RECT_V时,把每个顶点IntLPtoDP并加ptlDCOrig(三角形模式的顶点不在这里变换------它们由引擎内按边插值时处理?实际上IntEngGradientFillTriangle直接使用顶点坐标,说明三角形顶点在调用前未统一转换,这是一个值得注意的差异点;矩形顶点因引擎按矩形区域做水平/垂直逐行插值,需要精确设备坐标)。 - 抖动原点 :
ptlDitherOrg置 (0,0) 后 LPtoDP 加偏移------渐变在平移变换下保持抖动相位一致。 - 脏矩形累积、
DC_vPrepareDCsForBlit。 EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0, 0):以 RGB 调色板为源、目标表面调色板为目标的颜色翻译对象。IntEngGradientFill(&psurf->SurfObj, (CLIPOBJ*)&pdc->co, &exlo.xlo, pVertex, nVertex, pMesh, nMesh, &rclExtent, &ptlDitherOrg, ulMode)。EXLATEOBJ_vCleanup、DC_vFinishBlit、解锁 DC。
8.3 引擎侧:IntEngGradientFill → EngGradientFill(eng/gradient.c)
IntEngGradientFill(gradient.c:557)是引擎包装:若目标表面设置 HOOK_GRADIENTFILL(显示驱动实现了 DrvGradientFill),则调用 GDIDEVFUNCS(psoDest).GradientFill(...) 交给驱动;否则走软件实现 EngGradientFill(gradient.c:479)。
EngGradientFill 按 ulMode 分发:
- RECT_H / RECT_V :遍历每个
GRADIENT_RECT,调IntEngGradientFillRect(..., (ulMode == GRADIENT_FILL_RECT_H))。 - TRIANGLE :
IntEngIsNULLTriangle检测任意两顶点重合的退化三角形(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>>8,dc[id] = |(v2->Col>>8) - c[id]|,ec[id] = -(dy>>1)(误差项半初始化),ic[id] = ±1步进;HVSTEPCOL按ec += 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。
- 颜色插值用 Bresenham 增量式(宏
- 单色 (
v1与v2的 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 + ditherY到bt = 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)从左边到下边。 - 每像素的颜色 = 先沿两条边各插值一次、再沿行内插值一次(双线性插值的扫描线实现)。
- 三条边 DDA 初始化:
- 纯色三角形 (三顶点同色):用
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 的浮点乘加):
- HVINITCOL/HVSTEPCOL (矩形渐变,gradient.c:29):误差
ec初始-(dy>>1),每步ec += dc[id],ec > 0时c += ic并ec -= dy。这等价于用分数dc/dy累加逼近线性函数,每次只在误差越过阈值时进 1------避免了浮点累加误差,也避免了每像素的乘除法。所有运算为整数。 - FILLLINE/FDOCOL (三角形行内插值,gradient.c:237):对每一条水平扫描线,在左右边界色之间再用同样手法插值:
FINITCOL初始化gc/gd/ge/gi(ge = -(gx>>1)),FDOCOL每像素推进。注意它在每条线上重新初始化误差项,因此每行都是独立、无跨行误差累积的插值。
此外 INITLINE/INITCOL(三角形边)的 ex[line] = -(dy[line]>>1) 与多边形扫描线 POLYGONFILL_MakeEdge 的 Error = ErrorMax/2 是同一思想:半进位舍入保证插值结果对称(正向与反向遍历得到一致结果)。
8.8 EXLATEOBJ 颜色翻译机制
GreGradientFill 与 NtGdiExtFloodFill 都依赖 EXLATEOBJ 完成"逻辑颜色 → 表面像素值"的翻译:
EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0, 0):源调色板为gpalRGB(内置 RGB 调色板),目标为表面调色板psurf->ppal,无特殊标志。之后XLATEOBJ_iXlate(&exlo.xlo, rgb)返回目标格式下的像素值。- 洪水填充使用掩码
0xffffff(EXLATEOBJ_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)
DC_LockDc;无表面 → 返回 TRUE。- 起点
Pt(x, y)经IntLPtoDP转设备坐标(XStart/YStart是逻辑坐标)。 DC_vPrepareDCsForBlit。- 裁剪区域起点检查 :
prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis;若区域存在,REGION_PtInRegion(prgn, Pt)判断起点是否在可见/RAO 区域内,不在则直接结束(返回 FALSE);在则取区域包围盒DestRect作为洪水填充范围;区域为空则用整个位图尺寸。 - 脏矩形累积。
EXLATEOBJ_vInitialize(&exlo, &gpalRGB, psurf->ppal, 0, 0xffffff, 0)(掩码 0xffffff 只翻译 RGB)。ConvColor = XLATEOBJ_iXlate(&exlo.xlo, Color)把用户 COLORREF 翻译为目标表面像素值。DIB_XXBPP_FloodFillSolid(&psurf->SurfObj, &dc->eboFill.BrushObject, &DestRect, &Pt, ConvColor, FillType)。DC_vFinishBlit、EXLATEOBJ_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入栈前做三重检查:RECTL_bPointInRect:越出填充范围(裁剪区域包围盒)不入栈;pixel == BrushColor:已经涂成填充色的像素跳过 ------这正是防重复的关键(出栈后先DIB_PutPixel再入栈四邻域,邻域再次遇到该像素时颜色已变,自动排除,栈大小有保证);- 按
isSurf区分规则:FLOODFILLSURFACE 要求pixel == SurfaceColor;FLOODFILLBORDER 要求pixel != SurfaceColor(SurfaceColor 此时即边界色 ConvColor)。
- 主循环(以 FLOODFILLBORDER 为例):检查起点像素不是边界色(是则 FALSE)→
initFlood→ 起点入栈 → 循环:出栈 →DIB_PutPixel(x,y,BrushColor)→ 上下左右四邻域addItemFlood。 DIB_GetPixel/DIB_PutPixel走DibFunctionsForBitmapFormat[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 为边界色,填充被边界包围的连通区域(起点不能是边界色) | 一致:起点为边界色即失败;addItemFlood 用 isSurf=FALSE(pixel != 边界色) |
| FLOODFILLSURFACE | 以 Color 为表面色,填充所有同色连通区域(起点必须是表面色) | 一致:起点非表面色即失败;isSurf=TRUE(pixel == 表面色) |
| 画刷 | 用当前画刷填充(支持图案) | 仅 Brush->iSolidColor 纯色("Only solid fills supported for now") |
| 裁剪 | 受设备裁剪区约束 | 以 prgnRao/prgnVis 包围盒 DestRect 约束 |
| 边界 | 填充到画刷/表面边界 | 以 DstRect(区域包围盒)为硬边界 |
由此可见洪水填充在语义上对齐 Windows,但图案画刷支持与"裁剪区逐矩形精确定界"仍是 TODO。
9.5 洪水填充的复杂度与内存行为
- 空间复杂度 :
initFlood按width × height(DestRect包围盒尺寸)分配FLOODITEM(8 字节)。对于全屏 1920×1080 表面即约 16.6MB 的 PagedPool 一次性分配------这是"最坏情况"策略的代价。floodStart - 4 < floodData检测理论上几乎不会触发(因为每个已入栈像素都已满足填充规则且被涂色排除,栈深不可能超过可填充像素数),它更像防御性检查。 - 时间复杂度:每个被填充像素恰好访问一次(出栈 → PutPixel → 四邻域检查),O(填充面积)。四邻域 DFS 可能产生"蛇形"长栈,但内存已预分配,无动态扩容。
- 表面类型 :
DIB_GetPixel/DIB_PutPixel从DibFunctionsForBitmapFormat取函数指针,对 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 未启用)。NtGdiPolyPolyDraw的GdiPolyBezierTo分支注释引用 "From Wine 10.0 dlls/win32u/painting.c"------说明该分支的3n+1校验规则移植自 Wine。
10.5 性能与正确性评注
- 椭圆/圆角矩形选择路径还是中点算法 :路径模式(
BeginPath内)必然产生 13/16 点的贝塞尔轮廓,后续FillPath还要再经路径填充引擎做一次扫描转换;直接模式一次完成。因此非路径场景下,ReactOS 刻意保留中点算法以获得更好的性能与像素精确性。 - 多边形填充的双引擎 :
IntFillPolygon(BitBlt 填充)与FillPolygon(LineTo 填充)并存------前者每扫描线段一次IntEngBitBlt(适合画刷 PATCOPY,但每行一次调用开销不小),后者每线段一次IntEngLineTo。两者都没有使用区域(REGION)批量技术,扫描线极多时性能是 O(行数 × 调用开销)。 - 整数插值的可复现性:渐变与多边形扫描都用"误差项 + 半进位"的整数算法,保证同样输入在任何平台产生完全一致的像素(不依赖 FPU/编译器优化),这对图形兼容性测试至关重要。
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.flPath的DCPATH_ACTIVE位------BeginPath置位、EndPath/AbortPath清除。本文件所有形状入口的第一步都是它,决定"追加路径"还是"直接绘制"。DCPATH_CLOCKWISE:SetArcDirection(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 加速分支:
NtGdiRectangle加锁 DC,检查mxWorldToDevice.flAccel & XFORM_SCALE。- 构造 4 顶点
DestCoords,调IntGdiPolyPolygon(dc, DestCoords, &PolyCounts=4, 1)。 - 路径未打开 → 进入
IntGdiPolygon。 IntLPtoDP变换 4 顶点并加ptlDCOrig;计算DestRect包围盒。- 更新填充画刷与线条画笔(脏标记检查)。
- 填充:
IntFillPolygon建边缘表(4 条边 + 闭合边,水平边丢弃)→ 逐扫描线求截距 →IntEngBitBltPATCOPY 刷出矩形内部。 - 描边:细笔分支四条
IntEngLineTo(含闭合边),ROP 由ROP2_TO_MIX转换。 - 返回后
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 月)