Windows远程桌面开发之九-虚拟显示器(Windows 10 Indirect Display 虚拟显示器驱动开发)

                                                                                   by fanxiushu 2019-06-24 转载或引用请注明原始作者。


这里与远程桌面关系不是太大,但这个部分是xdisp_virt远程控制程序的实现多显示器桌面扩展的子功能,因此也归为远程桌面开发一类。
这篇文章与之前发布的
https://blog.csdn.net/fanxiushu/article/details/82731673 
WIN7以上系统WDDM虚拟显卡开发(WDDM Filter/Hook Driver 显卡过滤驱动开发之一)
联系比较紧密,同样是为了实现虚拟显示器,扩展桌面,WDDM HOOK采用windows类似黑客的hook办法。
由于没有一个通用和统一的接口。WDDM版本也多,显卡的种类也多。
要保证在大部分显卡上正常工作,几乎得去各类显卡上测试一遍,才能保证WDDM HOOK尽可能的正常使用。
由于我没这么多显卡来测试,也懒得去做测试,因此也就只匹配测试了自己使用的WIN7电脑的显卡。
而到了Windows 10 1607版本之后,微软提供了Indirect Display Driver的模型来实现虚拟显示器的功能,
这样也就用不着再在WDDM HOOK上打主意了。这也是本文重点讲述的功能。

可是Windows 10 1607之前的版本要实现虚拟显示器,该怎么办呢?
除了 WDDM HOOK难道就没别的办法了吗?
就像“WIN7以上系统WDDM虚拟显卡开发”文章中所说的那样:一开始想到的是开发一个虚拟显卡驱动。
可是问题又来了,微软并没像其他虚拟驱动那样提供了专门的虚拟总线来模拟某类虚拟驱动,比如虚拟磁盘驱动,虚拟网卡驱动等。
虚拟显卡驱动也需要底层的虚拟总线驱动的支持,通常真实的显卡是安装到PCI-E总线上的,PCI总线是支持DMA传输和硬件中断的。
要模拟一个总线驱动不难,难的是如何模拟DMA传输和PCI硬件中断,到目前为止,我并没找到一个办法来模拟这两样东西,
因此也只能把开发虚拟显卡驱动搁置到一边。
再说,即使开发了虚拟显卡驱动,对3D加速的的支持也是个难题,当然可以不去考虑3D,这对一般办公使用没什么问题。
可是对游戏或者对图形性能要求高的场所,也是个麻烦。
如果去考虑支持3D加速,那就得实现对应的应用层虚拟显卡驱动的几百个3D渲染函数,这种开发量也是非常恐怖的。
这样模拟出来的虚拟显卡也能达到像vmware里边的Vmware 3D SVGA 那样的3D加速虚拟显卡的级别,
但是对某些重度依赖显卡的游戏还是没辙。

这么看来,在WIN10 1607以下的版本,虚拟出新的显示器,而且要完全利用显卡的性能,除了WDDM HOOK,实在也找不到好办法了。
但是有没有在不需要显卡加速性能,也不需要开发虚拟显卡的情况下,能不能模拟虚拟显示器呢?
答案是:肯定有的!而且还是现成的,通过简单设置就可以。
这也是我多次折腾来折腾去,突然在网上看到的一种办法,在WIN7,WIN8,WIN10 1607之前的版本都有效。
具体做法就是,打开控制面板->显示->屏幕分辨率,“更改显示器外观”,选择“检测”,就会出现“未检测到其他显示器”,选中之后,
下面“多显示器”一栏,会出现“依然尝试在以下对象上进行连接:VGA”,选择它,就这样就建立了一个新的虚拟显示器,
当然这个新的显示器是不支持3D加速的。这个办法估计太少人使用,所以我一直都不知道它的存在。
可以看下图WIN7系统的解释:


如上图,2号显示器,就是通过这种办法虚拟出来的,左边的在xdisp_virt程序中已经识别出来ExtScreen#1 这个2号显示器。
当然不光是xdisp_virt,任何支持多显示器的远程控制程序,都能访问这个2号显示器,比如teamviewer远程控制程序。

不过到了 WIN10 1607之后,控制面板中的”显示“已经不存在了,再也找不到这种办法来增加虚拟显示器了。
这应该是增加了Indirect Display 之后,有了更添加更高效的虚拟显示器的解决方案了。

这样在WIN10 1607之后使用Indirect Display添加虚拟显示器,
WIN10 1607以前的系统(包括WIN7,WIN8,WIN10 1511)采用windows自带的添加虚拟显示器的功能,
(虽然这种不支持3D,但一般情况足够使用)
这就完善了整个windows平台添加虚拟显示器的解决办法了。
这也是前段时间急着更新xdisp_virt远程控制程序,增加支持多显示器这个功能的直接动力。

现在回到正题:Indirect Display开发。
Indirect Display的资料相当的少。
MSDN文档介绍也少的可怜,下面链接介绍了这个模型的大致概貌。
https://docs.microsoft.com/en-us/windows-hardware/drivers/display/indirect-display-driver-model-overview
下面链接介绍了开发Indirect Display大致流程
https://docs.microsoft.com/en-us/windows-hardware/drivers/display/iddcx-objects
除此之外,MSDN上找不到其他资料,还有就是MSDN上关于Indirect Display的每个接口函数功能的阐述,阐述的及其简单,
相当于是阐述了等于没阐述一样。
本来还打算找微软提供的WDK的sample例子,看看里边是否包含 Indirect Display实例代码,找了半天,也是没找到。
现在除了GITHUB 上
https://github.com/kfroe/IndirectDisplay
提供的例子代码,好像还真找不到其他例子,看看这个例子的作者,属于微软的员工。
我想,除了微软的员工外,如果在没有例子代码,接口函数描述得也极其简单,流程也描述的极其简单的情况下,
其他人是很难开发出Indirect Display Driver驱动的。

现在就以GITHUB上提供的例子代码,MSDN上简单的描述,以及我对显卡驱动本身的理解,
来描述Indirect Display驱动的开发,以及开发出我们自己的Indirect Display驱动。

在 “WIN7以上系统WDDM虚拟显卡开发” 一文中,阐述了 WDDM HOOK方式来模拟虚拟显示器。
在文中,讲了HOOK哪些回调函数来增加一个虚拟显示器,文章中还提到Indirect Display 驱动属于 UMDF驱动,
微软在内核专门提供了 IndirectKmd.sys 驱动来挂钩到每个显卡驱动,
就相当于给每个显卡驱动增加了真正的过滤驱动来完成类似我们在WDDM HOOK中实现的功能。
也就是微软在IndirectKmd.sys内核中实现通用的拦截处理功能,然后提供 应用层的Indirect Display框架,留给我们实现具体的虚拟显示器。
这就是 Indirect Display Driver 内部实现的核心原理。
核心部分原理跟 WDDM HOOK 差不多,只是人家的是正宗的过滤驱动,而且兼容考虑了各种不同显卡等问题。

现在重点描述这个应用层的UMDF 的 Indirect Display 框架。
虽然Indirect Display 本质上是附着在真正的显卡上的,但是在windows中,任然把他当成一个独立的显卡驱动来看待,
也就是MSDN上 https://docs.microsoft.com/en-us/windows-hardware/drivers/display/iddcx-objects
所描述的那样,每个 Indirect Display 都必须有一个唯一的 IDDCX_ADAPTER对象,用于描述这个显卡 adapter,
而IDDCX_ADAPTER 可以包含 0个或者多个 IDDCX_MONITOR 对象。
这个IDDCX_MONITOR对象就是用来描述这个Indirect Display可以模拟出多少个 显示器。
每个 IDDCX_MONITOR对象 都会包含 一个 IDDCX_SWAPCHAIN对象,
这个IDDCX_SWAPCHAIN对象就是用来从每个Monitor截取桌面图像数据使用的。
这么看下来,逻辑是非常清晰的。
首先我们在UMDF初始化的时候,配置相关参数,然后创建 IDDCX_ADAPTER对象,
然后就是根据我们的需求,模拟”插入“ 虚拟显示器。
每插入一个虚拟显示器,就等于是创建 一个IDDCX_MONITOR对象,
然后”拔掉“这个虚拟显示器,等于是销毁 IDDCX_MONITOR对象。
要从每个虚拟显示器获取到当前桌面的图像数据,
只要获取到IDDCX_MONITOR对象的 IDDCX_SWAPCHAIN对象,就能进一步获取到图像数据了。
这就是一个 Indirect  Display的开发流程。

我们再从 UMDF 开发流程来说,
在 UMDF的 DriverEntry入口函数中,如下初始化(伪代码):
extern "C" NTSTATUS DriverEntry(
    PDRIVER_OBJECT  pDriverObject,
    PUNICODE_STRING pRegistryPath)
{
    NTSTATUS status = 0;
    WDF_DRIVER_CONFIG Config;

    DPT("--DriverEntry [%wZ]\n", pRegistryPath);

    WDF_OBJECT_ATTRIBUTES Attributes;
    WDF_OBJECT_ATTRIBUTES_INIT(&Attributes);
   
    WDF_DRIVER_CONFIG_INIT(&Config,IndirectDisplayDeviceAdd);
   
    status = WdfDriverCreate(pDriverObject, pRegistryPath, &Attributes, &Config, WDF_NO_HANDLE);
    if (!NT_SUCCESS(status))
    {
        DPT("***WdfDriverCreate err=0x%X\n", status );
        return status;
    }
    DPT("--- Indirect Driver DriverEntry.\n");
    return status;
}

其中 IndirectDisplayDeviceAdd 函数相当于是内核驱动的 AddDevice 入口函数,
接着我们在此函数中,初始化Indirect Display 配置参数。如下代码
NTSTATUS IndirectDisplayDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT pDeviceInit)
{
    NTSTATUS status = STATUS_SUCCESS;
    WDF_PNPPOWER_EVENT_CALLBACKS PnpPowerCallbacks;

    //加电,等同于 IRP_MN_START_DEVICE设备启动,
    WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&PnpPowerCallbacks);
    PnpPowerCallbacks.EvtDeviceD0Entry = IndirectDeviceD0Entry;
    WdfDeviceInitSetPnpPowerEventCallbacks(pDeviceInit, &PnpPowerCallbacks);

    /////
    IDD_CX_CLIENT_CONFIG IddConfig;
    IDD_CX_CLIENT_CONFIG_INIT(&IddConfig);
    设置各种回调函数
    ///用于与我们的接口程序通讯
    IddConfig.EvtIddCxDeviceIoControl = IndirectDeviceIoControl;

    /// 调用 IddCxAdapterInitAsync 成功完成之后此函数被调用
    IddConfig.EvtIddCxAdapterInitFinished = IndirectAdapterInitFinished;

    /// 设置显示器的显示模式
    IddConfig.EvtIddCxParseMonitorDescription = IndirectParseMonitorDescription;
    IddConfig.EvtIddCxMonitorGetDefaultDescriptionModes = IndirectMonitorGetDefaultModes;
    IddConfig.EvtIddCxMonitorQueryTargetModes = IndirectMonitorQueryModes;
    IddConfig.EvtIddCxAdapterCommitModes = IndirectAdapterCommitModes;

    /// 渲染,也就是获取到虚拟显示器的桌面图像数据
    IddConfig.EvtIddCxMonitorAssignSwapChain = IndirectMonitorAssignSwapChain;
    IddConfig.EvtIddCxMonitorUnassignSwapChain = IndirectMonitorUnassignSwapChain;
     初始化Indirect Display 参数
    status = IddCxDeviceInitConfig(pDeviceInit, &IddConfig);
    if (!NT_SUCCESS(status)) {
        DPT("*** IddCxDeviceInitConfig err=0x%X\n", status );
        return status;
    }

    WDF_OBJECT_ATTRIBUTES Attr;
    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&Attr, AdapterContext);
    Attr.EvtCleanupCallback = IndirectDeviceCleanup; //设备关闭清除

    // Create Device
    WDFDEVICE device = NULL;
    status = WdfDeviceCreate(&pDeviceInit, &Attr, &device);
    if (!NT_SUCCESS(status)) {
        DPT("*** WdfDeviceCreate err=0x%X\n", status );
        return status;
    }

    /////把 WDFDEVICE设备与 Indirect Display关联起来。
    status = IddCxDeviceInitialize(device);
    if (!NT_SUCCESS(status)) {
        DPT("*** IddCxDeviceInitialize err=0x%X\n", status );
    }

    AdapterContext* ctx = getAdapterContext(device);
    RtlZeroMemory(ctx, sizeof(AdapterContext));
   
    /////
    return status;
}
然后就是在 设备启动回调函数中 IndirectDeviceD0Entry 创建 IDDCX_ADAPTER对象。
这个时候需要使用到 IddCxAdapterInitAsync  函数来创建。如下伪代码:
//// Indirect Display 设备启动,
static NTSTATUS IndirectDeviceD0Entry(WDFDEVICE Device, WDF_POWER_DEVICE_STATE PreviousState)
{
    NTSTATUS status = STATUS_SUCCESS;
    AdapterContext* ctx = getAdapterContext(Device);
    DPT("--- IndirectDeviceD0Entry\n");
    。。。。。设置相关参数
     IDARG_OUT_ADAPTER_INIT AdapterInitOut;
    status = IddCxAdapterInitAsync(&AdapterInit, &AdapterInitOut);
    if(NT_SUCCESS(status)){//  成功
    }
    else{ // 失败
    }
    .........
}

这里为了让 IddCxAdapterInitAsync 成功,我们必须再开发一个内核态的总线枚举驱动,下面会再次提到。

这样我们的初始化就完成了,安装之后,就会在系统中看到我们的“显卡”设备。
接着,我们可以利用 EvtIddCxDeviceIoControl  这个IOCTL回调函数,
接收从我们的应用层控制程序发来的插入或者移除虚拟显示器的命令。
从而创建或者删除 IDDCX_MONITOR对象来达到我们控制虚拟显示器的目的。
我们使用 IddCxMonitorCreate 来创建 IDDCX_MONITOR对象,创建成功之后,还得调用 IddCxMonitorArrival 告诉系统插入这个显示器。
如果这两个函数都成功后,WIN10 系统的显示器设置里就能检测到这个虚拟出来的显示器了。

之后,如果要”拔掉“这个虚拟显示器,调用 IddCxMonitorDeparture 函数。

虚拟显示器插入成功之后,首先要做的就是分配显示模式,就是分辨率,刷新率等这些参数。
也就是要处理上面回调函数中关于显示模式的四个回调函数。至于具体处理,这里也就不再赘述了,
无非就是配置相关分辨率啊,刷新率啊等等。

然后就是如何截取每个虚拟显示器的桌面图像数据。
每当虚拟显示器创建成功之后, EvtIddCxMonitorAssignSwapChain  回调函数就会被调用,这个用于分配一个IDDCX_SWAPCHAIN对象。
这个函数包含 IDARG_IN_SETSWAPCHAIN 参数,这个参数里边就包含 IDDCX_SWAPCHAIN 对象以及其他一些相关参数。
我们可以在 EvtIddCxMonitorAssignSwapChain   回调函数中创建一个线程
(当然在 EvtIddCxMonitorUnassignSwapChain  回调函数中结束这个线程就可以了)
在这个线程中,循环检测,截取虚拟显示器都得桌面图像数据,类似如下代码
void monitor_t::thread_func()
{
    HRESULT hr = init_render(); ///初始化渲染对象,其实主要是初始化 DirectX相关
    if (FAILED(hr)) {
        DPT("*** init_render err=0x%X\n", hr );
        ////
        deinit_render();
        WdfObjectDelete(this->hSwapChain);
        this->hSwapChain = NULL;
        return;
    }

    HANDLE wait[2];
    wait[0] = this->hNextSurfaceAvailable;
    wait[1] = this->h_evt_exit;
    ///
    while (TRUE) {
        //////
        IDARG_OUT_RELEASEANDACQUIREBUFFER Buffer = {};
        hr = IddCxSwapChainReleaseAndAcquireBuffer( hSwapChain, &Buffer);

        if (hr == E_PENDING) { //// wait for data
            ////
            DWORD ret = WaitForMultipleObjects(2, wait, FALSE, 16 );
            if ( ret == WAIT_OBJECT_0 || ret == WAIT_TIMEOUT) {
                ////
                continue;
            }
            else if (ret == WAIT_OBJECT_0 + 1) {// exit
                break;
            }
            else {
                DPT("** WaitForMultipleObjects err=%d\n", ret );
                break;
            }
            /////
        }

        //////
        if (FAILED(hr)) {
            DPT("** IddCxSwapChainReleaseAndAcquireBuffer hr=0x%X\n", hr );
            break;
        }

        //////获取图像数据
        if (this->is_capture_image) {
            ///
            this->capture(&Buffer.MetaData);
        }

        ////完成
        hr = IddCxSwapChainFinishedProcessingFrame(hSwapChain);
        if (FAILED(hr)) {
            DPT("*** IddCxSwapChainFinishedProcessingFrame hr=0x%X\n", hr );
            break;
        }
        //////
    }

    ////
    deinit_render();

    ///销毁 SwapChain,让系统立即再次发起EvtIddCxMonitorAssignSwapChain    回调。
    WdfObjectDelete(hSwapChain);
    hSwapChain = NULL;

    return ;
}

如上代码中,成功截取到桌面图像后,保存到IDARG_OUT_RELEASEANDACQUIREBUFFER 参数中,此参数唯一包含
IDDCX_METADATA 结构,IDDCX_METADATA 结构包含的参数比较多,
如果认真看看里边的成员,就会发现,这个与 DXGI Duplication Desktop 截屏非常的像。
里边的 IDXGIResource 参数 pSurface 指向的就是 包含桌面图像数据的表面。
因此这里,我们按照 DXGI那套截屏代码,进行处理,就能成功截取到真正的图像数据了。
因此这里也不再赘述。有兴趣可查看我以前的关于DXGI截屏的文章,或者DXGI截屏代码。

Indirect Display 驱动就这样开发完成了。
但是,当你开发完成这个驱动,不存在任何其他真实设备的情况下,去安装到系统中。
就是直接在设备管理器中“添加过时硬件”来安装,结果驱动是会安装成功的,
但是 IddCxAdapterInitAsync 函数这种情况下总是会返回  STATUS_NOT_SUPPORTED (0xC00000BB)错误。
要解决这个问题,我们要么安装到真实的硬件设备上,但是这里实现的虚拟显示器驱动,是不需要任何硬件的。
其实我们可以在内核中模拟出一个PDO物理设备对象来,把Indirect Display 挂载到这个PDO上即可,
也能保证 IddCxAdapterInitAsync 函数调用成功。
具体做法,就是实现一个内核级别的虚拟总线驱动,模拟出一个PDO物理设备对象。然后Indirect Display驱动安装到这个PDO设备上即可。
至于虚拟总线驱动,可以参阅 微软的例子代码 toaster例子里边的bus总线驱动。

至于 IddCxAdapterInitAsync 为何会出现这么奇怪的问题。
估计是UMDF驱动,毕竟是应用层驱动,并不能真正模拟出内核PDO物理设备对象来。
而且也可能微软设计开发这个Indirect Display框架的时候,估计也没去考虑这种情况下模拟出一个特殊PDO出来。

下图是安装这个Indirect Display在我电脑上的效果图:

如上图中 2 和 3, 这个是在设备管理器中安装 Indirect Display的效果图,2是安装的虚拟总线驱动,
3是从这个虚拟总线驱动模拟的PDO上安装的Indirect Display驱动。
1和4 是这个Indirect Display一共模拟了 5个虚拟显示器, 1号显示器是真实的显示器,左面显示的是xdisp_virt一共发现了 6个显示器。
看起来挺热闹的,模拟出这么多的虚拟显示器,真实显卡需要有强大的性能,否则系统会慢的出奇。

下图可能看起来可能比较费劲:,大致解释一下:
大屏幕是台式机显示屏,笔记本是WIN10,利用Indirect Display扩展成两个显示屏,笔记本的显示器设置成扩展桌面,
大屏幕中使用xdisp_virt的网页链接的是笔记本的主桌面,并且正在这个主桌面中,
也就是Indirect Display生成的虚拟显示器桌面中玩 极品飞车17 这个游戏。
笔记本显示器上还有一个窗口,也显示跟大屏幕上的游戏同样的画面,这个是从Indirect Display驱动直接截屏的数据显示到窗口中。
因为玩的游戏是2560X1600分辨率,而笔记本电脑性能也不是很强,因此游戏不是很顺畅。
但是可以肯定,Indirect Display完全支持跟真实显卡一样的3D加速(因为它本身就是附着在真实显卡上的),
否则的话,这个严重依赖显卡的游戏也没法启动了。


如果需要自己下载试玩,请关注 GITHUB上的 xdisp_virt项目:
https://github.com/fanxiushu/xdisp_virt

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章