ReactOS 图形系统分析(43):API 裁剪 — cliprgn.c

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 月分析时点),调试通道为 GdiClipRgnDBG_DEFAULT_CHANNEL(GdiClipRgn)),文件头注释自述其职责为 "Clip region functions"。

从结构上看,该文件包含 18 个函数,可分成四类:

  1. 可见/RAO 区域维护 (4 个):IntGdiReleaseRaoRgnIntGdiReleaseVisRgnUpdateVisRgnGdiSelectVisRgn ------ 负责维护 DC 的 prgnVis(窗口可见区域)与 prgnRao(有效合成区域),并与 ntuser 窗口系统联动。

  2. 应用裁剪选择 (4 个):IntSelectClipRgnIntGdiExtSelectClipRectIntGdiExtSelectClipRgnNtGdiExtSelectClipRgn ------ 对应 Win32 API SelectClipRgn(含 RGN_AND/OR/XOR/DIFF/COPY 五种合成模式)。

  3. 裁剪操作/查询/可见性测试 (7 个):NtGdiExcludeClipRect(= ExcludeClipRect)、NtGdiIntersectClipRect(= IntersectClipRect)、NtGdiOffsetClipRgn(= OffsetClipRgn)、GdiGetClipBox / NtGdiGetAppClipBox(= GetClipBox)、NtGdiPtVisible(= PtVisible)、NtGdiRectVisible(= RectVisible)。

  4. 元区域与合成 (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 采用脏标记 + 惰性求值

  1. 任何裁剪相关变更只调用 IntGdiReleaseRaoRgn(pDC),它只置位 pDC->fs |= DC_DIRTY_RAO (并通知用户态 GDI32 重读可见区域属性),不碰 prgnRao 本身;

  2. 等到真正需要 有效区域时(绘制前 dclife.cDC_vPrepareDCsForBlitGdiGetClipBoxNtGdiRectVisible 等),检查到 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 / prgnMetaIntSelectClipRgn 等维护,同样以 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 通过句柄表属性读取

DCLEVELdclevel)是一个独立的子结构(dc.h(file:///d:/reactos/win32ss/gdi/ntgdi/dc.h#L49-L89)),保存随 SaveDC/RestoreDC 保存/恢复的 DC 状态,prgnClipprgnMetapSurfacesizl 都在其中。这解释了为何裁剪区域要分两处放: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) 表示失败;
  • 注意 ERRORNULLREGION 是不同的值(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 注释也印证:"With pDC->co.pClipRgn = pDC->prgnRao ? pDC->prgnRao : pDC->prgnVisCLIPOBJ_cEnumStart uses XCLIPOBJ to build the rects from region objects rects in pClipRgn->Buffer";
  • IntEngUpdateClipRegioneng/clip.c(file:///d:/reactos/win32ss/gdi/eng/clip.c#L171-L215))根据矩形数量填充复杂度字段:count>1iDComplexity=DC_COMPLEXiFComplexity=(≤4?FC_RECT4:FC_COMPLEX)count≤1iDComplexity=DC_TRIVIAL(空矩形)或 DC_RECTiFComplexity=FC_RECTRects=&rclBounds。图形驱动通过 CLIPOBJ_cEnumStart/CLIPOBJ_bEnum 枚举这些矩形做逐矩形绘制。

3.5 句柄表标志与区域属性

IntGdiReleaseRaoRgn/UpdateVisRgn 操作 GDI 句柄表条目(GdiHandleTable->Entries[Index]):

  • GDI_ENTRY_VALIDATE_VISntgdihdl.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 个);GdiGetClipBoxGdiSelectVisRgn 为跨模块导出的内部接口(见 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,也不重算,只做两件事:
    1. pDC->fs |= DC_DIRTY_RAO; ------ 置脏标记,后续绘制/查询前由 CLIPPING_UpdateGCRegion 重算;
    2. Entry->Flags |= GDI_ENTRY_VALIDATE_VIS; ------ 通过句柄表通知用户态 GDI32 重读可见区域属性。
  • 实现流程GDI_HANDLE_GET_INDEX(pDC->BaseObject.hHmgr) 从 DC 对象头的句柄值取出句柄表下标,定位 GDI_TABLE_ENTRY,然后置位上述两项。
  • 使用方式 :被本文件几乎所有修改裁剪的函数调用(IntSelectClipRgn 各处、NtGdiOffsetClipRgnIntGdiSetMetaRgnGdiSelectVisRgn),也被 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)
  • 作用 :释放可见区域并退回默认值。流程:
    1. IntGdiReleaseRaoRgn(pDC); ------ 先置 RAO 脏标记;
    2. REGION_Delete(pDC->prgnVis); ------ 删除旧可见区域;
    3. 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 等用户态快捷路径读取。
  • 实现流程
    1. GDI_ENTRY_VALIDATE_VIS(防止用户态在同步中途读到半更新数据);
    2. pdc->pdcattr->VisRectRegion.iComplexity = REGION_GetRgnBox(pdc->prgnVis, &pdc->pdcattr->VisRectRegion.Rect); ------ 一次性取得复杂度与包围盒(REGION_GetRgnBox 返回复杂度,见《分析_38》);
    3. AttrFlags = ATTR_RGN_VALID; ------ 标记属性有效;
    4. 清除 GDI_ENTRY_VALIDATE_VIS
  • 使用方式GdiSelectVisRgn(间接)、NtGdiOffsetClipRgnCLIPPING_UpdateGCRegion 末尾都会调用,保证属性与 prgnVis 一致。
  • 注意事项VisRectRegionDC_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 的唯一入口。DceUpdateVisRgnwindc.c(file:///d:/reactos/win32ss/user/ntuser/windc.c#L267))在 GetDC/BeginPaint/窗口变化后调用它。
  • 实现流程
    1. DC_LockDc(hdc),失败则 EngSetLastError(ERROR_INVALID_HANDLE)
    2. if (!prgn):打印 "SVR: Setting NULL Region",调用 IntGdiReleaseVisRgn(dc) + IntSetDefaultRegion(dc)(恢复默认全窗口区域),解锁返回;
    3. 否则 dc->fs |= DC_DIRTY_RAO;(可见区域变了,有效区域必然失效);
    4. REGION_bCopy(dc->prgnVis, prgn); ------ 深拷贝(ntuser 传入的区域随后会被 REGION_Delete,见 windc.c L272-275);
    5. REGION_bOffsetRgn(dc->prgnVis, -dc->ptlDCOrig.x, -dc->ptlDCOrig.y); ------ 关键坐标变换:ntuser 给的是设备坐标,DC 内部保存 DC 相对坐标,减原点;
    6. DC_UnlockDc(dc)
  • 使用方式 :ntuser windc.c 三处调用(L267 主路径、L632/L1130 空区域场景);dclife.cDC_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。把调用方区域 prgnfnMode 合成进 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 或 已有原裁剪)"):这类模式必须与"原裁剪"或"全区域基准"做布尔运算,不能原地替换:

    1. 新建空区域 prgnNClip
    2. 已有原裁剪prgnOrigClip 非空):Ret = IntGdiCombineRgn(prgnNClip, prgnOrigClip, prgn, fnMode)。成功则删除原区域、dc->dclevel.prgnClip = prgnNClipIntGdiReleaseRaoRgn;失败则删除 prgnNClip。注意:此时 prgn 为 NULL 会失败(IntGdiCombineRgnprgnSrc2 == NULL 且非 COPY 模式断言)。
    3. 无原裁剪 :以 prgnVis 的包围盒作为"全区域"基准------REGION_GetRgnBox(dc->prgnVis, &rcl)prgnClip = IntSysCreateRectpRgnIndirect(&rcl),再 IntGdiCombineRgn(prgnNClip, prgnClip, prgn, fnMode),成功则挂接并释放 RAO,随后删除临时 prgnClip。(函数中有一段 #if 0 的历史代码:对 PDEV_META_DEVICE 元设备还要叠加 ppdev->ptlOrigionptlDCOrig,当前版本改为直接取 prgnVis 包围盒。)

    分支 B(fall-through,即 RGN_COPY,或 RGN_AND 且无原裁剪)

    1. !prgn(NULL 区域,RGN_COPY 语义) :若已有原裁剪则删除之、dclevel.prgnClip = NULLIntGdiReleaseRaoRgn;返回 SIMPLEREGION(NULL 裁剪 = 全区域 = 简单区域)。
    2. 有原裁剪IntGdiCombineRgn(prgnOrigClip, prgn, NULL, RGN_COPY) 原地替换原裁剪("Clip could fail",失败时 Ret=ERROR 且原区域保持),成功则 IntGdiReleaseRaoRgn
    3. 无原裁剪 :新建 prgnNClipREGION_bCopy(prgnNClip, prgn)Ret = REGION_Complexity(prgnNClip),挂接并释放 RAO。
  • 返回 :新 prgnClip 的复杂度(NULLREGION/SIMPLEREGION/COMPLEXREGION)或 ERROR。_Success_ 注解要求非 ERROR。

  • 注意事项

    • prgnClipprgnMeta 都存在时,最终有效区域由 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 标志。
  • 实现流程
    1. BOOL NoRegion = fnMode & GDIBS_NORECT; fnMode &= ~GDIBS_NORECT; ------ 剥离批处理标志;
    2. NoRegion(NULL 区域语义) :仅当 fnMode == RGN_COPYIntSelectClipRgn(dc, NULL, RGN_COPY),随后若 DC_DIRTY_RAOCLIPPING_UpdateGCRegion(dc)(这里主动立即合成 ,因为批处理路径没有后续绘制前的兜底),返回 REGION_Complexity(dc->prgnVis)(NULL 裁剪 = 全可见区域);
    3. 有矩形
      • 快捷路径(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)
  • 返回值语义 :与 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 分支直接调用它)。
  • 实现流程
    1. !prgn(NULL 区域):仅 RGN_COPYIntSelectClipRgn(dc, NULL, RGN_COPY),成功则返回 REGION_Complexity(dc->prgnVis)
    2. 否则 IntSelectClipRgn(dc, prgn, fnMode);若 DC_DIRTY_RAOCLIPPING_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 同时服务 SelectClipRgnExtSelectClipRgn 两个用户态 API)。
  • 实现流程
    1. 参数校验fnMode < RGN_AND || fnMode > RGN_COPYEngSetLastError(ERROR_INVALID_PARAMETER) 返回 ERROR;
    2. DC_LockDc(hDC) 失败 → EngSetLastError(ERROR_INVALID_HANDLE)
    3. prgn = REGION_LockRgn(hrgn)REGION_LockRgn 会先 REGION_vSyncRegion 同步用户态属性,见《分析_38》);
    4. prgn == NULL && fnMode != RGN_COPYretval = ERROR(注释明言 "doesn't set this",即不设置 last error,与 Windows 行为对齐------SelectClipRgn 传无效句柄时行为未定义);
    5. 否则 retval = IntGdiExtSelectClipRgn(dc, prgn, fnMode)#if 0 内是走批处理矩形的实验代码);
    6. REGION_UnlockRgn(prgn)DC_UnlockDc(dc),返回。
  • 返回值:ERROR / NULLREGION / SIMPLEREGION / COMPLEXREGION(见 4.7 的语义)。
  • 注意事项REGION_LockRgnhrgn 无效时返回 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)的包围盒(逻辑坐标)。
  • 实现流程
    1. DC_LockDc(hdc)
    2. if (pdc->fs & DC_DIRTY_RAO) CLIPPING_UpdateGCRegion(pdc); ------ 先确保合成
    3. prgnRaoREGION_GetRgnBox(pdc->prgnRao, prc);无(意味着无 API 裁剪):ASSERT(pdc->prgnVis)REGION_GetRgnBox(pdc->prgnVis, prc)
    4. IntDPtoLP(pdc, (LPPOINT)prc, 2) ------ 设备坐标转逻辑坐标(两个点即矩形的对角);
    5. 解锁返回复杂度。
  • 注意事项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 − rectRGN_DIFF)。
  • 实现流程
    1. DC_LockDc 失败 → EngSetLastError(ERROR_INVALID_HANDLE)
    2. 组装 RECTLRECTL_vMakeWellOrdered(&rect) 规范化(左右/上下颠倒自动修正),IntLPtoDP(pdc, (LPPOINT)&rect, 2) 转设备坐标;
    3. prgn = IntSysCreateRectpRgnIndirect(&rect)
    4. iComplexity = IntSelectClipRgn(pdc, prgn, RGN_DIFF);(核心逻辑见 4.5 分支 A);
    5. REGION_Delete(prgn)(临时区域用完即弃);
    6. Windows 行为模拟if (iComplexity == SIMPLEREGION) iComplexity = COMPLEXREGION; ------ 挖洞后不可能是"简单矩形",Windows 语义上 ExcludeClipRect 结果要么 NULLREGION 要么 COMPLEXREGION;
    7. 解锁返回。
  • 注意事项 :本函数不主动 调用 CLIPPING_UpdateGCRegionIntSelectClipRgn 只置了脏标记),合成被推迟;这与 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 ∩ rectRGN_AND)。
  • 实现流程 :与 NtGdiExcludeClipRect 完全同构,仅两处不同:
    • 合成模式改为 RGN_ANDIntSelectClipRgn(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)(逻辑坐标)。
  • 实现流程
    1. DC_LockDc;失败且 !hdcEngSetLastError(ERROR_INVALID_HANDLE)(对 NULL 句柄的特殊处理);
    2. 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 已置位,此处为防御性重复);
    3. prgnClip (NULL 裁剪 = 全区域):iComplexity = SIMPLEREGION(平移不影响全区域);
    4. 解锁返回复杂度。
  • 注意事项 :本函数是少数直接 调用 UpdateVisRgn 的地方(因为 prgnClip 平移后,即使尚未合成,可见属性也应保持一致性);REGION_bOffsetRgnnCount == 0 的空区域直接返回 TRUE(见《分析_38》),因此空裁剪平移不报错。

4.14 NtGdiPtVisible(cliprgn.c L595-621)

c 复制代码
BOOL APIENTRY NtGdiPtVisible(HDC hDC, int X, int Y)
  • 作用 :Win32 PtVisible:判断逻辑坐标点 (X, Y) 是否在当前可见裁剪区域内。
  • 实现流程
    1. DC_LockDc;失败 → EngSetLastError(ERROR_INVALID_HANDLE),返回 FALSE;
    2. prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis; ------ 有有效合成区域用它,否则用可见区域;
    3. POINT pt = {X, Y}; IntLPtoDP(dc, &pt, 1); 转设备坐标(注意是单点变换);
    4. ret = REGION_PtInRegion(prgn, pt.x, pt.y); ------ 命中测试(先测包围盒 INRECT,再逐矩形扫描,见《分析_38》);
    5. 解锁返回。
  • 注意事项(与 NtGdiRectVisible 的差异) :本函数不检查 DC_DIRTY_RAO 也不调用 CLIPPING_UpdateGCRegion------若裁剪刚被修改(prgnRao 尚为旧值或已被删除而脏标记置位),本函数可能返回基于旧/不完整区域的结果。这是源码中的一处不对称(GdiGetClipBoxNtGdiRectVisible 都有合成检查,唯独 NtGdiPtVisible 没有),分析时值得留意。

4.15 NtGdiRectVisible(cliprgn.c L623-673)

c 复制代码
BOOL APIENTRY NtGdiRectVisible(HDC hDC, LPRECT UnsafeRect)
  • 作用 :Win32 RectVisible:判断矩形(逻辑坐标,可任意颠倒)是否与当前可见裁剪区域相交(任意非空交集即 TRUE)。
  • 实现流程
    1. DC_LockDc;失败 → EngSetLastError(ERROR_INVALID_HANDLE)
    2. SEH 读用户缓冲_SEH2_TRY { ProbeForRead(UnsafeRect, sizeof(RECT), 1); Rect = *UnsafeRect; } 异常则 SetLastNtError(Status) 返回 FALSE------这是与 NtGdiPtVisible(值参数,无需探测)的重要区别;
    3. if (dc->fs & DC_DIRTY_RAO) CLIPPING_UpdateGCRegion(dc); ------ 先合成 (与 PtVisible 形成对照);
    4. prgn = dc->prgnRao ? dc->prgnRao : dc->prgnVis;
    5. IntLPtoDP(dc, (LPPOINT)&Rect, 2); 转设备坐标;
    6. Result = REGION_RectInRegion(prgn, &Rect); ------ REGION_RectInRegion 内部会先归一化矩形再与包围盒做 EXTENTCHECK 快速排除,然后带 y 分带跳过逻辑逐矩形扫描;
    7. 解锁返回。
  • 注意事项 :矩形"任意颠倒"由 REGION_RectInRegion 内部处理(自动交换 left/right、top/bottom),NtGdiRectVisible 本身不调用 RECTL_vMakeWellOrdered

4.16 IntGdiSetMetaRgn(cliprgn.c L675-720)

c 复制代码
int FASTCALL IntGdiSetMetaRgn(PDC pDC)
  • 作用 :内部版 SetMetaRgn(Win32 语义:把当前裁剪区域与现有元区域组合为新元区域 ,然后清空裁剪区域 ------此后应用设置的任何新裁剪都会与冻结的元区域取交)。四种组合情形:
    1. 有元区域 + 有裁剪prgn = 新建空区域REGION_bIntersectRegion(prgn, prgnMeta, prgnClip)(两区域取交),成功后删除旧 prgnMeta 并把 prgn 挂为新的 prgnMeta;再删除旧 prgnClip 并置 prgnClip = NULLIntGdiReleaseRaoRgn;返回 REGION_Complexity(prgnMeta)
    2. 有元区域 + 无裁剪 :元区域不变,直接返回 REGION_Complexity(prgnMeta)
    3. 无元区域 + 有裁剪prgnMeta = prgnClip; prgnClip = NULL;指针搬运 ,不拷贝),返回 REGION_Complexity(prgnMeta)
    4. 无元区域 + 无裁剪 :返回 SIMPLEREGION
  • 使用方式 :由 NtGdiSetMetaRgn 调用;SaveDC/RestoreDC 相关代码也引用 prgnMeta(见注释 "See Restore/SaveDC")。
  • 注意事项 :情形 3 是"零拷贝"的指针转移,因此 SaveDC 保存 DC 状态时对 prgnMeta/prgnClip 的处理必须格外小心(深度拷贝而非浅拷贝),否则恢复后会出现双重释放/悬垂指针;情形 1 的求交意味着"先 SetMetaRgnSelectClipRgn"的最终有效区域是元区域 ∩ 新裁剪

4.17 NtGdiSetMetaRgn(cliprgn.c L723-737)

c 复制代码
int APIENTRY NtGdiSetMetaRgn(HDC hDC)
  • 作用SetMetaRgn 的系统调用包装:DC_LockDcIntGdiSetMetaRgn(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)
  • 作用唯一的合成器 :把三层裁剪(prgnVisdclevel.prgnClipdclevel.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(应用裁剪层组合)

    c 复制代码
    if (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(有效区域)

    c 复制代码
    if (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 的拷贝)。

    ④ 设备坐标化 + 引擎对接

    c 复制代码
    REGION_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.cDC_vPrepareDCsForBlitdclife.c(file:///d:/reactos/win32ss/gdi/ntgdi/dclife.c#L549-L550))在每次绘制前检查 pdcDest->fs & DC_DIRTY_RAO 并调用;GdiGetClipBoxNtGdiRectVisibleIntGdiExtSelectClipRect/Rgn(批处理与 Nt 路径)也会触发。

  • 注意事项

    • 注释指出 pDC->co 应被驱动使用:"CLIPOBJ_cEnumStart uses XCLIPOBJ to build the rects from region objects rects in pClipRgn->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 ...)

要点回顾:

  1. 合成只发生一次CLIPPING_UpdateGCRegion 同时刷新 prgnAPIprgnRaoerclClippDC->co 四者,且只在 DC_DIRTY_RAO 置位时执行,之后立即清除脏标记。
  2. prgnRao 是绘制事实标准 :所有绘制入口(dclife.c 的 blit 准备路径)与查询(GetClipBox/RectVisible)都优先使用 prgnRaoNULL 时回退 prgnVis
  3. 驱动只认识 XCLIPOBJ :驱动回调(DrvBitBlt 等)收到的是 CLIPOBJ*(即 &pDC->co),通过枚举 API 逐矩形获取裁剪,无需理解 win32k 的三层区域;erclClip 供无需逐矩形枚举的快速路径直接使用。
  4. 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_vCleanupdclife.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 设计要点

  1. 惰性合成:裁剪操作 O(1) 置脏、绘制前 O(n) 合成,把昂贵的区域布尔运算从"每次修改"摊销到"每次绘制";
  2. 三层职责分离prgnVis(系统)永远不可被应用覆盖;prgnClip(应用)可自由修改;prgnMeta(录制)提供一次性的"冻结"语义;
  3. 坐标空间双向转换-ptlDCOrig(注入时)与 +ptlDCOrig(合成时)严格配对,保证 prgnVis/prgnRao 稳定在 DC 相对坐标,erclClip/co 稳定在设备坐标;
  4. prgnDefault 哨兵prgnVis 永不置 NULL,释放即退回全局默认区域,避免合成器与查询路径的 NULL 检查泛滥;
  5. 用户态联动GDI_ENTRY_VALIDATE_VIS + VisRectRegion 属性让 GDI32 能以极低成本获取"当前可见裁剪"快照,不必每次走系统调用;
  6. Windows 兼容细节ExcludeClipRect/IntersectClipRectSIMPLEREGION→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 删除 prgnClipprgnVis 不变,有效区域=全可见区域
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 返回"可见裁剪"而非"应用裁剪"、SelectClipRgnRGN_COPY 与 NULL 区域特殊语义。差异点集中在内部架构(三层区域、惰性合成、XCLIPOBJ)而非对外行为。

8.4 内存与生命周期细节

  • 系统区域 vs 用户区域prgnVis/prgnRao/prgnAPI/dclevel.prgnClip/prgnMeta 全部通过 IntSysCreateRectpRgn 创建(无句柄、prgnattr = &rgnattrATTR_RGN_VALID,见 region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c#L2408-L2432)),是纯内核私有 区域;而应用传入的 hrgn 是带句柄与用户态属性的"用户区域"(REGION_LockRgn 加锁)。cliprgn.c 只以"值拷贝"方式消费用户区域,从不持有其指针,因此无需担心用户区域被删除后的悬垂引用。
  • REGION_DeleteprgnDefault 哨兵REGION_Delete(pRgn) 内部对 prgnDefault 直接返回(region.c L2452-2458),这是 IntGdiReleaseVisRgn 能把 prgnVis 指向 prgnDefault 的安全前提;任何路径都不允许对 prgnDefault 调用删除。
  • SaveDC/RestoreDCdclevelprgnClip/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/DCFLAGSDC_DIRTY_RAO)/DC_LockDc
region.c(file:///d:/reactos/win32ss/gdi/ntgdi/region.c)(《分析_38》) REGION_* 布尔运算、IntGdiCombineRgnIntSysCreateRectpRgnprgnDefaultREGION_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_VISATTR_RGN_VALID/DIRTYGDI_TABLE_ENTRYDC_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/COPYNULLREGION/SIMPLEREGION/COMPLEXREGION

10. 与《分析_31》及分册关系

说明
《分析_31》2.2 表 cliprgn.c(6 入口) 本分析按源码实际展开为 8 个 NtGdi* 系统调用 + 10 个内部函数(共 18 个)
区域运算 region.c(《分析_38》):IntGdiCombineRgnREGION_bCopy/bIntersectRegion/bOffsetRgnREGION_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) 声明调试通道(文件头部)。在调试构建中:

  • DPRINTIntGdiExtSelectClipRgn 内的 "A/B" 复杂度打印)只在启用 GdiClipRgn 通道时输出,用于观察合成前后复杂度变化;
  • DPRINT1GdiSelectVisRgn 的 "SVR: Setting NULL Region")在非 NDEBUG 构建中总是输出,可用于判断 ntuser 是否以空区域调用;
  • ERRCLIPPING_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_UpdateGCRegionERR 分配失败日志;检查 prgnAPI 是否为 NULL
PtVisible 结果与 RectVisible 不一致 已知不对称:PtVisible 不强制合成,可能读到旧 prgnRao;对照 4.14 注意事项
SetMetaRgn 后绘制异常 检查 SaveDC/RestoreDCprgnMeta 指针转移的深拷贝契约(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.cIntEngUpdateClipRegion 契约(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 指向 prgnRaoprgnVis
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 重算可见区域

(完)

相关推荐
W_326002 小时前
Python文件进阶:一维数据与 CSV 文件读写
开发语言·python
Tyler_TXZ2 小时前
C++C语言之——二叉树
c语言·开发语言·数据结构·c++·二叉树
小羊先生car2 小时前
F429-HAL-RS485(回环/双机实验)(2026/8/16)
c语言·单片机·嵌入式硬件·软件构建
不会代码的小猴2 小时前
C++新增关键字
开发语言·c++·笔记
yaoxin5211232 小时前
497. Java 反射 - 使用反射读取注解
java·开发语言·python
2019一路前行3 小时前
Python 函数式编程
开发语言·python
冯汉栩3 小时前
Swift Control View,Label渐变颜色(源码)
开发语言·ios·swift
白狐_7985 小时前
408 数据结构|线索二叉树两题详解:先序线索化后的空链域 + 中序前驱/后继判断
c语言·数据结构·链表
从小就看凹凸曼^o^5 小时前
Python基础5 - 字典与集合:(1)字典
开发语言·python
GeekZHR5 小时前
C语言指针进阶补充6:动态内存管理、mem系列内存函数、复杂指针声明,一次补齐指针的“三大盲区“
java·c语言·算法·指针