ReactOS 图形系统分析(43):API 裁剪 --- cliprgn.c
1. 概述
cliprgn.c(file:///d:/reactos/win32ss/gdi/ntgdi/cliprgn.c) 位于 win32ss/gdi/ntgdi/ 目录,是 win32k.sys 内核态 GDI 的 API 裁剪(ClipRegion)子系统 核心实现。文件共约 855 行 (截至 2026 年 8 月分析时点),调试通道为 GdiClipRgn(DBG_DEFAULT_CHANNEL(GdiClipRgn)),文件头注释自述其职责为 "Clip region functions"。
从结构上看,该文件包含 18 个函数,可分成四类:
-
可见/RAO 区域维护 (4 个):
IntGdiReleaseRaoRgn、IntGdiReleaseVisRgn、UpdateVisRgn、GdiSelectVisRgn------ 负责维护 DC 的prgnVis(窗口可见区域)与prgnRao(有效合成区域),并与 ntuser 窗口系统联动。 -
应用裁剪选择 (4 个):
IntSelectClipRgn、IntGdiExtSelectClipRect、IntGdiExtSelectClipRgn、NtGdiExtSelectClipRgn------ 对应 Win32 APISelectClipRgn(含RGN_AND/OR/XOR/DIFF/COPY五种合成模式)。 -
裁剪操作/查询/可见性测试 (7 个):
NtGdiExcludeClipRect(=ExcludeClipRect)、NtGdiIntersectClipRect(=IntersectClipRect)、NtGdiOffsetClipRgn(=OffsetClipRgn)、GdiGetClipBox/NtGdiGetAppClipBox(=GetClipBox)、NtGdiPtVisible(=PtVisible)、NtGdiRectVisible(=RectVisible)。 -
元区域与合成 (3 个):
IntGdiSetMetaRgn/NtGdiSetMetaRgn(=SetMetaRgn)、CLIPPING_UpdateGCRegion(三层区域合成为 XCLIPOBJ 的引擎)。应用层: SelectClipRgn / ExcludeClipRect / IntersectClipRect / OffsetClipRgn
GetClipBox / PtVisible / RectVisible / SetMetaRgn
↓ user32 → win32k!NtGdi* 系统调用
┌───────────────────────────────────────────────────────────────────┐
│ cliprgn.c(win32ss/gdi/ntgdi) │
│ ┌─────────────────┐ ┌───────────────────┐ ┌──────────────────┐ │
│ │ 区域维护(联动) │ │ 裁剪选择/操作 │ │ 合成与引擎对接 │ │
│ │ IntGdiRelease... │ │ IntSelectClipRgn │ │ CLIPPING_Update... │ │
│ │ UpdateVisRgn │ │ IntGdiExtSelect... │ │ GCRegion │ │
│ │ GdiSelectVisRgn │ │ NtGdiExtSelect... │ │ (Vis∩Meta∩Clip) │ │
│ │ │ │ NtGdiExclude... │ │ ↓ │ │
│ │ │ │ NtGdiIntersect... │ │ IntEngUpdateClip... │ │
│ │ │ │ NtGdiOffset... │ │ Region → XCLIPOBJ│ │
│ └─────────────────┘ └───────────────────┘ └──────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
↑ GdiSelectVisRgn(ntuser 调用) ↓ DC.co / erclClip
ntuser 窗口可见区域(vis.c/windc.c) GDI 引擎(eng/clip.c)与图形驱动
cliprgn.c 本身不做任何区域布尔运算的实现 ,所有 REGION_* 运算(求交、求并、相减、异或、命中测试)都委托给 region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c)(见《分析_38》);它也不直接访问窗口对象,窗口可见区域由 ntuser 计算后通过 GdiSelectVisRgn 灌入。cliprgn.c 的真正职责是:在 DC 上维护"应用可见的裁剪状态",并在绘制前把三层裁剪(窗口可见 ∩ 元区域 ∩ 应用裁剪)惰性合成为引擎可枚举的 XCLIPOBJ。
2. 设计动机:三层裁剪模型
2.1 为什么需要三层裁剪
Windows GDI 在一个 DC 上同时存在三套相互独立的裁剪语义,它们由不同的主体维护:
| 层 | 区域字段 | 含义 | 维护者 |
|---|---|---|---|
| 系统可见层 | prgnVis |
窗口在屏幕上"真正可见"的部分(被父窗口边界裁剪、被上方兄弟窗口遮挡、被 SetWindowRgn 挖洞) |
ntuser(VIS_ComputeVisibleRegion),经 GdiSelectVisRgn 写入 |
| 应用裁剪层 | dclevel.prgnClip |
应用通过 SelectClipRgn/IntersectClipRect/ExcludeClipRect 设置的裁剪区 |
cliprgn.c(本文件) |
| 元区域层 | dclevel.prgnMeta |
元文件录制时"冻结"的裁剪(SetMetaRgn),此后应用再设新裁剪时与它取交 |
cliprgn.c(本文件) |
应用只"看得见"第二层(GetClipBox 反映的是应用裁剪与可见区域的交集),但系统必须保证绘制永远不越出窗口可见范围------即使应用把裁剪区设得比窗口还大。因此三层必须组合成一个最终区域 ,这个最终区域就是 prgnRao。
2.2 合成公式与惰性求值
CLIPPING_UpdateGCRegion 中的组合逻辑为:
prgnAPI = prgnClip ∩ prgnMeta (若两者都存在;只有一方则直接拷贝)
prgnRao = prgnVis ∩ prgnAPI (若无应用裁剪则 prgnRao = prgnVis)
最终有效区域 = Vis ∩ Meta ∩ Clip
区域布尔运算是 O(n₁+n₂) 的归并扫描,代价不低;而窗口移动、重绘时 prgnVis 会频繁变化。若每次裁剪操作后都立即重算 prgnRao,大量中间状态的计算都会被浪费。因此 ReactOS 采用脏标记 + 惰性求值:
-
任何裁剪相关变更只调用
IntGdiReleaseRaoRgn(pDC),它只置位pDC->fs |= DC_DIRTY_RAO(并通知用户态 GDI32 重读可见区域属性),不碰prgnRao本身; -
等到真正需要 有效区域时(绘制前
dclife.c的DC_vPrepareDCsForBlit、GdiGetClipBox、NtGdiRectVisible等),检查到DC_DIRTY_RAO才调用CLIPPING_UpdateGCRegion一次性完成合成。SelectClipRgn / 窗口移动(GdiSelectVisRgn) / OffsetClipRgn / SetMetaRgn
│ 只置位 DC_DIRTY_RAO(O(1))
▼
┌──────────┐ 绘制前 / 查询前 ┌──────────────────────────┐
│ 脏标记置位 │ ──────────────────▶ │ CLIPPING_UpdateGCRegion │
└──────────┘ │ Rao = Vis ∩ Meta ∩ Clip │
└──────────────────────────┘
│
▼
XCLIPOBJ(DC.co) + erclClip
2.3 坐标空间约定
裁剪区域的坐标空间在多层间有明确约定,这是理解本文件的关键:
prgnVis由 ntuser 以设备坐标 (屏幕坐标)计算,但GdiSelectVisRgn写入 DC 时会减去dc->ptlDCOrig,即 DC 内部以 "DC 相对坐标" 保存;dclevel.prgnClip/prgnMeta由IntSelectClipRgn等维护,同样以 DC 相对坐标保存(其"全区域"基准取自prgnVis的包围盒);CLIPPING_UpdateGCRegion合成出prgnRao后,先REGION_bOffsetRgn(prgnRao, +ptlDCOrig)转回设备坐标 ,填入erclClip并交给引擎IntEngUpdateClipRegion,用完再平移回去------这样prgnRao在 DC 生命周期内始终保持 DC 相对坐标,后续可直接与prgnVis再次求交。
3. 核心数据结构
3.1 DC 的裁剪相关字段(dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h#L95-L137))
DC 对象是 win32k 中所有裁剪状态的宿主。裁剪直接相关的字段如下:
| 字段 | 类型 | 含义 |
|---|---|---|
prgnVis |
PREGION |
窗口可见区域,注释明确"must never be 0" (_Notnull_)。DC 初始化时设为全表面矩形;失效时退回 prgnDefault |
prgnRao |
PREGION |
有效合成区域(Raster Operation Area),prgnVis 与 API 裁剪的交集;为 NULL 表示无 API 裁剪,直接使用 prgnVis |
prgnAPI |
PREGION |
应用裁剪与元区域的组合(Clip ∩ Meta),由 CLIPPING_UpdateGCRegion 内部重建 |
dclevel.prgnClip |
PREGION |
应用裁剪区域(对应 SelectClipRgn 等),NULL 表示无裁剪 |
dclevel.prgnMeta |
PREGION |
元区域(SetMetaRgn 冻结),NULL 表示无元区域 |
erclClip |
RECTL |
有效裁剪的包围盒(设备坐标),即 prgnRao->rdh.rcBound 的副本,驱动/引擎可直接使用 |
ptlDCOrig |
POINTL |
DC 原点偏移(窗口在屏幕上的位置),三层区域坐标换算的桥梁 |
co |
XCLIPOBJ |
内嵌的引擎裁剪对象,CLIPPING_UpdateGCRegion 通过 IntEngUpdateClipRegion 填充 |
fs |
INT |
DC 状态标志,其中 DC_DIRTY_RAO(0x0010) 是裁剪脏标记 |
pdcattr->VisRectRegion |
RGN_ATTR |
可见区域属性(复杂度+包围盒),供用户态 GDI32 通过句柄表属性读取 |
DCLEVEL(dclevel)是一个独立的子结构(dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h#L49-L89)),保存随 SaveDC/RestoreDC 保存/恢复的 DC 状态,prgnClip、prgnMeta、pSurface、sizl 都在其中。这解释了为何裁剪区域要分两处放:dclevel.prgnClip/prgnMeta 是可保存/恢复 的 DC 状态,而 prgnVis/prgnRao/prgnAPI 是系统内部合成缓存,不属于 DC 状态。
3.2 RGN_* 常量与区域复杂度(wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h#L356-L363))
合成模式(IntGdiCombineRgn 的 iCombineMode / SelectClipRgn 的 fnMode):
RGN_AND = 1 RGN_OR = 2 RGN_XOR = 3 RGN_DIFF = 4 RGN_COPY = 5
复杂度返回值:
NULLREGION = 1 SIMPLEREGION = 2 COMPLEXREGION = 3 ERROR = 0
NtGdiExtSelectClipRgn校验fnMode必须在[RGN_AND, RGN_COPY]闭区间内(1~5);- 所有
NtGdi*裁剪函数成功时返回新裁剪区域的复杂度;ERROR(0)表示失败; - 注意
ERROR与NULLREGION是不同的值(0 与 1),IntSelectClipRgn的_Success_(return!=ERROR)SAL 注解即要求返回值不能是 0。
另有批处理专用常量(ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h#L507)):
#define GDIBS_NORECT 0x80000000
其注释说明:"Both ExtSelectClipRgn and TextOut pass a nill RECT."------GDI 批处理(Gdi32 侧的 GDIBSEXTSELECTCLIPRGN 等)在需要表示"NULL 区域"时用该高位标志,IntGdiExtSelectClipRect 会先剥离它。
3.3 DC 状态标志(dc.h DCFLAGS)
| 标志 | 值 | 含义 |
|---|---|---|
DC_DISPLAY |
0x0001 | 显示 DC |
DC_DIRECT |
0x0002 | 直接 DC |
DC_PERMANANT |
0x0008 | 不可删除 DC |
DC_DIRTY_RAO |
0x0010 | 裁剪脏标记:prgnRao/erclClip/co 已失效,需重算 |
DC_ACCUM_WMGR |
0x0020 | 累积绘制(窗口管理器) |
DC_ACCUM_APP |
0x0040 | 累积绘制(应用) |
DC_REDIRECTION |
0x2000 | 重定向 DC |
DC_DIRTY_RAO 是本文件的"信号中枢":IntGdiReleaseRaoRgn 置位,CLIPPING_UpdateGCRegion 清除,GdiGetClipBox/NtGdiRectVisible/DC_vPrepareDCsForBlit 检查。
3.4 XCLIPOBJ(engobjects.h(file:///d:/reactos/win32ss/gdi/eng/engobjects.h#L52-L80))
XCLIPOBJ(EXtended CLip and Window Region Object)是引擎侧裁剪枚举对象,DC.co 即为其一个内嵌实例:
c
typedef struct _XCLIPOBJ
{
CLIPOBJ; /* 引擎公共部分(rclBounds/iDComplexity/iFComplexity/iMode/Rects...) */
RWNDOBJ; /* 窗口对象部分(pvConsumer/rclClient/psoOwner) */
struct _REGION *pClipRgn; /* prgnRao_ or (prgnVis_ if (prgnRao_ == z)) */
RECTL rclClipRgn;
RECTL* Rects; /* 指向区域矩形缓冲或 rclBounds */
DWORD cScan;
DWORD reserved;
ULONG EnumPos; /* 枚举游标(CLIPOBJ_cEnumStart/CLIPOBJ_bEnum 使用) */
LONG lscnSize;
ULONG EnumMax;
ULONG iDirection; /* CD_ANY 等 */
ULONG iType;
LONG lUpDown;
BOOL bAll;
DWORD RectCount; /* 矩形数量/枚举模式 */
PVOID pDDA;
} XCLIPOBJ, *PXCLIPOBJ;
关键点:
pClipRgn的注释直接说明它的取值:prgnRao,若为 NULL 则回退到prgnVis------这正是 cliprgn.c 各处dc->prgnRao ? dc->prgnRao : dc->prgnVis表达式的引擎侧对应;CLIPPING_UpdateGCRegion注释也印证:"WithpDC->co.pClipRgn = pDC->prgnRao ? pDC->prgnRao : pDC->prgnVis,CLIPOBJ_cEnumStartuses XCLIPOBJ to build the rects from region objects rects inpClipRgn->Buffer";IntEngUpdateClipRegion(eng/clip.c(file:///d:/reactos/win32ss/gdi/eng/clip.c#L171-L215))根据矩形数量填充复杂度字段:count>1时iDComplexity=DC_COMPLEX、iFComplexity=(≤4?FC_RECT4:FC_COMPLEX);count≤1时iDComplexity=DC_TRIVIAL(空矩形)或DC_RECT、iFComplexity=FC_RECT、Rects=&rclBounds。图形驱动通过CLIPOBJ_cEnumStart/CLIPOBJ_bEnum枚举这些矩形做逐矩形绘制。
3.5 句柄表标志与区域属性
IntGdiReleaseRaoRgn/UpdateVisRgn 操作 GDI 句柄表条目(GdiHandleTable->Entries[Index]):
GDI_ENTRY_VALIDATE_VIS(ntgdihdl.h(file:///d:/reactos/win32ss/include/ntgdihdl.h#L40),值 4):注释 "Validating Visible region data"。置位表示用户态 GDI32 需要重读该 DC 的可见区域属性(VisRectRegion);ATTR_RGN_VALID(0x10):区域属性有效,用户态可安全读取;ATTR_RGN_DIRTY(0x20):区域属性脏,下次加锁时由REGION_vSyncRegion把属性写回区域体(见《分析_38》)。
3.6 函数清单总览(带行号)
以下是 cliprgn.c 全部 18 个函数及其源码行号(行号以 2026 年 8 月源码为准):
| # | 函数 | 行号 | 类别 |
|---|---|---|---|
| 1 | IntGdiReleaseRaoRgn |
L16-24 | 区域维护 |
| 2 | IntGdiReleaseVisRgn |
L26-33 | 区域维护 |
| 3 | UpdateVisRgn |
L39-52 | 区域维护 |
| 4 | GdiSelectVisRgn |
L57-89 | 区域维护 |
| 5 | IntSelectClipRgn |
L91-209 | 裁剪选择(核心) |
| 6 | IntGdiExtSelectClipRect |
L218-274 | 裁剪选择(批处理) |
| 7 | IntGdiExtSelectClipRgn |
L276-308 | 裁剪选择(内部) |
| 8 | NtGdiExtSelectClipRgn |
L310-361 | 系统调用(SelectClipRgn) |
| 9 | GdiGetClipBox |
L363-405 | 查询(内部) |
| 10 | NtGdiGetAppClipBox |
L408-436 | 系统调用(GetClipBox) |
| 11 | NtGdiExcludeClipRect |
L438-484 | 系统调用(ExcludeClipRect) |
| 12 | NtGdiIntersectClipRect |
L486-535 | 系统调用(IntersectClipRect) |
| 13 | NtGdiOffsetClipRgn |
L537-593 | 系统调用(OffsetClipRgn) |
| 14 | NtGdiPtVisible |
L595-621 | 系统调用(PtVisible) |
| 15 | NtGdiRectVisible |
L623-673 | 系统调用(RectVisible) |
| 16 | IntGdiSetMetaRgn |
L675-720 | 元区域(内部) |
| 17 | NtGdiSetMetaRgn |
L723-737 | 系统调用(SetMetaRgn) |
| 18 | CLIPPING_UpdateGCRegion |
L739-853 | 合成(核心) |
其中 8 个 NtGdi* 系统调用入口 直接被 win32k 的 syscall 分发表引用(对应《分析_31》2.2 表中的 "cliprgn.c(6 入口)",本分析按源码展开为 8 个);GdiGetClipBox 与 GdiSelectVisRgn 为跨模块导出的内部接口(见 cliprgn.h(file:///d:/reactos/win32ss/gdi/ntgdi/cliprgn.h)),其余为文件内静态使用的 FASTCALL 辅助。
4. 函数逐一详解
以下按源码出现顺序逐一展开 18 个函数。
4.1 IntGdiReleaseRaoRgn(cliprgn.c L16-24)
c
VOID FASTCALL IntGdiReleaseRaoRgn(PDC pDC)
- 签名/参数 :
PDC已加锁的 DC 指针;FASTCALL寄存器传参;无返回值。 - 作用 :宣告"有效区域(RAO)已失效",是所有 裁剪变更的收尾动作。它不删除
prgnRao,也不重算,只做两件事:pDC->fs |= DC_DIRTY_RAO;------ 置脏标记,后续绘制/查询前由CLIPPING_UpdateGCRegion重算;Entry->Flags |= GDI_ENTRY_VALIDATE_VIS;------ 通过句柄表通知用户态 GDI32 重读可见区域属性。
- 实现流程 :
GDI_HANDLE_GET_INDEX(pDC->BaseObject.hHmgr)从 DC 对象头的句柄值取出句柄表下标,定位GDI_TABLE_ENTRY,然后置位上述两项。 - 使用方式 :被本文件几乎所有修改裁剪的函数调用(
IntSelectClipRgn各处、NtGdiOffsetClipRgn、IntGdiSetMetaRgn、GdiSelectVisRgn),也被 dcutil.c(file:///d:/reactos/win32ss/gdi/ntgdi/dcutil.c#L356) 的IntSetDefaultRegion调用。 - 注意事项 :调用方必须保证
pDC已通过DC_LockDc加锁(函数体内无加锁);该函数是 O(1) 的,因此可以在热路径上频繁调用。
4.2 IntGdiReleaseVisRgn(cliprgn.c L26-33)
c
VOID FASTCALL IntGdiReleaseVisRgn(PDC pDC)
- 作用 :释放可见区域并退回默认值。流程:
IntGdiReleaseRaoRgn(pDC);------ 先置 RAO 脏标记;REGION_Delete(pDC->prgnVis);------ 删除旧可见区域;pDC->prgnVis = prgnDefault;------ 指向全局默认区域(region.c 中的PREGION prgnDefault = NULL,全局仅一个,因此绝不能 被REGION_Delete释放------这正是REGION_Delete开头if (pRgn == prgnDefault) return;的原因)。
- 注意事项 :注释强调 "Vis can not be NULL!!!",因为
CLIPPING_UpdateGCRegion第一行就是ASSERT(pDC->prgnVis),且GdiGetClipBox等在无 RAO 时直接读prgnVis。所以"释放"不是置 NULL,而是换到prgnDefault这个哨兵区域。 - 使用方式 :
GdiSelectVisRgn收到 NULL 区域时调用(随后IntSetDefaultRegion恢复全窗口矩形);ntuser 在 DCE 清理路径也会用到。
4.3 UpdateVisRgn(cliprgn.c L39-52)
c
VOID FASTCALL UpdateVisRgn(PDC pdc)
- 作用 :把
prgnVis的复杂度与包围盒同步到 DC 的用户态可见属性pdc->pdcattr->VisRectRegion,供 GDI32 通过GetDCBrushColor/GetClipBox等用户态快捷路径读取。 - 实现流程 :
- 置
GDI_ENTRY_VALIDATE_VIS(防止用户态在同步中途读到半更新数据); pdc->pdcattr->VisRectRegion.iComplexity = REGION_GetRgnBox(pdc->prgnVis, &pdc->pdcattr->VisRectRegion.Rect);------ 一次性取得复杂度与包围盒(REGION_GetRgnBox返回复杂度,见《分析_38》);AttrFlags = ATTR_RGN_VALID;------ 标记属性有效;- 清除
GDI_ENTRY_VALIDATE_VIS。
- 置
- 使用方式 :
GdiSelectVisRgn(间接)、NtGdiOffsetClipRgn、CLIPPING_UpdateGCRegion末尾都会调用,保证属性与prgnVis一致。 - 注意事项 :
VisRectRegion是DC_ATTR的成员(ntgdihdl.h(file:///d:/reactos/win32ss/include/ntgdihdl.h#L350)),属于用户态可共享的 DC 属性区;GDI_ENTRY_VALIDATE_VIS类似"内存屏障/总线协议"标志。
4.4 GdiSelectVisRgn(cliprgn.c L57-89)
c
VOID FASTCALL GdiSelectVisRgn(HDC hdc, PREGION prgn)
- 签名/参数 :
hdc是 DC 句柄(注意这里是 HDC 而非 PDC,因为它是跨模块导出 给 ntuser 的);prgn是新可见区域(ntuser 计算出的、设备坐标)。 - 作用 :ntuser 把窗口可见区域灌入 DC 的唯一入口。
DceUpdateVisRgn(windc.c(file:///d:/reactos/win32ss/user/ntuser/windc.c#L267))在GetDC/BeginPaint/窗口变化后调用它。 - 实现流程 :
DC_LockDc(hdc),失败则EngSetLastError(ERROR_INVALID_HANDLE);if (!prgn):打印"SVR: Setting NULL Region",调用IntGdiReleaseVisRgn(dc)+IntSetDefaultRegion(dc)(恢复默认全窗口区域),解锁返回;- 否则
dc->fs |= DC_DIRTY_RAO;(可见区域变了,有效区域必然失效); REGION_bCopy(dc->prgnVis, prgn);------ 深拷贝(ntuser 传入的区域随后会被REGION_Delete,见 windc.c L272-275);REGION_bOffsetRgn(dc->prgnVis, -dc->ptlDCOrig.x, -dc->ptlDCOrig.y);------ 关键坐标变换:ntuser 给的是设备坐标,DC 内部保存 DC 相对坐标,减原点;DC_UnlockDc(dc)。
- 使用方式 :ntuser
windc.c三处调用(L267 主路径、L632/L1130 空区域场景);dclife.c的DC_vUpdateDC中有一段#if 0的等价代码(被禁用)。 - 注意事项 :
prgnVis采用"拷贝而非接管"策略;调用方对传入区域仍拥有所有权。坐标换算方向(-ptlDCOrig)与CLIPPING_UpdateGCRegion末尾的+ptlDCOrig严格配对。
4.5 IntSelectClipRgn(cliprgn.c L91-209)------ 裁剪选择的核心
c
_Success_(return!=ERROR)
int FASTCALL IntSelectClipRgn(_In_ PDC dc, _In_ PREGION prgn, _In_ int fnMode)
-
作用 :内部版
SelectClipRgn。把调用方区域prgn按fnMode合成进dc->dclevel.prgnClip,返回新裁剪区域复杂度。prgn == NULL时表示"无裁剪"(仅在RGN_COPY下有语义,与 Win32 一致)。 -
参数 :
dc已加锁 DC;prgn调用方区域(可 NULL);fnMode∈ {RGN_AND, RGN_OR, RGN_XOR, RGN_DIFF, RGN_COPY}。 -
实现流程(分两大分支):
分支 A("非 RGN_COPY 且(非 RGN_AND 或 无 prgn 或 已有原裁剪)"):这类模式必须与"原裁剪"或"全区域基准"做布尔运算,不能原地替换:
- 新建空区域
prgnNClip; - 已有原裁剪 (
prgnOrigClip非空):Ret = IntGdiCombineRgn(prgnNClip, prgnOrigClip, prgn, fnMode)。成功则删除原区域、dc->dclevel.prgnClip = prgnNClip、IntGdiReleaseRaoRgn;失败则删除prgnNClip。注意:此时prgn为 NULL 会失败(IntGdiCombineRgn对prgnSrc2 == NULL且非 COPY 模式断言)。 - 无原裁剪 :以
prgnVis的包围盒作为"全区域"基准------REGION_GetRgnBox(dc->prgnVis, &rcl),prgnClip = IntSysCreateRectpRgnIndirect(&rcl),再IntGdiCombineRgn(prgnNClip, prgnClip, prgn, fnMode),成功则挂接并释放 RAO,随后删除临时prgnClip。(函数中有一段#if 0的历史代码:对 PDEV_META_DEVICE 元设备还要叠加ppdev->ptlOrigion与ptlDCOrig,当前版本改为直接取prgnVis包围盒。)
分支 B(fall-through,即 RGN_COPY,或 RGN_AND 且无原裁剪):
!prgn(NULL 区域,RGN_COPY 语义) :若已有原裁剪则删除之、dclevel.prgnClip = NULL、IntGdiReleaseRaoRgn;返回SIMPLEREGION(NULL 裁剪 = 全区域 = 简单区域)。- 有原裁剪 :
IntGdiCombineRgn(prgnOrigClip, prgn, NULL, RGN_COPY)原地替换原裁剪("Clip could fail",失败时Ret=ERROR且原区域保持),成功则IntGdiReleaseRaoRgn。 - 无原裁剪 :新建
prgnNClip,REGION_bCopy(prgnNClip, prgn),Ret = REGION_Complexity(prgnNClip),挂接并释放 RAO。
- 新建空区域
-
返回 :新
prgnClip的复杂度(NULLREGION/SIMPLEREGION/COMPLEXREGION)或 ERROR。_Success_注解要求非 ERROR。 -
注意事项:
prgnClip与prgnMeta都存在时,最终有效区域由CLIPPING_UpdateGCRegion取二者交集,IntSelectClipRgn不需要(也无法)感知元区域;- 本函数不调用
CLIPPING_UpdateGCRegion,只置脏标记------合成推迟到绘制前; - "以
prgnVis包围盒为基准"意味着首次IntersectClipRect等操作隐含了"不超出可见区域"的系统约束,天然防止裁剪出窗口范围; - 分支 A 中
prgn == NULL && fnMode == RGN_AND且无原裁剪时,会落入分支 B 的 NULL 处理路径(删除裁剪),这在语义上等于"清空裁剪"。
4.6 IntGdiExtSelectClipRect(cliprgn.c L218-274)
c
_Success_(return!=ERROR)
int FASTCALL IntGdiExtSelectClipRect(_In_ PDC dc, _In_ PRECTL prcl, _In_ int fnMode)
- 作用 :矩形版
SelectClipRgn,供 GDI 批处理子系统调用 (文件注释:"Call from Gdi Batch Subsystem")。Gdi32 批处理记录GDIBSEXTSELECTCLIPRGN时传一个矩形或GDIBS_NORECT标志。 - 实现流程 :
BOOL NoRegion = fnMode & GDIBS_NORECT; fnMode &= ~GDIBS_NORECT;------ 剥离批处理标志;- NoRegion(NULL 区域语义) :仅当
fnMode == RGN_COPY时IntSelectClipRgn(dc, NULL, RGN_COPY),随后若DC_DIRTY_RAO则CLIPPING_UpdateGCRegion(dc)(这里主动立即合成 ,因为批处理路径没有后续绘制前的兜底),返回REGION_Complexity(dc->prgnVis)(NULL 裁剪 = 全可见区域); - 有矩形 :
- 快捷路径(CORE-16246 相关) :若当前
prgnClip已是 SIMPLEREGION 且与传入矩形完全相同,直接返回REGION_Complexity(prgnRao ? prgnRao : prgnVis),避免一次无意义的分配与合成; prgn = IntSysCreateRectpRgnIndirect(prcl)→IntSelectClipRgn(dc, prgn, fnMode)→ 若脏则CLIPPING_UpdateGCRegion(dc)→ 返回REGION_Complexity(prgnRao ? prgnRao : prgnVis)→REGION_Delete(prgn)。
- 快捷路径(CORE-16246 相关) :若当前
- 返回值语义 :与 Win32
SelectClipRgn一致------返回可见裁剪(RAO)的复杂度 而非prgnClip的复杂度,因为批处理路径立即合成了。 - 注意事项 :注释 "CORE-16246 : Needs to be a one box Clip Region" 提示该快捷路径假设裁剪是单矩形;与
IntGdiExtSelectClipRgn不同,本函数在批处理上下文中主动 调用CLIPPING_UpdateGCRegion。
4.7 IntGdiExtSelectClipRgn(cliprgn.c L276-308)
c
_Success_(return!=ERROR)
int FASTCALL IntGdiExtSelectClipRgn(_In_ PDC dc, _In_ PREGION prgn, _In_ int fnMode)
- 作用 :
NtGdiExtSelectClipRgn的内核内部实现(NtGdiExtSelectClipRgn的#else分支直接调用它)。 - 实现流程 :
!prgn(NULL 区域):仅RGN_COPY时IntSelectClipRgn(dc, NULL, RGN_COPY),成功则返回REGION_Complexity(dc->prgnVis);- 否则
IntSelectClipRgn(dc, prgn, fnMode);若DC_DIRTY_RAO则CLIPPING_UpdateGCRegion(dc);返回REGION_Complexity(prgnRao ? prgnRao : prgnVis)。
- 使用方式 :
NtGdiExtSelectClipRgn校验参数后调用;cliprgn.h中导出。 - 注意事项 :与
IntGdiExtSelectClipRect一样,成功时返回 RAO 复杂度(与 Win32 行为一致:SelectClipRgn返回"新裁剪区域与可见区域的交集"的复杂度);两条DPRINT("IntGdiExtSelectClipRgn A/B %d")帮助跟踪合成前后复杂度。
4.8 NtGdiExtSelectClipRgn(cliprgn.c L310-361)------ 系统调用入口
c
int APIENTRY NtGdiExtSelectClipRgn(HDC hDC, HRGN hrgn, int fnMode)
- 作用 :Win32
SelectClipRgn的系统调用实现(win32k 的NtGdiExtSelectClipRgn同时服务SelectClipRgn与ExtSelectClipRgn两个用户态 API)。 - 实现流程 :
- 参数校验 :
fnMode < RGN_AND || fnMode > RGN_COPY→EngSetLastError(ERROR_INVALID_PARAMETER)返回 ERROR; DC_LockDc(hDC)失败 →EngSetLastError(ERROR_INVALID_HANDLE);prgn = REGION_LockRgn(hrgn)(REGION_LockRgn会先REGION_vSyncRegion同步用户态属性,见《分析_38》);prgn == NULL && fnMode != RGN_COPY→retval = ERROR(注释明言 "doesn't set this",即不设置 last error,与 Windows 行为对齐------SelectClipRgn传无效句柄时行为未定义);- 否则
retval = IntGdiExtSelectClipRgn(dc, prgn, fnMode)(#if 0内是走批处理矩形的实验代码); REGION_UnlockRgn(prgn)、DC_UnlockDc(dc),返回。
- 参数校验 :
- 返回值:ERROR / NULLREGION / SIMPLEREGION / COMPLEXREGION(见 4.7 的语义)。
- 注意事项 :
REGION_LockRgn在hrgn无效时返回 NULL,此时若fnMode == RGN_COPY会以"NULL 区域"语义继续(等价于SelectClipRgn(hdc, NULL, RGN_COPY)清空裁剪),这是 GDI 句柄层与裁剪语义的巧妙耦合。
4.9 GdiGetClipBox(cliprgn.c L363-405)
c
_Success_(return!=ERROR)
INT FASTCALL GdiGetClipBox(_In_ HDC hdc, _Out_ LPRECT prc)
- 作用 :内部版
GetClipBox,返回当前有效裁剪区域 (prgnRao,无则prgnVis)的包围盒(逻辑坐标)。 - 实现流程 :
DC_LockDc(hdc);if (pdc->fs & DC_DIRTY_RAO) CLIPPING_UpdateGCRegion(pdc);------ 先确保合成;- 有
prgnRao:REGION_GetRgnBox(pdc->prgnRao, prc);无(意味着无 API 裁剪):ASSERT(pdc->prgnVis)后REGION_GetRgnBox(pdc->prgnVis, prc); IntDPtoLP(pdc, (LPPOINT)prc, 2)------ 设备坐标转逻辑坐标(两个点即矩形的对角);- 解锁返回复杂度。
- 注意事项 :
GetClipBox返回的是可见裁剪的框(可能因窗口遮挡而小于应用裁剪),这正是 Windows 文档语义: "retrieves the dimensions of the bounding rectangle of the visible clipping region"。
4.10 NtGdiGetAppClipBox(cliprgn.c L408-436)
c
_Success_(return!=ERROR)
INT APIENTRY NtGdiGetAppClipBox(_In_ HDC hdc, _Out_ LPRECT prc)
- 作用 :
GetClipBox的系统调用包装。先调GdiGetClipBox得到内核侧rect,再用 SEH 安全地ProbeForWrite后写回用户缓冲区。 - 实现流程 :
iComplexity = GdiGetClipBox(hdc, &rect);若非 ERROR,_SEH2_TRY { ProbeForWrite(prc, sizeof(RECT), 1); *prc = rect; } _SEH2_EXCEPT { iComplexity = ERROR; }。 - 注意事项:这是典型的"先内核栈计算、后 SEH 回写"模式,避免用户指针进入运算路径;异常时返回 ERROR 而非崩溃。
4.11 NtGdiExcludeClipRect(cliprgn.c L438-484)
c
INT APIENTRY NtGdiExcludeClipRect(_In_ HDC hdc, _In_ INT xLeft, _In_ INT yTop, _In_ INT xRight, _In_ INT yBottom)
- 作用 :Win32
ExcludeClipRect:从当前裁剪区域中挖掉 指定矩形(逻辑坐标),即prgnClip = prgnClip − rect(RGN_DIFF)。 - 实现流程 :
DC_LockDc失败 →EngSetLastError(ERROR_INVALID_HANDLE);- 组装
RECTL,RECTL_vMakeWellOrdered(&rect)规范化(左右/上下颠倒自动修正),IntLPtoDP(pdc, (LPPOINT)&rect, 2)转设备坐标; prgn = IntSysCreateRectpRgnIndirect(&rect);iComplexity = IntSelectClipRgn(pdc, prgn, RGN_DIFF);(核心逻辑见 4.5 分支 A);REGION_Delete(prgn)(临时区域用完即弃);- Windows 行为模拟 :
if (iComplexity == SIMPLEREGION) iComplexity = COMPLEXREGION;------ 挖洞后不可能是"简单矩形",Windows 语义上 ExcludeClipRect 结果要么 NULLREGION 要么 COMPLEXREGION; - 解锁返回。
- 注意事项 :本函数不主动 调用
CLIPPING_UpdateGCRegion(IntSelectClipRgn只置了脏标记),合成被推迟;这与IntGdiExtSelectClipRect/Rgn的"立即合成"形成对照。
4.12 NtGdiIntersectClipRect(cliprgn.c L486-535)
c
INT APIENTRY NtGdiIntersectClipRect(_In_ HDC hdc, _In_ INT xLeft, _In_ INT yTop, _In_ INT xRight, _In_ INT yBottom)
- 作用 :Win32
IntersectClipRect:当前裁剪区域与指定矩形求交 ,即prgnClip = prgnClip ∩ rect(RGN_AND)。 - 实现流程 :与
NtGdiExcludeClipRect完全同构,仅两处不同:- 合成模式改为
RGN_AND(IntSelectClipRgn(pdc, prgn, RGN_AND)); - 复杂度模拟同为
SIMPLEREGION → COMPLEXREGION(Windows 兼容:求交后结果若是单矩形,Windows 仍报告 COMPLEXREGION,因为裁剪区域被视为"组合区域")。
- 合成模式改为
- 注意事项 :首次调用时
prgnClip为 NULL,IntSelectClipRgn会以prgnVis包围盒为基准求交(见 4.5),因此首次IntersectClipRect的结果天然被限制在窗口可见范围内;同样不主动合成,走惰性路径。
4.13 NtGdiOffsetClipRgn(cliprgn.c L537-593)
c
INT APIENTRY NtGdiOffsetClipRgn(_In_ HDC hdc, _In_ INT xOffset, _In_ INT yOffset)
- 作用 :Win32
OffsetClipRgn:把当前应用裁剪区域平移(xOffset, yOffset)(逻辑坐标)。 - 实现流程 :
DC_LockDc;失败且!hdc时EngSetLastError(ERROR_INVALID_HANDLE)(对 NULL 句柄的特殊处理);- 有
prgnClip:- 坐标变换用两点法:
apt[0] = {0,0}、apt[1] = {xOffset, yOffset},IntLPtoDP(pdc, apt, 2)------ 注释说明:"we need to convert 2 coordinates to account for rotation / shear / offset",即用两点的设备坐标差作为平移量,正确处理旋转/切变/非 1:1 映射下的位移语义; REGION_bOffsetRgn(prgnClip, apt[1].x - apt[0].x, apt[1].y - apt[0].y);失败(越界)返回 ERROR;- 成功则
IntGdiReleaseRaoRgn(pdc)+UpdateVisRgn(pdc)(同步可见属性)+iComplexity = REGION_Complexity(prgnClip); - 末尾冗余执行
pdc->fs |= DC_DIRTY_RAO;(IntGdiReleaseRaoRgn已置位,此处为防御性重复);
- 坐标变换用两点法:
- 无
prgnClip(NULL 裁剪 = 全区域):iComplexity = SIMPLEREGION(平移不影响全区域); - 解锁返回复杂度。
- 注意事项 :本函数是少数直接 调用
UpdateVisRgn的地方(因为prgnClip平移后,即使尚未合成,可见属性也应保持一致性);REGION_bOffsetRgn对nCount == 0的空区域直接返回 TRUE(见《分析_38》),因此空裁剪平移不报错。
4.14 NtGdiPtVisible(cliprgn.c L595-621)
c
BOOL APIENTRY NtGdiPtVisible(HDC hDC, int X, int Y)
- 作用 :Win32
PtVisible:判断逻辑坐标点(X, Y)是否在当前可见裁剪区域内。 - 实现流程 :
DC_LockDc;失败 →EngSetLastError(ERROR_INVALID_HANDLE),返回 FALSE;prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis;------ 有有效合成区域用它,否则用可见区域;POINT pt = {X, Y}; IntLPtoDP(dc, &pt, 1);转设备坐标(注意是单点变换);ret = REGION_PtInRegion(prgn, pt.x, pt.y);------ 命中测试(先测包围盒INRECT,再逐矩形扫描,见《分析_38》);- 解锁返回。
- 注意事项(与
NtGdiRectVisible的差异) :本函数不检查DC_DIRTY_RAO也不调用CLIPPING_UpdateGCRegion------若裁剪刚被修改(prgnRao尚为旧值或已被删除而脏标记置位),本函数可能返回基于旧/不完整区域的结果。这是源码中的一处不对称(GdiGetClipBox与NtGdiRectVisible都有合成检查,唯独NtGdiPtVisible没有),分析时值得留意。
4.15 NtGdiRectVisible(cliprgn.c L623-673)
c
BOOL APIENTRY NtGdiRectVisible(HDC hDC, LPRECT UnsafeRect)
- 作用 :Win32
RectVisible:判断矩形(逻辑坐标,可任意颠倒)是否与当前可见裁剪区域相交(任意非空交集即 TRUE)。 - 实现流程 :
DC_LockDc;失败 →EngSetLastError(ERROR_INVALID_HANDLE);- SEH 读用户缓冲 :
_SEH2_TRY { ProbeForRead(UnsafeRect, sizeof(RECT), 1); Rect = *UnsafeRect; }异常则SetLastNtError(Status)返回 FALSE------这是与NtGdiPtVisible(值参数,无需探测)的重要区别; if (dc->fs & DC_DIRTY_RAO) CLIPPING_UpdateGCRegion(dc);------ 先合成 (与PtVisible形成对照);prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis;;IntLPtoDP(dc, (LPPOINT)&Rect, 2);转设备坐标;Result = REGION_RectInRegion(prgn, &Rect);------REGION_RectInRegion内部会先归一化矩形再与包围盒做EXTENTCHECK快速排除,然后带 y 分带跳过逻辑逐矩形扫描;- 解锁返回。
- 注意事项 :矩形"任意颠倒"由
REGION_RectInRegion内部处理(自动交换 left/right、top/bottom),NtGdiRectVisible本身不调用RECTL_vMakeWellOrdered。
4.16 IntGdiSetMetaRgn(cliprgn.c L675-720)
c
int FASTCALL IntGdiSetMetaRgn(PDC pDC)
- 作用 :内部版
SetMetaRgn(Win32 语义:把当前裁剪区域与现有元区域组合为新元区域 ,然后清空裁剪区域 ------此后应用设置的任何新裁剪都会与冻结的元区域取交)。四种组合情形:- 有元区域 + 有裁剪 :
prgn = 新建空区域,REGION_bIntersectRegion(prgn, prgnMeta, prgnClip)(两区域取交),成功后删除旧prgnMeta并把prgn挂为新的prgnMeta;再删除旧prgnClip并置prgnClip = NULL;IntGdiReleaseRaoRgn;返回REGION_Complexity(prgnMeta); - 有元区域 + 无裁剪 :元区域不变,直接返回
REGION_Complexity(prgnMeta); - 无元区域 + 有裁剪 :
prgnMeta = prgnClip; prgnClip = NULL;(指针搬运 ,不拷贝),返回REGION_Complexity(prgnMeta); - 无元区域 + 无裁剪 :返回
SIMPLEREGION。
- 有元区域 + 有裁剪 :
- 使用方式 :由
NtGdiSetMetaRgn调用;SaveDC/RestoreDC相关代码也引用prgnMeta(见注释 "See Restore/SaveDC")。 - 注意事项 :情形 3 是"零拷贝"的指针转移,因此
SaveDC保存 DC 状态时对prgnMeta/prgnClip的处理必须格外小心(深度拷贝而非浅拷贝),否则恢复后会出现双重释放/悬垂指针;情形 1 的求交意味着"先SetMetaRgn再SelectClipRgn"的最终有效区域是元区域 ∩ 新裁剪。
4.17 NtGdiSetMetaRgn(cliprgn.c L723-737)
c
int APIENTRY NtGdiSetMetaRgn(HDC hDC)
- 作用 :
SetMetaRgn的系统调用包装:DC_LockDc→IntGdiSetMetaRgn(pDC)→DC_UnlockDc。 - 注意事项 :加锁失败时调用的是
EngSetLastError(ERROR_INVALID_PARAMETER)------与本文件其他函数(统一用ERROR_INVALID_HANDLE)不一致,属源码中的小瑕疵;返回值为新元区域复杂度或 ERROR。
4.18 CLIPPING_UpdateGCRegion(cliprgn.c L739-853)------ 三层合成的核心
c
VOID FASTCALL CLIPPING_UpdateGCRegion(PDC pDC)
-
作用 :唯一的合成器 :把三层裁剪(
prgnVis、dclevel.prgnClip、dclevel.prgnMeta)合成出prgnRao,填充erclClip,并通过IntEngUpdateClipRegion刷新pDC->co(XCLIPOBJ)。任何"绘制前需要有效裁剪"的路径都先检查DC_DIRTY_RAO再调用它。 -
实现流程(对照源码逐段):
① 初始化 :
RECTL_vSetEmptyRect(&pDC->erclClip);并ASSERT(pDC->prgnVis)(可见区域必须有效,呼应 4.2 的 "Vis can not be NULL!!!")。② 重建 prgnAPI(应用裁剪层组合):
cif (pDC->prgnAPI) { REGION_Delete(pDC->prgnAPI); pDC->prgnAPI = NULL; } // 先丢弃旧缓存 if (pDC->dclevel.prgnMeta || pDC->dclevel.prgnClip) { pDC->prgnAPI = IntSysCreateRectpRgn(0,0,0,0); if (!pDC->prgnAPI) { ERR("Failed to allocate prgnAPI! Expect drawing issues!\n"); return; } if (!pDC->dclevel.prgnMeta) REGION_bCopy(pDC->prgnAPI, pDC->dclevel.prgnClip); else if (!pDC->dclevel.prgnClip) REGION_bCopy(pDC->prgnAPI, pDC->dclevel.prgnMeta); else REGION_bIntersectRegion(pDC->prgnAPI, pDC->dclevel.prgnClip, pDC->dclevel.prgnMeta); }即
prgnAPI = Clip ∩ Meta(一方缺失则直接拷贝另一方,两者都缺失则保持 NULL)。③ 重建 prgnRao(有效区域):
cif (pDC->prgnRao) REGION_Delete(pDC->prgnRao); pDC->prgnRao = IntSysCreateRectpRgn(0,0,0,0); if (!pDC->prgnRao) { ERR("Failed to allocate prgnRao! Expect drawing issues!\n"); return; } if (pDC->prgnAPI) REGION_bIntersectRegion(pDC->prgnRao, pDC->prgnVis, pDC->prgnAPI); else REGION_bCopy(pDC->prgnRao, pDC->prgnVis);即
Rao = Vis ∩ API(无 API 裁剪时 Rao = Vis 的拷贝)。④ 设备坐标化 + 引擎对接:
cREGION_bOffsetRgn(pDC->prgnRao, pDC->ptlDCOrig.x, pDC->ptlDCOrig.y); // DC 相对 → 设备 RtlCopyMemory(&pDC->erclClip, &pDC->prgnRao->rdh.rcBound, sizeof(RECTL)); // 包围盒进 erclClip pDC->fs &= ~DC_DIRTY_RAO; // 清除脏标记(合成完成) UpdateVisRgn(pDC); // 同步可见区域属性 IntEngUpdateClipRegion(&pDC->co, pDC->prgnRao->rdh.nCount, pDC->prgnRao->Buffer, &pDC->erclClip); // 填充 XCLIPOBJ REGION_bOffsetRgn(pDC->prgnRao, -pDC->ptlDCOrig.x, -pDC->ptlDCOrig.y); // 还原 DC 相对坐标 -
使用方式 :
dclife.c的DC_vPrepareDCsForBlit(dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L549-L550))在每次绘制前检查pdcDest->fs & DC_DIRTY_RAO并调用;GdiGetClipBox、NtGdiRectVisible、IntGdiExtSelectClipRect/Rgn(批处理与 Nt 路径)也会触发。 -
注意事项:
- 注释指出
pDC->co应被驱动使用:"CLIPOBJ_cEnumStartuses XCLIPOBJ to build the rects from region objects rects inpClipRgn->Buffer",且pDC->co.pClipRgn = pDC->prgnRao ? pDC->prgnRao : pDC->prgnVis------引擎侧无需关心合成细节; - 两处
ERR(...)是"尽力而为"的降级处理(分配失败时报错返回,erclClip保持空矩形,绘制可能丢失区域),注释明言 "Best we can do here. Better than crashing."; #if 0块内保留了 w2k3 风格的"无任何应用裁剪时直接把prgnVis平移到设备坐标并交给引擎"的快路径,ReactOS 当前版本依赖统一的prgnRao路径(注释 "(w7u) ReactOS relies on Rao");prgnRao在函数返回时保持 DC 相对坐标 ,因此下一次合成可直接与prgnVis/prgnAPI求交,无需额外换算。
- 注释指出
5. 三层区域合成流程(Vis ∩ Meta ∩ Clip → XCLIPOBJ)
以一次典型绘制为例,完整展示三层区域如何汇入引擎:
应用裁剪操作 窗口系统变化
SelectClipRgn / ExcludeClipRect / GetDC / BeginPaint / 窗口移动 / 遮挡变化
IntersectClipRect / OffsetClipRgn / │
SetMetaRgn │ VIS_ComputeVisibleRegion(vis.c)
│ IntGdiReleaseRaoRgn(置脏) │ DceUpdateVisRgn(windc.c)
▼ ▼ GdiSelectVisRgn(拷入 prgnVis + 置脏)
┌────────────────────────────────────────────────────────────────────┐
│ DC(win32k) │
│ │
│ dclevel.prgnClip ──┐ │
│ ├─(CLIPPING_UpdateGCRegion)─▶ prgnAPI │
│ dclevel.prgnMeta ──┘ = Clip ∩ Meta = Vis ∩ prgnAPI │
│ = prgnRao │
│ prgnVis(DC 相对坐标)─────────────────────────────────┘ │
│ │
│ prgnRao +ptlDCOrig → 设备坐标 │
│ │ │
│ ├─▶ erclClip = prgnRao->rdh.rcBound │
│ └─▶ IntEngUpdateClipRegion(&pDC->co, ...) ──▶ XCLIPOBJ 就绪│
└────────────────────────────────────────────────────────────────────┘
│ 驱动: CLIPOBJ_cEnumStart / CLIPOBJ_bEnum
▼
逐矩形光栅化(DrvTextOut / DrvBitBlt / DrvFillPath ...)
要点回顾:
- 合成只发生一次 :
CLIPPING_UpdateGCRegion同时刷新prgnAPI、prgnRao、erclClip、pDC->co四者,且只在DC_DIRTY_RAO置位时执行,之后立即清除脏标记。 prgnRao是绘制事实标准 :所有绘制入口(dclife.c 的 blit 准备路径)与查询(GetClipBox/RectVisible)都优先使用prgnRao,NULL时回退prgnVis。- 驱动只认识 XCLIPOBJ :驱动回调(
DrvBitBlt等)收到的是CLIPOBJ*(即&pDC->co),通过枚举 API 逐矩形获取裁剪,无需理解 win32k 的三层区域;erclClip供无需逐矩形枚举的快速路径直接使用。 prgnRao的生命周期 :由CLIPPING_UpdateGCRegion分配、删除、重分配;IntGdiReleaseRaoRgn只标记不删除------这是"惰性"能够成立的内存基础。
5.4 典型场景走查(基于源码流程推演)
以下四个场景从应用视角出发,逐步追踪区域状态变化,帮助把第 4 章的 18 个函数串成整体。
场景一:首次 IntersectClipRect(无任何裁剪的 DC)
初始:prgnVis = 全窗口矩形(DC 相对坐标);prgnClip = NULL;prgnMeta = NULL;prgnRao = NULL
1. 应用调用 IntersectClipRect(hdc, x1,y1,x2,y2)
2. NtGdiIntersectClipRect:
- 矩形规范化 + IntLPtoDP 转设备坐标
- prgn = IntSysCreateRectpRgnIndirect(&rect)
- IntSelectClipRgn(dc, prgn, RGN_AND)
· 分支 A:fnMode=RGN_AND 且无原裁剪(prgnOrigClip==NULL)→ 落入分支 B
· 分支 B 非 NULL:无原裁剪 → 新建 prgnNClip,REGION_bCopy(prgnNClip, prgn)
→ dc->dclevel.prgnClip = prgnNClip;IntGdiReleaseRaoRgn(置 DC_DIRTY_RAO)
· 返回 REGION_Complexity(prgnNClip)
- REGION_Delete(prgn)
3. 应用调用 GetClipBox(hdc, &rc):
- GdiGetClipBox:见 DC_DIRTY_RAO → CLIPPING_UpdateGCRegion
· prgnAPI = prgnClip 的拷贝(无 prgnMeta)
· prgnRao = prgnVis ∩ prgnAPI(= 全窗口 ∩ 新矩形)
· erclClip = prgnRao 包围盒;fs &= ~DC_DIRTY_RAO
· IntEngUpdateClipRegion 填充 co
- 返回 prgnRao 包围盒(逻辑坐标)
结论:GetClipBox 结果 = 应用矩形 ∩ 窗口可见区域(窗口被部分遮挡时 GetClipBox 会"缩水")
场景二:先 ExcludeClipRect 再 SelectClipRgn(已有裁剪的叠加)
初始:prgnClip = R1(矩形区域);prgnRao = R1 ∩ prgnVis(已合成)
1. ExcludeClipRect(hdc, ...) → NtGdiExcludeClipRect → IntSelectClipRgn(dc, prgn, RGN_DIFF)
- 分支 A:prgnOrigClip=R1 非空 → IntGdiCombineRgn(prgnNClip, R1, 新矩形, RGN_DIFF)
· R1 减掉新矩形 = R2;挂接 dclevel.prgnClip = R2;IntGdiReleaseRaoRgn
- 返回复杂度;因 R2 是"挖洞"结果 → SIMPLEREGION 被模拟为 COMPLEXREGION
2. SelectClipRgn(hdc, hrgnNew, RGN_AND) → IntSelectClipRgn(dc, prgnNew, RGN_AND)
- 分支 A:prgnOrigClip=R2 非空 → prgnNClip = R2 ∩ prgnNew = R3;挂接 R3
3. 绘制 BitBlt → DC_vPrepareDCsForBlit → CLIPPING_UpdateGCRegion
- prgnRao = prgnVis ∩ R3
结论:三种裁剪操作(DIFF→AND)在 prgnClip 上依次叠加,最终统一与 prgnVis 求交
场景三:SetMetaRgn 后的 SelectClipRgn(元区域冻结语义)
初始:prgnClip = R1;prgnMeta = NULL
1. SetMetaRgn(hdc) → IntGdiSetMetaRgn:
- 情形 3(无元区域 + 有裁剪):prgnMeta = prgnClip(R1);prgnClip = NULL;置脏
2. SelectClipRgn(hdc, hrgnNew, RGN_COPY) → prgnClip = R2(应用新裁剪)
3. 绘制 → CLIPPING_UpdateGCRegion:
- prgnAPI = prgnClip ∩ prgnMeta = R2 ∩ R1
- prgnRao = prgnVis ∩ (R2 ∩ R1)
结论:SetMetaRgn 之后设置的任何新裁剪(R2)都会被冻结的元区域(R1)再次约束,
最终有效区域 = prgnVis ∩ R1 ∩ R2 ------ 这正是元文件录制所需的"一层层收紧"语义
场景四:窗口被部分遮挡 + 应用裁剪(三层同时生效)
窗口 W 的可见区域(ntuser 计算):prgnVis = 窗口除被遮挡区外的多边形区域
应用设置复杂裁剪:prgnClip = 一个多边形区域(COMPLEXREGION)
绘制前合成:
① prgnAPI = prgnClip(无元区域)
② prgnRao = prgnVis ∩ prgnAPI ------ 区域布尔求交,结果可能是多个碎片矩形
③ erclClip = prgnRao 包围盒(一个粗略外框)
④ co.Rects = prgnRao->Buffer 的拷贝(引擎枚举用精确矩形集)
驱动 DrvBitBlt 收到 CLIPOBJ:
- iDComplexity = DC_COMPLEX(多个矩形)
- CLIPOBJ_cEnumStart / CLIPOBJ_bEnum 逐矩形绘制,碎片区外的像素不被触碰
结论:遮挡与裁剪同时生效,且只有"可见且未裁剪"的像素参与光栅化
四个场景覆盖了 IntSelectClipRgn 的两个分支、CLIPPING_UpdateGCRegion 的全部组合情形以及 IntGdiSetMetaRgn 的指针转移语义,可直接对照第 4 章源码逐行验证。
6. 与 ntuser 窗口可见区域的联动
6.1 可见区域的来源:VIS_ComputeVisibleRegion
窗口的可见区域由 ntuser 子系统计算(vis.c(file:///d:/reactos/win32ss/user/ntuser/vis.c#L12-L37)):
VIS_ComputeVisibleRegion(Wnd, ClientArea, ClipChildren, ClipSiblings)
① 起点:rcClient(ClientArea=TRUE)或 rcWindow 的矩形区域
② 沿父链向上(while CurrentWindow):
a. 与父窗口 rcClient 求交(RGN_AND)------ 不出父窗口边界
b. 若 (PreviousWindow->style & WS_CLIPSIBLINGS) 或要求 ClipSiblings:
遍历父窗口子链表(当前窗口之前的兄弟),
跳过不可见/WS_EX_TRANSPARENT 的兄弟,
对可见兄弟与其 rcWindow(及 SetWindowRgn 区域)做 RGN_DIFF ------ 被遮挡部分挖掉
③ 返回设备坐标的可见区域
6.2 注入 DC:DceUpdateVisRgn → GdiSelectVisRgn
GetDC / BeginPaint / DCX 标志变化 / 窗口重排后
└─ DceUpdateVisRgn(dce, pwndOrg, DCXFlags) [windc.c L~200-276]
├─ RgnVisible = DceGetVisRgn(...) → VIS_ComputeVisibleRegion(...)
├─ DCX_INTERSECTRGN:与 dce->hrgnClip 求交(GetDC(hwnd, hrgnClip) 语义)
├─ DCX_EXCLUDERGN :与 dce->hrgnClip 求差
├─ Dce->DCXFlags &= ~DCX_DCEDIRTY
├─ GdiSelectVisRgn(Dce->hDC, RgnVisible) ← 本文件入口
├─ IntEngWindowChanged(Window, WOC_RGN_CLIENT) ← 通知显示驱动窗口区域变化
└─ REGION_Delete(RgnVisible) (GdiSelectVisRgn 内部已深拷贝)
GdiSelectVisRgn 内部:深拷贝进 dc->prgnVis → 减 ptlDCOrig 转 DC 相对坐标 → 置 DC_DIRTY_RAO(见 4.4)。此后任何绘制都会触发 CLIPPING_UpdateGCRegion 把新可见区域纳入合成。
6.3 失效与重建时机
| 时机 | 动作 | 结果 |
|---|---|---|
GetDC/BeginPaint 返回 DC |
DceUpdateVisRgn(DCE 脏时) |
prgnVis 更新为当前窗口可见区域 |
| 窗口移动/缩放/遮挡变化(winpos.c) | 标记 DCE 脏 + IntEngWindowChanged(WOC_RGN_CLIENT) |
下次取 DC 时重算 prgnVis |
SelectClipRgn(hdc, NULL, RGN_COPY) |
IntGdiReleaseVisRgn + IntSetDefaultRegion |
prgnVis 退回全局默认区域 |
| 任意裁剪 API | IntGdiReleaseRaoRgn |
仅置 DC_DIRTY_RAO,绘制前才合成 |
DC 销毁(DC_vCleanup,dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L357-L410)) |
依次 REGION_Delete prgnClip/prgnMeta/prgnVis/prgnRao/prgnAPI + IntEngFreeClipResources(&pdc->co) |
全部裁剪资源回收 |
注意 DC_vCleanup 的释放顺序:先释放 dclevel 中可保存/恢复的裁剪(prgnClip/prgnMeta),再释放系统缓存(prgnVis/prgnRao/prgnAPI),最后释放引擎裁剪对象资源(IntEngFreeClipResources 释放 co.Rects 若其指向堆内存)。
7. 调用链(mermaid)
7.1 裁剪设置调用链(时序)
eng/clip.c region.c cliprgn.c win32k.sys GDI32 (user32/gdi32) 应用程序 eng/clip.c region.c cliprgn.c win32k.sys GDI32 (user32/gdi32) 应用程序 #mermaid-svg-7eClb7Js0Q6cuFYe{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-7eClb7Js0Q6cuFYe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7eClb7Js0Q6cuFYe .error-icon{fill:#552222;}#mermaid-svg-7eClb7Js0Q6cuFYe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7eClb7Js0Q6cuFYe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7eClb7Js0Q6cuFYe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7eClb7Js0Q6cuFYe .marker.cross{stroke:#333333;}#mermaid-svg-7eClb7Js0Q6cuFYe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7eClb7Js0Q6cuFYe p{margin:0;}#mermaid-svg-7eClb7Js0Q6cuFYe .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7eClb7Js0Q6cuFYe text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7eClb7Js0Q6cuFYe .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7eClb7Js0Q6cuFYe .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7eClb7Js0Q6cuFYe #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7eClb7Js0Q6cuFYe .sequenceNumber{fill:white;}#mermaid-svg-7eClb7Js0Q6cuFYe #sequencenumber{fill:#333;}#mermaid-svg-7eClb7Js0Q6cuFYe #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7eClb7Js0Q6cuFYe .messageText{fill:#333;stroke:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7eClb7Js0Q6cuFYe .labelText,#mermaid-svg-7eClb7Js0Q6cuFYe .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .loopText,#mermaid-svg-7eClb7Js0Q6cuFYe .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7eClb7Js0Q6cuFYe .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7eClb7Js0Q6cuFYe .noteText,#mermaid-svg-7eClb7Js0Q6cuFYe .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7eClb7Js0Q6cuFYe .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7eClb7Js0Q6cuFYe .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7eClb7Js0Q6cuFYe .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7eClb7Js0Q6cuFYe .actorPopupMenu{position:absolute;}#mermaid-svg-7eClb7Js0Q6cuFYe .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7eClb7Js0Q6cuFYe .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7eClb7Js0Q6cuFYe .actor-man circle,#mermaid-svg-7eClb7Js0Q6cuFYe line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7eClb7Js0Q6cuFYe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 合成被推迟到绘制前 SelectClipRgn(hdc, hrgn, RGN_AND)NtGdiExtSelectClipRgnIntGdiExtSelectClipRgnREGION_LockRgn(hrgn) // 同步用户态属性IntSelectClipRgn(dc, prgn, RGN_AND)IntGdiCombineRgn(...) // 区域布尔运算IntGdiReleaseRaoRgn(dc) // 置 DC_DIRTY_RAOREGION_UnlockRgn复杂度返回
7.2 绘制触发合成的调用链
渲染错误: Mermaid 渲染失败: Parse error on line 17: ... fs &= ~DC_DIRTY_RAO CR->>ENG: IntEn -----------------------^ Expecting '()', 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'SOLID_ARROW_TOP', 'SOLID_ARROW_BOTTOM', 'STICK_ARROW_TOP', 'STICK_ARROW_BOTTOM', 'SOLID_ARROW_TOP_DOTTED', 'SOLID_ARROW_BOTTOM_DOTTED', 'STICK_ARROW_TOP_DOTTED', 'STICK_ARROW_BOTTOM_DOTTED', 'SOLID_ARROW_TOP_REVERSE', 'SOLID_ARROW_BOTTOM_REVERSE', 'STICK_ARROW_TOP_REVERSE', 'STICK_ARROW_BOTTOM_REVERSE', 'SOLID_ARROW_TOP_REVERSE_DOTTED', 'SOLID_ARROW_BOTTOM_REVERSE_DOTTED', 'STICK_ARROW_TOP_REVERSE_DOTTED', 'STICK_ARROW_BOTTOM_REVERSE_DOTTED', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'NEWLINE'
7.3 窗口可见区域变化触发合成
DC 对象 cliprgn.c vis.c ntuser (winpos.c/windc.c) DC 对象 cliprgn.c vis.c ntuser (winpos.c/windc.c) #mermaid-svg-QWAGakSS3yn4Pe3E{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-QWAGakSS3yn4Pe3E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QWAGakSS3yn4Pe3E .error-icon{fill:#552222;}#mermaid-svg-QWAGakSS3yn4Pe3E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QWAGakSS3yn4Pe3E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QWAGakSS3yn4Pe3E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QWAGakSS3yn4Pe3E .marker.cross{stroke:#333333;}#mermaid-svg-QWAGakSS3yn4Pe3E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QWAGakSS3yn4Pe3E p{margin:0;}#mermaid-svg-QWAGakSS3yn4Pe3E .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QWAGakSS3yn4Pe3E text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QWAGakSS3yn4Pe3E .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QWAGakSS3yn4Pe3E .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QWAGakSS3yn4Pe3E #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QWAGakSS3yn4Pe3E .sequenceNumber{fill:white;}#mermaid-svg-QWAGakSS3yn4Pe3E #sequencenumber{fill:#333;}#mermaid-svg-QWAGakSS3yn4Pe3E #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QWAGakSS3yn4Pe3E .messageText{fill:#333;stroke:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QWAGakSS3yn4Pe3E .labelText,#mermaid-svg-QWAGakSS3yn4Pe3E .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .loopText,#mermaid-svg-QWAGakSS3yn4Pe3E .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QWAGakSS3yn4Pe3E .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QWAGakSS3yn4Pe3E .noteText,#mermaid-svg-QWAGakSS3yn4Pe3E .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QWAGakSS3yn4Pe3E .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QWAGakSS3yn4Pe3E .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QWAGakSS3yn4Pe3E .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QWAGakSS3yn4Pe3E .actorPopupMenu{position:absolute;}#mermaid-svg-QWAGakSS3yn4Pe3E .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QWAGakSS3yn4Pe3E .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QWAGakSS3yn4Pe3E .actor-man circle,#mermaid-svg-QWAGakSS3yn4Pe3E line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QWAGakSS3yn4Pe3E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 下次绘制 → CLIPPING_UpdateGCRegion 自动合成新可见区域 窗口移动/缩放/遮挡变化Dce->>DCXFlags |= DCX_DCEDIRTY(标记 DCE 脏)DceGetVisRgn → VIS_ComputeVisibleRegion(...)RgnVisible(设备坐标)GdiSelectVisRgn(hdc, RgnVisible)REGION_bCopy(prgnVis, RgnVisible)REGION_bOffsetRgn(prgnVis, -ptlDCOrig) // DC 相对坐标fs |= DC_DIRTY_RAOIntEngWindowChanged(Window, WOC_RGN_CLIENT)
8. 关键流程速查与设计要点汇总
8.1 函数职责一览
| 函数 | 类别 | 关键行为 | 是否立即合成 |
|---|---|---|---|
IntGdiReleaseRaoRgn |
区域维护 | 置 DC_DIRTY_RAO + GDI_ENTRY_VALIDATE_VIS |
--- |
IntGdiReleaseVisRgn |
区域维护 | 删 prgnVis,退回 prgnDefault |
--- |
UpdateVisRgn |
区域维护 | 同步 VisRectRegion 属性 |
--- |
GdiSelectVisRgn |
区域维护 | 拷贝+平移 prgnVis,置脏 |
否(惰性) |
IntSelectClipRgn |
裁剪选择 | 按 fnMode 合成 prgnClip |
否(惰性) |
IntGdiExtSelectClipRect |
裁剪选择(批处理) | 矩形版;含单矩形快捷路径 | 是 |
IntGdiExtSelectClipRgn |
裁剪选择(内部) | Nt 入口的实现 | 是 |
NtGdiExtSelectClipRgn |
系统调用 | 参数校验 + 加锁 + 委托 | 是(经内部函数) |
GdiGetClipBox |
查询 | 返回 RAO/Vis 包围盒(逻辑坐标) | 是(先合成) |
NtGdiGetAppClipBox |
系统调用 | SEH 回写用户缓冲 | 是 |
NtGdiExcludeClipRect |
裁剪操作 | RGN_DIFF 挖矩形;SIMPLEREGION→COMPLEXREGION |
否 |
NtGdiIntersectClipRect |
裁剪操作 | RGN_AND 求交;同上复杂度模拟 |
否 |
NtGdiOffsetClipRgn |
裁剪操作 | 两点法换算后平移 prgnClip |
否(+UpdateVisRgn) |
NtGdiPtVisible |
可见性测试 | 点命中;不检查 DIRTY_RAO | 否(不对称) |
NtGdiRectVisible |
可见性测试 | 矩形相交;SEH 读缓冲;先合成 | 是 |
IntGdiSetMetaRgn |
元区域 | 冻结裁剪为元区域并清空裁剪 | 否(置脏) |
NtGdiSetMetaRgn |
系统调用 | 包装 IntGdiSetMetaRgn |
否 |
CLIPPING_UpdateGCRegion |
合成 | Rao=Vis∩Meta∩Clip → XCLIPOBJ | 本体 |
8.2 设计要点
- 惰性合成:裁剪操作 O(1) 置脏、绘制前 O(n) 合成,把昂贵的区域布尔运算从"每次修改"摊销到"每次绘制";
- 三层职责分离 :
prgnVis(系统)永远不可被应用覆盖;prgnClip(应用)可自由修改;prgnMeta(录制)提供一次性的"冻结"语义; - 坐标空间双向转换 :
-ptlDCOrig(注入时)与+ptlDCOrig(合成时)严格配对,保证prgnVis/prgnRao稳定在 DC 相对坐标,erclClip/co稳定在设备坐标; prgnDefault哨兵 :prgnVis永不置 NULL,释放即退回全局默认区域,避免合成器与查询路径的 NULL 检查泛滥;- 用户态联动 :
GDI_ENTRY_VALIDATE_VIS+VisRectRegion属性让 GDI32 能以极低成本获取"当前可见裁剪"快照,不必每次走系统调用; - Windows 兼容细节 :
ExcludeClipRect/IntersectClipRect的SIMPLEREGION→COMPLEXREGION模拟、NtGdiExtSelectClipRgn无效句柄不设 last error 等,都是刻意对齐 Win32 语义的行为。
8.3 与 Win32 API 行为对照
| Win32 API | ReactOS 入口 | 复杂度返回语义 | 特殊处理 |
|---|---|---|---|
SelectClipRgn(hdc, hrgn, mode) |
NtGdiExtSelectClipRgn |
可见裁剪(RAO)复杂度;失败 ERROR | 无效 hrgn 且非 RGN_COPY → ERROR 但不设 last error;NULL+COPY = 清空裁剪 |
SelectClipRgn(hdc, NULL, RGN_COPY) |
同上(prgn==NULL 路径) |
SIMPLEREGION | 删除 prgnClip;prgnVis 不变,有效区域=全可见区域 |
ExcludeClipRect |
NtGdiExcludeClipRect |
NULLREGION/COMPLEXREGION | SIMPLEREGION 强制转 COMPLEXREGION |
IntersectClipRect |
NtGdiIntersectClipRect |
NULLREGION/COMPLEXREGION | 同上;首次调用隐含"∩ 窗口可见区" |
OffsetClipRgn |
NtGdiOffsetClipRgn |
平移后复杂度;无裁剪 SIMPLEREGION | 两点法换算正确处理旋转/缩放 |
GetClipBox |
NtGdiGetAppClipBox |
RAO/Vis 复杂度;ERROR | 先强制合成(保证最新) |
PtVisible |
NtGdiPtVisible |
TRUE/FALSE | 值参数无需 SEH;不强制合成(不对称) |
RectVisible |
NtGdiRectVisible |
TRUE/FALSE | SEH 探测读缓冲;先强制合成 |
SetMetaRgn |
NtGdiSetMetaRgn |
新元区域复杂度 | 4 种组合情形;裁剪被清空并"冻结" |
对照 Win32 文档(MSDN)可发现:ReactOS 的实现语义基本一致,特别是 ExcludeClipRect/IntersectClipRect 的"结果要么 NULLREGION 要么 COMPLEXREGION"规则(源码注释 "Emulate Windows behavior")、GetClipBox 返回"可见裁剪"而非"应用裁剪"、SelectClipRgn 的 RGN_COPY 与 NULL 区域特殊语义。差异点集中在内部架构(三层区域、惰性合成、XCLIPOBJ)而非对外行为。
8.4 内存与生命周期细节
- 系统区域 vs 用户区域 :
prgnVis/prgnRao/prgnAPI/dclevel.prgnClip/prgnMeta全部通过IntSysCreateRectpRgn创建(无句柄、prgnattr = &rgnattr、ATTR_RGN_VALID,见 region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c#L2408-L2432)),是纯内核私有 区域;而应用传入的hrgn是带句柄与用户态属性的"用户区域"(REGION_LockRgn加锁)。cliprgn.c 只以"值拷贝"方式消费用户区域,从不持有其指针,因此无需担心用户区域被删除后的悬垂引用。 REGION_Delete与prgnDefault哨兵 :REGION_Delete(pRgn)内部对prgnDefault直接返回(region.c L2452-2458),这是IntGdiReleaseVisRgn能把prgnVis指向prgnDefault的安全前提;任何路径都不允许对prgnDefault调用删除。SaveDC/RestoreDC与dclevel:prgnClip/prgnMeta位于DCLEVEL,随SaveDC/RestoreDC保存恢复(dc.h 注释 "See Restore/SaveDC" 出现在IntGdiSetMetaRgn)。IntGdiSetMetaRgn的指针转移 (情形 3:prgnMeta = prgnClip; prgnClip = NULL)要求DC_vRestoreDC对区域的保存必须是深拷贝,否则恢复旧 DC 状态时会与当前 DC 共享同一区域对象导致双重释放。prgnAPI的临时性 :prgnAPI只在CLIPPING_UpdateGCRegion内短暂存在(先删旧、再建新、合成完即弃用),它不是 DC 的持久状态,DC_vCleanup中的REGION_Delete(pdc->prgnAPI)只是兜底清理。- 分配失败降级 :
CLIPPING_UpdateGCRegion两处ERR()说明分配prgnAPI/prgnRao失败时直接返回(erclClip保持空、fs保持脏),绘制将退化为"无裁剪"(全区域绘制)------"Better than crashing" 的工程取舍。
9. 源码索引
| 文件 | 关键内容 |
|---|---|
| cliprgn.c(file:///d:/reactos/win32ss/gdi/ntgdi/cliprgn.c) | 18 个函数(本分析全部展开):区域维护 4、裁剪选择 4、操作/查询/可见性 7、元区域与合成 3 |
| cliprgn.h(file:///d:/reactos/win32ss/gdi/ntgdi/cliprgn.h) | 跨模块导出:GdiSelectVisRgn/IntGdiExtSelectClipRgn/CLIPPING_UpdateGCRegion/UpdateVisRgn 等 |
| dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h) | DC/DCLEVEL/DCFLAGS(DC_DIRTY_RAO)/DC_LockDc |
| region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c)(《分析_38》) | REGION_* 布尔运算、IntGdiCombineRgn、IntSysCreateRectpRgn、prgnDefault、REGION_Complexity/GetRgnBox/PtInRegion/RectInRegion |
| region.h(file:///d:/reactos/win32ss/gdi/ntgdi/region.h) | REGION 结构、IntSysCreateRectpRgnIndirect 宏 |
| dclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c) | DC 初始化(prgnVis 初值)、DC_vPrepareDCsForBlit(绘制前合成触发)、DC_vCleanup(裁剪资源回收) |
| dcutil.c(file:///d:/reactos/win32ss/gdi/ntgdi/dcutil.c) | IntSetDefaultRegion(默认区域恢复,调用 IntGdiReleaseRaoRgn) |
| engobjects.h(file:///d:/reactos/win32ss/gdi/eng/engobjects.h) | XCLIPOBJ 结构(pClipRgn = prgnRao ? prgnRao : prgnVis) |
| clip.c(file:///d:/reactos/win32ss/gdi/eng/clip.c) | IntEngUpdateClipRegion/IntEngInitClipObj/IntEngFreeClipResources/CLIPOBJ_cEnumStart/CLIPOBJ_bEnum |
| vis.c(file:///d:/reactos/win32ss/user/ntuser/vis.c) | VIS_ComputeVisibleRegion(窗口可见区域计算) |
| windc.c(file:///d:/reactos/win32ss/user/ntuser/windc.c) | DceGetVisRgn/DceUpdateVisRgn(调用 GdiSelectVisRgn 的联动点) |
| ntgdihdl.h(file:///d:/reactos/win32ss/include/ntgdihdl.h) | GDI_ENTRY_VALIDATE_VIS、ATTR_RGN_VALID/DIRTY、GDI_TABLE_ENTRY、DC_ATTR.VisRectRegion |
| ntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h) | GDIBS_NORECT(批处理 NULL 矩形标志) |
| wingdi.h(file:///d:/reactos/sdk/include/psdk/wingdi.h) | RGN_AND/OR/XOR/DIFF/COPY、NULLREGION/SIMPLEREGION/COMPLEXREGION |
10. 与《分析_31》及分册关系
| 项 | 说明 |
|---|---|
| 《分析_31》2.2 表 cliprgn.c(6 入口) | 本分析按源码实际展开为 8 个 NtGdi* 系统调用 + 10 个内部函数(共 18 个) |
| 区域运算 | region.c(《分析_38》):IntGdiCombineRgn、REGION_bCopy/bIntersectRegion/bOffsetRgn、REGION_Complexity 等 |
| 裁剪合成触发点 | dclife.c DC_vPrepareDCsForBlit(《分析_49》) |
| 引擎裁剪对象 | XCLIPOBJ/CLIPOBJ(《分析_4》2.2/3.2、eng/clip.c) |
| 窗口可见区域 | ntuser vis.c/windc.c(窗口管理分册) |
本文档基于 ReactOS 源代码
win32ss/gdi/ntgdi/cliprgn.c(855 行,18 个函数)及关联模块逐行分析(2026 年 8 月)。所有函数签名、行号、行为描述均以源码为准。
11. 调试观察
11.1 调试通道
cliprgn.c 使用 DBG_DEFAULT_CHANNEL(GdiClipRgn) 声明调试通道(文件头部)。在调试构建中:
DPRINT(IntGdiExtSelectClipRgn内的 "A/B" 复杂度打印)只在启用GdiClipRgn通道时输出,用于观察合成前后复杂度变化;DPRINT1(GdiSelectVisRgn的 "SVR: Setting NULL Region")在非 NDEBUG 构建中总是输出,可用于判断 ntuser 是否以空区域调用;ERR(CLIPPING_UpdateGCRegion的两处分配失败)总是输出,出现"Failed to allocate prgnRao/prgnAPI! Expect drawing issues!"说明内存不足且绘制将失去裁剪约束;- 序列日志(串口)中可通过
warn:/err:前缀区分严重级别(见 AGENTS.md 调试章节)。
11.2 常见问题的排查线索
| 现象 | 排查方向 |
|---|---|
| 绘制内容越过窗口边界 | 检查 prgnVis 是否正确注入(GdiSelectVisRgn 断点)、ptlDCOrig 是否与窗口位置一致、CLIPPING_UpdateGCRegion 是否被触发 |
GetClipBox 返回区域比预期大 |
检查 prgnClip 是否被意外清空(如 SelectClipRgn(NULL, RGN_COPY))、DC_DIRTY_RAO 是否被清除过 |
| 裁剪不生效(全区域绘制) | 检查 CLIPPING_UpdateGCRegion 的 ERR 分配失败日志;检查 prgnAPI 是否为 NULL |
PtVisible 结果与 RectVisible 不一致 |
已知不对称:PtVisible 不强制合成,可能读到旧 prgnRao;对照 4.14 注意事项 |
SetMetaRgn 后绘制异常 |
检查 SaveDC/RestoreDC 与 prgnMeta 指针转移的深拷贝契约(8.4) |
| 窗口移动后裁剪滞后一帧 | 检查 DCE 脏标记(DCX_DCEDIRTY)与 DceUpdateVisRgn 是否在重绘前执行(6.3) |
11.3 与相关分册的联调提示
- 修改
prgnVis注入逻辑时,需同步验证 vis.c(file:///d:/reactos/win32ss/user/ntuser/vis.c) 的区域计算与 windc.c(file:///d:/reactos/win32ss/user/ntuser/windc.c) 的 DCE 生命周期; - 修改
CLIPPING_UpdateGCRegion时,需回归eng/clip.c的IntEngUpdateClipRegion契约(count>1才拷贝矩形,否则Rects=&rclBounds),避免驱动读到过期缓冲; - 区域布尔运算的边界情况(空区域、包围盒不相交、坐标溢出)由 region.c 的
REGION_RegionOp系列处理(《分析_38》),cliprgn.c 本身不做防御------若合成结果异常,优先排查 region.c。
12. 术语表
| 术语 | 全称/出处 | 含义 |
|---|---|---|
prgnVis |
Visible Region | 窗口可见区域:窗口在屏幕上真正可见的部分,由 ntuser 计算、GdiSelectVisRgn 注入,永不 NULL |
prgnRao |
Raster Operation Area | 有效合成区域:prgnVis ∩ prgnAPI,绘制与查询的事实标准;NULL 表示无 API 裁剪 |
prgnAPI |
API Region | 应用裁剪组合:prgnClip ∩ prgnMeta,仅在 CLIPPING_UpdateGCRegion 内临时存在 |
prgnClip |
Clip Region | 应用裁剪区域(dclevel 内),对应 SelectClipRgn/ExcludeClipRect/IntersectClipRect/OffsetClipRgn |
prgnMeta |
Meta Region | 元区域(dclevel 内),SetMetaRgn 冻结的裁剪,与新裁剪取交 |
DC_DIRTY_RAO |
DC flag 0x0010 | 裁剪脏标记:prgnRao/erclClip/co 失效,需 CLIPPING_UpdateGCRegion 重算 |
erclClip |
Effective Clip Rect | 有效裁剪包围盒(设备坐标),prgnRao->rdh.rcBound 的副本 |
ptlDCOrig |
DC Origin | DC 原点(窗口屏幕位置),三层区域坐标换算的枢纽 |
XCLIPOBJ |
eXtended CLIP OBJect | 引擎裁剪对象(DC.co),pClipRgn 指向 prgnRao 或 prgnVis |
VisRectRegion |
DC_ATTR 成员 | 可见区域属性(复杂度+包围盒),供用户态 GDI32 共享读取 |
GDI_ENTRY_VALIDATE_VIS |
句柄表标志 4 | "可见区域数据需重新验证",置位/清除包夹 VisRectRegion 更新 |
RGN_AND/OR/XOR/DIFF/COPY |
1/2/3/4/5 | 区域合成模式:求交/求并/异或/相减/拷贝 |
NULLREGION/SIMPLEREGION/COMPLEXREGION |
1/2/3 | 区域复杂度:空 / 单矩形 / 多矩形;ERROR=0 |
GDIBS_NORECT |
0x80000000 | GDI 批处理中表示"NULL 矩形"(ExtSelectClipRgn/TextOut 共用) |
prgnDefault |
全局哨兵区域 | prgnVis 释放后的退路(REGION_Delete 对其免疫) |
DCX_DCEDIRTY |
DCE 标志 | ntuser 侧 DCE 脏标记,触发 DceUpdateVisRgn 重算可见区域 |
(完)