1. 背景概要
最近在分析处理一个屏幕采集的问题:
ROG16 笔记本带有集成显卡和独立显卡两个显卡, 在外接一个4K显示器并设置为主显示器时, 使用 DXGI 方式采集笔记本内置屏幕图像, 实际得到的却是外接屏幕的图像.
该问题的根源是:
外接显示器是有独立显卡驱动显示, 笔记本内置显示器是内置显卡驱动显示.
对应的实际问题代码是:
// 应用代码
hr = ::D3D11CreateDevice(NULL, DriverTypes[DriverTypeIndex], NULL, 0, FeatureLevels, NumFeatureLevels,
D3D11_SDK_VERSION, &_Device, &FeatureLevel, &_DeviceContext);
//接口函数
HRESULT WINAPI D3D11CreateDevice(
_In_opt_ IDXGIAdapter* pAdapter,
D3D_DRIVER_TYPE DriverType,
HMODULE Software,
UINT Flags,
_In_reads_opt_( FeatureLevels ) CONST D3D_FEATURE_LEVEL* pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
_COM_Outptr_opt_ ID3D11Device** ppDevice,
_Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel,
_COM_Outptr_opt_ ID3D11DeviceContext** ppImmediateContext );
D3D11CreateDevice
是 DirectX 11 中的函数,用于创建 Direct3D 11 设备和设备上下文。这个函数通常用于初始化和配置 Direct3D 11 渲染环境,以便应用程序可以进行图形渲染。
以下是函数参数的简要说明:
pAdapter
:一个指向IDXGIAdapter
接口的指针,表示用于创建 Direct3D 11 设备的适配器(显卡)。可以传入nullptr
,让 Direct3D 自动选择一个适配器。DriverType
:一个D3D_DRIVER_TYPE
枚举值,表示驱动类型,指定了使用的驱动类型,如硬件驱动、软件驱动等。Software
:一个HMODULE
,通常为nullptr
。如果DriverType
设置为D3D_DRIVER_TYPE_SOFTWARE
,则可以将此参数指定为用于软件驱动的 DLL 模块的句柄。Flags
:一组标志,用于配置设备的创建选项,例如调试模式等。pFeatureLevels
:一个数组,表示所需的 Direct3D 特性级别的列表。函数将选择最接近所需特性级别的设备。通常,你可以传入一个包含所需特性级别的数组。FeatureLevels
:pFeatureLevels
数组中特性级别的数量。SDKVersion
:使用的 SDK 版本。通常设置为D3D11_SDK_VERSION
。ppDevice
:返回一个指向创建的 Direct3D 11 设备的指针。pFeatureLevel
:返回所创建设备的特性级别。ppImmediateContext
:返回一个指向设备上下文的指针。设备上下文用于执行渲染命令。
函数将根据传入的参数创建一个 Direct3D 11 设备和设备上下文,并返回相关信息。这些设备和上下文将用于执行图形渲染操作,如顶点缓冲区设置、着色器程序加载、渲染目标设置等。
当第一个参数 pAdapter 传入为 NULL时, Direct3D 自动选择一个适配器,在ROG16 上测试,选择的就是独立显卡. 此时是无法获取笔记本内置屏幕图像.
2. IDXGIAdapter与显示器
2.1 IDXGIAdapter
IDXGIAdapter
(DirectX Graphics Infrastructure Adapter Interface)是 DirectX 中的一个接口,用于表示图形适配器(显卡)的信息和属性。这个接口允许应用程序与计算机上的不同图形适配器进行交互,并获取有关它们的详细信息,如制造商、型号、内存大小、显示输出等等。
IDXGIAdapter
接口通常用于以下用途:
- 枚举系统上的图形适配器:应用程序可以使用
IDXGIFactory
接口来获取系统上所有的适配器,并使用EnumAdapters
方法来枚举它们,获取IDXGIAdapter
对象。 - 获取适配器属性:通过
IDXGIAdapter
,应用程序可以获取适配器的属性信息,如适配器描述、供应商标识符、设备标识符、修订版本、内存大小等等。 - 获取适配器的输出(显示器):通过
IDXGIAdapter
,应用程序可以获取与适配器相关的所有输出,以便管理显示器配置。 - 选择渲染适配器:如果系统上有多个图形适配器,应用程序可以使用
IDXGIAdapter
来选择适合渲染的适配器。 - 与适配器相关的其他操作:
IDXGIAdapter
还提供了其他方法,以便与适配器相关的操作,如创建交换链、获取适配器输出、查询支持的显示模式等等。
查询Windows API可知, IDXGIFactory 的 EnumAdapters 函数可以枚举出当前设备的图形适配器。
virtual HRESULT STDMETHODCALLTYPE EnumAdapters(
/* [in] */ UINT Adapter,
/* [annotation][out] */
_COM_Outptr_ IDXGIAdapter **ppAdapter) = 0;
2.2 IDXGIFactory
IDXGIFactory
是 DirectX Graphics Infrastructure (DXGI) 中的一个接口,用于管理和创建 DXGI 对象,包括适配器、交换链和其他与图形硬件和显示相关的资源。DXGI 旨在提供一个抽象层,使应用程序能够与不同的图形硬件和显示设备进行交互,而无需关心底层硬件的详细信息。
IDXGIFactory
接口允许应用程序执行以下任务:
- 创建和管理适配器:使用
IDXGIFactory
,应用程序可以列举系统上的图形适配器,获取它们的属性和信息,选择要用于渲染的适配器,以及创建IDXGIAdapter
对象来访问适配器的详细信息。 - 创建交换链:交换链(Swap Chain)是用于双缓冲渲染的关键对象,它允许应用程序在前后缓冲之间切换,以实现平滑的帧更新。
IDXGIFactory
允许应用程序创建和管理交换链。 - 创建 DXGI 设备:
IDXGIFactory
可以用于创建 DXGI 设备,这是与图形硬件通信的主要接口。 - 枚举支持的显示模式:
IDXGIFactory
允许应用程序枚举支持的显示模式,如分辨率、刷新率和颜色位深度。 - 管理多个显示器:
IDXGIFactory
支持多显示器环境,应用程序可以使用它来获取多个适配器和显示器的信息,以支持多显示器应用程序。 - 支持 DXGI 1.2+ 特性:从 DXGI 1.2 版本开始,
IDXGIFactory
提供了额外的功能,如创建 DXGI 1.2+ 设备、获取输出信息等。
IDXGIFactory的创建过程如下:
HRESULT hr = S_OK;
IDXGIFactory* pFactory = nullptr;
hr = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)(&pFactory));
if (FAILED(hr)) {
spdlog::error("CreateDXGIFactory failed");
return;
}
// 使用结束后需要释放IDXGIFactory
pFactory->Release();
如果使用DXGI1.2新特性, 则使用 IDXGIFactory7 来创建。
2.3 枚举IDXGIAdapter 及其显示器信息
创建IDXGIFactory 后即可枚举当前电脑的 IDXGIAdapter :
IDXGIAdapter* pAdapter = nullptr;
for (UINT adapterIndex = 0; pFactory->EnumAdapters(adapterIndex, &pAdapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex)
{
DXGI_ADAPTER_DESC adapterDesc;
pAdapter->GetDesc(&adapterDesc);
spdlog::info("Adapter Index:{0}, Description:{1}, DeviceId:{2}, VendorId:{3}, SubSysId:{4}, Revision:{5}, AdapterLuid(H-L):{6}-{7} ",
adapterIndex, Wchar2String(adapterDesc.Description), adapterDesc.DeviceId, adapterDesc.VendorId, adapterDesc.SubSysId,
adapterDesc.Revision, adapterDesc.AdapterLuid.HighPart, adapterDesc.AdapterLuid.LowPart);
// print adapter output info
IDXGIOutput* pOutput;
int outputCount = 0;
for (UINT j = 0; pAdapter->EnumOutputs(j, &pOutput) != DXGI_ERROR_NOT_FOUND; ++j) {
DXGI_OUTPUT_DESC outputDesc;
pOutput->GetDesc(&outputDesc);
spdlog::info("Adapter Output Index:{0}, DeviceName:{1}", j, Wchar2String(outputDesc.DeviceName));
}
}
这里使用了DXGI_ADAPTER_DESC 来获取 IDXGIAdapter 的相关信息。
DXGI_ADAPTER_DESC
结构是用于描述 DXGI 适配器(图形适配器)的一种结构,它包含了适配器的各种属性和信息。以下是 DXGI_ADAPTER_DESC
结构中的一些主要属性和它们的含义:
Description
(描述):一个包含适配器描述信息的字符串。这通常包括了适配器的名称和制造商信息。VendorId
(制造商标识符):一个整数值,表示适配器的制造商。这是一个供应商标识号,通常用于识别制造商,例如NVIDIA、AMD、Intel等。每个制造商都有一个唯一的标识符。DeviceId
(设备标识符):一个整数值,表示适配器的设备标识符。这通常用于标识具体的硬件设备型号,如某个特定的GPU型号。SubSysId
(子系统标识符):一个整数值,表示适配器的子系统标识符。这通常用于进一步区分同一型号的适配器,以区分不同的子系统配置。Revision
(适配器修订号):一个整数值,表示适配器的修订版本。这可以用于区分不同修订版本的适配器。DedicatedVideoMemory
(独立视频内存):一个整数值,表示适配器拥有的专用视频内存的大小(以字节为单位)。DedicatedSystemMemory
(独立系统内存):一个整数值,表示适配器拥有的专用系统内存的大小(以字节为单位)。SharedSystemMemory
(共享系统内存):一个整数值,表示适配器可以共享的系统内存的大小(以字节为单位)。AdapterLuid
(本地唯一标识符):一个包含适配器的本地唯一标识符(LUID)的结构,用于唯一标识适配器。
获取到 IDXGIAdapter 后使用其 EnumOutputs 函数枚举出 IDXGIOutput:
virtual HRESULT STDMETHODCALLTYPE EnumOutputs(
/* [in] */ UINT Output,
/* [annotation][out][in] */
_COM_Outptr_ IDXGIOutput **ppOutput) = 0;
IDXGIOutput
用于表示与显示器输出相关的对象。它允许应用程序查询和获取与特定显示器输出相关的信息,如显示模式、像素格式、物理位置等。
一些主要的功能和用途包括:
- 查询显示模式 :通过
IDXGIOutput
接口,你可以获取支持的显示模式列表,包括分辨率、刷新率等信息。这对于选择合适的显示模式以及配置游戏或图形应用程序的渲染很有用。 - 获取显示器属性 :你可以获取有关显示器输出的属性,例如显示器的物理位置、大小、像素格式和 DPI(每英寸像素数)等。
- 更改显示模式:你可以使用
IDXGIOutput
接口来更改显示模式,例如切换分辨率或刷新率。这对于游戏和图形应用程序的全屏模式切换非常有用。 - 查询支持的像素格式:你可以获取支持的像素格式列表,以确定可以在特定显示器上渲染的像素格式。
最后使用 IDXGIOutput 的 GetDesc 函数获取其描述信息。
DXGI_OUTPUT_DESC
结构是用于描述 DXGI 输出的结构,包含了有关输出的信息。这些信息包括:
DeviceName
:一个字符串,表示输出设备的名称。DesktopCoordinates
:一个RECT
结构,表示输出的桌面坐标。这个坐标表示输出在整个桌面中的位置和大小。AttachedToDesktop
:一个布尔值,指示输出是否连接到桌面。如果为TRUE
,则表示输出是实际显示在桌面上的;如果为FALSE
,则表示输出未连接到桌面。Rotation
:一个DXGI_MODE_ROTATION
枚举值,表示输出的旋转状态。可以是DXGI_MODE_ROTATION_IDENTITY
(无旋转)、DXGI_MODE_ROTATION_ROTATE90
(顺时针旋转 90 度)等。Monitor
:一个HMONITOR
句柄,表示与输出关联的显示器。
3. HMONITOR
在上一步最后 我们获取到的 DXGI_OUTPUT_DESC 结构里包含了 HMONITOR, 而 HMONITOR 就是 Windows设备管理 API里面显示器的句柄,可在多处使用。它通常用于与显示器相关的操作,例如获取显示器的特性和配置信息。虽然 HMONITOR
是一个句柄,但它实际上是一个指向 MONITORINFOEX
结构的指针。
MONITORINFOEX
结构包含了有关显示器的各种信息,包括但不限于以下字段:
cbSize
:结构的大小,用于版本控制。在使用MONITORINFOEX
结构时,需要设置cbSize
为结构的大小。rcMonitor
:一个RECT
结构,表示整个显示器的位置和大小。rcWork
:一个RECT
结构,表示可用工作区的位置和大小。工作区通常是排除了任务栏和其他系统元素的显示区域。dwFlags
:一些标志,用于指示结构中哪些字段有效。szDevice
:一个字符串,表示显示器的设备名称。
除了上述字段之外,HMONITOR
对象还可以用于调用一些函数来获取更多关于显示器的信息,例如:
- 使用
GetMonitorInfo
函数来获取MONITORINFOEX
结构的信息。 - 使用
EnumDisplaySettings
函数来获取显示器的分辨率和刷新率信息。 - 使用
GetDeviceCaps
函数来获取显示器的一些设备属性。
修改上述代码并输出HMONITOR 相关信息, 完整代码如下:
HRESULT hr = S_OK;
IDXGIFactory* pFactory = nullptr;
hr = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)(&pFactory));
if (FAILED(hr)) {
spdlog::error("CreateDXGIFactory failed");
return;
}
IDXGIAdapter* pAdapter = nullptr;
for (UINT adapterIndex = 0; pFactory->EnumAdapters(adapterIndex, &pAdapter) != DXGI_ERROR_NOT_FOUND; ++adapterIndex)
{
DXGI_ADAPTER_DESC adapterDesc;
pAdapter->GetDesc(&adapterDesc);
spdlog::info("Adapter Index:{0}, Description:{1}, DeviceId:{2}, VendorId:{3}, SubSysId:{4}, Revision:{5}, AdapterLuid(H-L):{6}-{7} ",
adapterIndex, Wchar2String(adapterDesc.Description), adapterDesc.DeviceId, adapterDesc.VendorId, adapterDesc.SubSysId,
adapterDesc.Revision, adapterDesc.AdapterLuid.HighPart, adapterDesc.AdapterLuid.LowPart);
// print adapter output info
IDXGIOutput* pOutput;
int outputCount = 0;
for (UINT j = 0; pAdapter->EnumOutputs(j, &pOutput) != DXGI_ERROR_NOT_FOUND; ++j) {
DXGI_OUTPUT_DESC outputDesc;
pOutput->GetDesc(&outputDesc);
MONITORINFOEX monitorInfo;
monitorInfo.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(outputDesc.Monitor, &monitorInfo))
{
// 输出友好名称
spdlog::info("Adapter Output Index:{0}, DeviceName:{1}, szDevice:{2}, right:{3}, bottom:{4}",
j, Wchar2String(outputDesc.DeviceName), monitorInfo.szDevice, monitorInfo.rcMonitor.right, monitorInfo.rcMonitor.bottom);
}
}
}
pFactory->Release();
运行上述代码输出的结果如下:
[2023-10-30 18:59:17.788] [info] Adapter Index:0, Description:NVIDIA GeForce RTX 4060 Laptop GPU, DeviceId:10464, VendorId:4318, SubSysId:408752195, Revision:161, AdapterLuid(H-L):0-68515
[2023-10-30 18:59:17.788] [info] Adapter Output Index:0, DeviceName:\\.\DISPLAY5
[2023-10-30 18:59:17.788] [info] Adapter Index:1, Description:Intel(R) Iris(R) Xe Graphics, DeviceId:42912, VendorId:32902, SubSysId:408752195, Revision:4, AdapterLuid(H-L):0-67183
[2023-10-30 18:59:17.788] [info] Adapter Output Index:0, DeviceName:\\.\DISPLAY1
[2023-10-30 18:59:17.789] [info] Adapter Index:2, Description:Intel(R) Iris(R) Xe Graphics, DeviceId:42912, VendorId:32902, SubSysId:408752195, Revision:4, AdapterLuid(H-L):0-104167
[2023-10-30 18:59:17.789] [info] Adapter Index:3, Description:Microsoft Basic Render Driver, DeviceId:140, VendorId:5140, SubSysId:0, Revision:0, AdapterLuid(H-L):0-68375
可以看出电脑上的两个显卡:
- 独显: NVIDIA GeForce RTX 4060 Laptop GPU
- 核显: Intel(R) Iris(R) Xe Graphics
最后一个是安装的远程工具向日葵虚拟的一个显卡。
4. 总结
上述为本次解决屏幕采集问题所学的一些关于 DXGI 的知识,后续会持续更新一些在解决问题学习到的知识点。如果感兴趣请关注我的账号:DevWiki,关注后可获取群信息进群一起学习交流。
原文链接为我的博客,点击可以查看历史博文~