ReactOS 图形系统分析(25):多显示器/平移显示与杂项 — multidisp.c / pandisp.c / engmisc.c
1. 概述
《分析_4》第 45~46 行把multidisp.c/pandisp.c(多显示器/平移显示)与engmisc.c(杂项)并列。三者实为三种角色:
| 文件 | 角色 | 现状 |
|---|---|---|
| [multidisp.c](file:///d:/reactos/win32ss/gdi/eng/multidisp.c) | 多显示器(META)显示驱动入口MultiEnableDriver | UNIMPLEMENTED 桩(仅声明,恒 FALSE) |
| [pandisp.c](file:///d:/reactos/win32ss/gdi/eng/pandisp.c) | 平移显示(Panning Display)驱动:桌面大于屏幕时,用"视口"漫游大桌面 | 完整实现(gPanDispDrvFn 13 个 DDI) |
| [engmisc.c](file:///d:/reactos/win32ss/gdi/eng/engmisc.c) | 引擎杂项服务:设备表面"进入/离开"、系统查询、代码页、tick | 全部实现 |
平移显示(pandisp.c)是核心:它包装一个真实显示驱动(underlying PDEV),对外提供更大的虚拟桌面,内部把绘制重定向到"影子表面(shadow)",再按需把**视口(viewport)**区域同步到物理屏幕。用户移动鼠标到屏幕边缘时视口随之滚动——这就是DEVMODE.dmPanningWidth/dmPanningHeight的语义。
DEVMODE: dmPelsWidth×dmPelsHeight(虚拟桌面) > dmPanningWidth×dmPanningHeight(物理屏幕) ┌─────────────────────────── 虚拟桌面 (szlDesktop) ───────────────────────────┐ │ │ │ ┌─────────────── 视口 rclViewport(物理屏幕大小) ───────────────┐ │ │ │ 光标移到边缘 → 视口滚动 (PanMovePointer) │ │ │ │ 绘制 → 影子表面 psoShadow → PanSynchronize 同步视口区域到屏幕 │ │ │ └───────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────────┘2. 加载机制(pdevobj.c 的 vtable 替换)
[PDEVOBJ_pLoadPDEV/pEnableDisplay](file:///d:/reactos/win32ss/gdi/eng/pdevobj.c#L511-L575) 决定何时启用平移:
/* 1. META 设备 → 多显示器驱动(桩) */if(ldevtype==LDEV_DEVICE_META)pldev=LDEVOBJ_pLoadInternal(MultiEnableDriver,ldevtype);/* 2. 显示设备且请求了平移尺寸 → 用平移 vtable 覆盖驱动函数表 */if(ldevtype==LDEV_DEVICE_DISPLAY&&(dwAccelerationLevel>=5||pdm->dmFields&(DM_PANNINGWIDTH|DM_PANNINGHEIGHT))){/* 缺省填平:dmPanning* = dmPels* */if(!(pdm->dmFields&DM_PANNINGWIDTH))pdm->dmPanningWidth=pdm->dmPelsWidth;if(!(pdm->dmFields&DM_PANNINGHEIGHT))pdm->dmPanningHeight=pdm->dmPelsHeight;/* 用 gPanDispDrvFn 覆盖 ppdev->apfn 对应项 */for(i=0;i<gPanDispDrvCount;i++)ppdev->apfn[gPanDispDrvFn[i].iFunc]=gPanDispDrvFn[i].pfn;}elseppdev->pfn=ppdev->pldev->pfn;/* 普通驱动函数表 */关键:平移驱动不是独立驱动模块,而是同一 PDEV 上对底层驱动函数表的包装层(vtable 替换),无需加载新 DLL。PANDEV保存了底层驱动的完整apfn[INDEX_LAST]表(用GETPFN(name)宏回调底层)。
3. PANDEV 结构与生命周期
3.1 PANDEV([pandisp.c](file:///d:/reactos/win32ss/gdi/eng/pandisp.c#L19-L36))
typedefstruct_PANDEV{ULONG iBitmapFormat;// 底层表面格式HDEV hdev;SIZEL szlDesktop;// 虚拟桌面大小(dmPelsW×H)RECTL rclViewport;// 视口(物理屏幕大小的漫游窗口)HSURF hsurf;// 设备表面(对引擎暴露,桌面大小)HSURF hsurfShadow;// 影子表面(实际绘制目标,内存位图)SURFOBJ*psoShadow;/* 底层 PDEV */ULONG flUnderlyingGraphicsCaps;SIZEL szlScreen;// 物理屏幕大小(dmPanningW×H)DHPDEV dhpdevScreen;// 底层 PDEV 句柄BOOL enabledScreen;// 底层表面是否已启用SURFOBJ*surfObjScreen;// 底层屏幕表面PFN apfn[INDEX_LAST];// 底层驱动函数表(包装回调用)}PANDEV,*PPANDEV;3.2 生命周期 DDI
| 函数 | 行为 |
|---|---|
PanEnablePDEV | 用dmPanningW×H的 DEVMODE启用底层 PDEV(GETPFN(EnablePDEV)),保存底层函数表;把上报给 GDI 的分辨率升级为虚拟桌面dmPelsW×H;置GCAPS_PANNING+GCAPS2_SYNCFLUSH|SYNCTIMER |
PanCompletePDEV/PanDisablePDEV | 透传底层CompletePDEV/DisablePDEV,释放 PANDEV |
PanEnableSurface | ① 启用底层表面并锁定(surfObjScreen)②EngCreateDeviceSurface建桌面大小设备表面 ③EngAssociateSurface挂钩 HOOK_BITBLT/COPYBITS/ALPHABLEND/GRADIENTFILL/STROKEPATH/SYNCHRONIZE/TEXTOUT ④EngCreateBitmap建影子表面(桌面大小,BMF_TOPDOWN);视口初始居中 |
PanDisableSurface | 逆序清理(可被PanEnableSurface的失败路径调用,故各步判空) |
设计:对 GDI/引擎而言,设备表面是桌面大小;而真正可见的物理屏幕只是其中rclViewport一块。引擎的绘制都发生在影子表面(引擎可直接读写内存位图),再由同步机制拷到物理屏。
4. 绘制重定向与同步
4.1 重定向到影子表面
PanBitBlt(psoTrg,psoSrc,...){/* 设备表面 → 影子表面(源/目标都换) */if(psoTrg->iType==STYPE_DEVICE)psoTrg=pandev->psoShadow;if(psoSrc&&psoSrc->iType==STYPE_DEVICE)psoSrc=pandev->psoShadow;res=EngBitBlt(psoTrg,psoSrc,...);// 引擎软件绘制if(res)PanSynchronize(gPan,NULL);// 绘制完立刻同步(见下)returnres;}同类包装:PanCopyBits(→PanBitBlt,ROP4_SRCCOPY)、PanAlphaBlend(→EngAlphaBlend到影子)、PanGradientFill(→EngGradientFill到影子)。PanStrokePath/PanTextOut未实现(UNIMPLEMENTED + ASSERT(FALSE))。
4.2 同步 — PanSynchronize
PanSynchronize(dhpdev,prcl){/* FIXME: 当前整块拷贝影子缓冲,未按 prcl 局部优化 */ptlSrc=rclViewport.left/top;// 视口左上角rclDest=0,0,szlScreen.cx,szlScreen.cy;// 整块物理屏幕EngCopyBits(surfObjScreen,psoShadow,NULL,NULL,&rclDest,&ptlSrc);}把影子表面中视口区域拷贝到物理屏幕表面(EngCopyBits)。注册为INDEX_DrvSynchronize,同时GCAPS2_SYNCFLUSH让引擎在绘制后调用;但源码 FIXME 明确:当前靠各绘制函数手动调用(GCAPS2_SYNCTIMER定时同步尚未实现,用全局gPan兜底)。
5. 视口漫游 — PanMovePointer
光标移到屏幕边缘时移动视口(GCAPS_PANNING语义):
PanMovePointer(pso,x,y,prcl){/* 忽略隐藏/平移通知(无 DrvSetPointerShape,ASSERT x>=0 && y<0) */if(x<0||y>=0)return;if(底层支持 GCAPS_PANNING)底层MovePointer(...);// 透传底层光标y+=pso->sizlBitmap.cy;// 光标 y 相对屏幕底部换算/* 视口跟随光标:越左/右/上/下边缘则滚动 */if(x<rclViewport.left){rclViewport.left=x;right=x+szlScreen.cx;bChanged=TRUE;}elseif(x>=rclViewport.right){right=x+1;left=x-szlScreen.cx;bChanged=TRUE;}/* y 同理 */if(bChanged)PanSynchronize(dhpdev,NULL);// 滚动后立即同步}6. 多显示器 — multidisp.c(桩)
MultiEnableDriver是LDEV_DEVICE_META(多显示器设备)的驱动入口——Windows 中由它聚合多个 PDEV 成统一桌面(与MDEVOBJ多显示器管理配套)。ReactOS 当前为UNIMPLEMENTED桩(恒返回 FALSE),即META 设备路径未启用,多显示器目前只靠mdevobj.c的 MDEV 映射对象做基础支持。
7. 杂项 — engmisc.c
7.1 IntEngEnter / IntEngLeave — 设备表面的"进入/离开"(核心工具)
问题:引擎软件绘制(BitBlt/AlphaBlend/StretchBlt/LineTo/TransBlt/Gradient)需要能直接读写像素(pvScan0/lDelta)的位图表面;而设备管理表面(无像素缓冲)无法直接操作。
IntEngEnter 解决:把设备表面换成"临时内存位图"再操作,结束后IntEngLeave用DrvCopyBits拷回:
IntEngEnter(&el,psoDest,&DestRect,ReadOnly,&Translate,&psoOutput){if(设备表面&&无 pvScan0){if(!HOOK_COPYBITS)returnFALSE;// 驱动不支持 CopyBits 则失败psoOutput=EngCreateBitmap(...);// 临时位图(目标区域大小)GDIDEVFUNCS(psoDest).CopyBits(psoOutput,psoDest,...);// 先把设备内容拷入Translate=-DestRect.left/top;// 坐标系平移到临时位图}elsepsoOutput=psoDest;// 位图表面直接用/* 若挂钩 HOOK_SYNCHRONIZE → 调用 SynchronizeSurface/Synchronize */}IntEngLeave(&el){if(OutputObj!=DestObj)// 用了临时位图{if(!ReadOnly)GDIDEVFUNCS(DestObj).CopyBits(DestObj,OutputObj,...);// 结果拷回设备表面释放临时位图+TrivialClipObj;}}调用方(36 处):[bitblt.c](file:///d:/reactos/win32ss/gdi/eng/bitblt.c#L965)、[alphablend.c](file:///d:/reactos/win32ss/gdi/eng/alphablend.c#L103)、[stretchblt.c](file:///d:/reactos/win32ss/gdi/eng/stretchblt.c#L205)、[lineto.c](file:///d:/reactos/win32ss/gdi/eng/lineto.c#L553)、[transblt.c](file:///d:/reactos/win32ss/gdi/eng/transblt.c#L40)、[gradient.c](file:///d:/reactos/win32ss/gdi/eng/gradient.c#L85)——这正是"引擎软件渲染能作用于设备表面"的通用桥梁(pandisp 的PanBitBlt回调EngBitBlt时即受益)。
7.2 其他杂项函数
| 函数 | 行为 |
|---|---|
EngGetProcessHandle | 恒返回 NULL(Win2K 起废弃,ABI 保留) |
EngGetCurrentCodePage | 转发RtlGetDefaultCodePage返回 OEM/ANSI 代码页([winddi.h](file:///d:/reactos/sdk/include/psdk/winddi.h) DDI 服务) |
EngQuerySystemAttribute | 查询系统属性:EngNumberOfProcessors(NtQuerySystemInformation Basic)→ CPU 数;EngProcessorFeature(Processor 信息)→ 特性位 |
EngGetTickCount | KeQueryTickCount+KiTickCountToMs返回毫秒 tick |
8. 与《分析_4》标注对照
| 标注 | 核对 |
|---|---|
| 《_4》第 45 行:multidisp.c/pandisp.c =「多显示器/平移显示」 | 基本准确。补充:multidisp.c 是META 设备入口(桩);真正实现是 pandisp.c(平移显示 vtable 包装层);多显示器聚合另见 mdevobj.c(MDEVOBJ) |
| 《_4》第 46 行:engmisc.c =「杂项」 | 准确。核心是IntEngEnter/IntEngLeave(设备表面 ↔ 临时位图的 CopyBits 桥梁),另有系统查询/代码页/tick |
9. 源码索引
| 文件 | 关键内容 |
|---|---|
| [pandisp.c](file:///d:/reactos/win32ss/gdi/eng/pandisp.c) | PANDEV、PanEnablePDEV/EnableSurface/DisableSurface、PanBitBlt/PanCopyBits/PanAlphaBlend/PanGradientFill、PanSynchronize、PanMovePointer、gPanDispDrvFn(13 项) |
| [multidisp.c](file:///d:/reactos/win32ss/gdi/eng/multidisp.c) | MultiEnableDriver(LDEV_DEVICE_META 入口,桩) |
| [engmisc.c](file:///d:/reactos/win32ss/gdi/eng/engmisc.c) | IntEngEnter/IntEngLeave、EngGetProcessHandle、EngGetCurrentCodePage、EngQuerySystemAttribute、EngGetTickCount |
| [pdevobj.c](file:///d:/reactos/win32ss/gdi/eng/pdevobj.c#L511-L575) | 平移 vtable 替换逻辑、META 驱动加载 |
| [mdevobj.c](file:///d:/reactos/win32ss/gdi/eng/mdevobj.c) / mdevobj.h | MDEVOBJ 多显示器映射对象 |
| [ntgdi/misc.h](file:///d:/reactos/win32ss/gdi/ntgdi/misc.h#L14-L21) | IntEngEnter/IntEngLeave 声明 |
关联模块:
- 底层回调:
EngBitBlt/EngAlphaBlend/EngGradientFill/EngCopyBits(《分析_4》3.1) - 加载入口:
LDEVOBJ_pLoadInternal(《分析_4》3.4) - 设备对象:PDEVOBJ(ppdev->apfn 被替换)
- 光标联动:
PanMovePointer与《分析_24》鼠标管理的交互(FIXME 提及 MouseSafety 误调)
10. 各函数作用和使用
10.1 pandisp.c — 平移显示驱动(DDI 函数表 gPanDispDrvFn)
| 函数 | 作用 | 使用方式/要点 |
|---|---|---|
PanEnablePDEV | 启用平移设备:以dmPanningW×H启用底层 PDEV,上报桌面尺寸dmPelsW×H | 由引擎经PDEVOBJ_bEnablePDEV调用;内部回调底层EnablePDEV并保存其apfn表;置GCAPS_PANNING/GCAPS2_SYNCFLUSH|SYNCTIMER。失败返回 NULL(已释放 PANDEV) |
PanCompletePDEV | 设备就绪通知 | 透传底层CompletePDEV;保存hdev |
PanDisablePDEV | 卸载设备 | 回调底层DisablePDEV后EngFreeMem释放 PANDEV |
PanEnableSurface | 启用表面:建设备表面(桌面尺寸)+影子表面+ 挂钩 HOOK_* | 引擎在启用显示后调用;失败路径会自行调用PanDisableSurface清理(故后者须容错未初始化状态) |
PanDisableSurface | 逆序清理所有表面/影子/底层表面 | 各步判空,可被PanEnableSurface失败路径复用 |
PanSynchronize | 把影子表面视口区域EngCopyBits到物理屏幕 | 注册为DrvSynchronize;当前由各绘制函数手动调用(FIXME:应定时同步) |
PanBitBlt | 位块传输:设备表面→影子表面,绘制后同步 | 引擎绘制 DDI 回调;源/目标若是设备表面则替换为影子表面,再调EngBitBlt,成功后PanSynchronize |
PanCopyBits | 表面拷贝 | 简化为PanBitBlt(..., ROP4_SRCCOPY)(DrvCopyBits回调) |
PanStrokePath | 路径描边 | UNIMPLEMENTED(ASSERT(FALSE),当前不可用) |
PanTextOut | 文本输出 | UNIMPLEMENTED(ASSERT(FALSE),当前不可用) |
PanMovePointer | 光标移动 +视口漫游 | DrvMovePointer回调;x<0(隐藏)或y>=0(平移通知)直接忽略;光标触边则滚动rclViewport并PanSynchronize;底层支持GCAPS_PANNING时先透传光标 |
PanAlphaBlend | Alpha 混合到影子表面后同步 | 引擎回调;调EngAlphaBlend(psoShadow, ...)后PanSynchronize |
PanGradientFill | 渐变填充到影子表面后同步 | 引擎回调;调EngGradientFill(psoShadow, ...)后PanSynchronize |
共用说明:GETPFN(name)宏(pandev->apfn[INDEX_Drv##name])用于回调底层驱动同名 DDI——所有 Pan* 函数都是"底层驱动函数的包装层"。
10.2 multidisp.c — 多显示器驱动入口
| 函数 | 作用 | 使用方式/要点 |
|---|---|---|
MultiEnableDriver | 多显示器(META 设备)驱动加载入口 | UNIMPLEMENTED 桩,恒返回 FALSE。仅当LDEV_DEVICE_META时由LDEVOBJ_pLoadInternal引用;实现前 META 设备路径不可用 |
10.3 engmisc.c — 引擎杂项
| 函数 | 作用 | 使用方式/要点 |
|---|---|---|
IntEngEnter | 设备表面"进入":把不可直接读写的设备表面换成临时内存位图,供引擎软件绘制 | 内部函数([misc.h](file:///d:/reactos/win32ss/gdi/ntgdi/misc.h#L14-L21) 声明);bitblt/alphablend/stretchblt/lineto/transblt/gradient 等绘制前成对调用。前置条件:设备表面须挂钩HOOK_COPYBITS,否则返回 FALSE;输出Translate用于坐标平移;成功后若挂钩HOOK_SYNCHRONIZE会先同步 |
IntEngLeave | 设备表面"离开":把临时位图结果经DrvCopyBits拷回设备表面并释放临时资源 | 必须与IntEngEnter严格成对(ReadOnly为 TRUE 时不做回拷);漏调会泄漏位图/裁剪对象 |
EngGetProcessHandle | 获取进程句柄 | 恒返回 NULL(Win2K 起废弃,驱动勿依赖) |
EngGetCurrentCodePage | 查询 OEM/ANSI 代码页 | DDI 服务(win32k 导出);转发RtlGetDefaultCodePage |
EngQuerySystemAttribute | 查询系统属性 | 驱动用CapNum=EngNumberOfProcessors取 CPU 数、EngProcessorFeature取处理器特性位;其余枚举返回 FALSE |
EngGetTickCount | 获取系统 tick(毫秒) | KeQueryTickCount+KiTickCountToMs;驱动计时用 |
本文档基于 ReactOS 源代码
win32ss/gdi/eng/multidisp.c、pandisp.c、engmisc.c及关联模块分析(2026 年 8 月)