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

 

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