项目地址在:https://github.com/transient-fool/CaptureHost.git
最近在做上位机的时候,碰到客户提出这么一个要求,就是说我们的软件全屏展示之后,还需要一个窗口来展示另外一个软件的界面。因为我们的软件是由很多的窗口出现的,利用的是C# 的winForm里面的去统一管理不同的控件,也就是说所有的控件最终都是嵌入到Panel中,这个小视频窗口也是。询问ai给了集中方式,一开始是采用了简单的win32原生的api:PrintWindow 和 BitBlt。
cs
[DllImport("user32.dll")]
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, int nFlags);
[DllImport("gdi32.dll")]
private static extern bool BitBlt(IntPtr hdcDest, int xDest, int yDest, int width, int height,IntPtr hdcSrc, int xSrc, int ySrc, int rop);
但是这两者捕获的软件窗口有个很大的问题,就是一些用gpu渲染的窗口没法捕获,比如像kiro这种现代的桌面端,走的是gpu合成路线,这个时候用这两种方式捕获指挥展示黑屏。
可以看到右侧捕获的vs的界面没有问题,但是左侧的Kiro无法捕获,cursor也是同样的道理。可见这种方式有很大的局限性。后来又提供了一种新的方法,就是win10以上电脑版本提供的一种新的api;Windows.Graphics.Capture。可以捕获gpu合成的软件界面。但是由于我的.NET版本太低了,还是4的版本号(谁知道为啥公司用这么Low版本的.NET),与这个新的api很难交互,所以就采取了新的方式,就是用C++调用api去捕获软件窗口并且把帧放到共享内存,然后由C#读取这块共享内存并且展示。
windows电脑中使用句柄来**唯一标识和管理各种系统资源的值。**我们要捕获软件的窗口,就要通过软件的窗口句柄来获取窗口。这里可以用VS自带的窗口工具Spy++来查看窗口句柄。
但是我们要捕获的肯定是软件最外层的窗口,而不是内部的窗口,这里展示的就是内部窗口。我们需要用代码获取到窗口的顶层窗口:
cs
static HWND ResolveTopLevelWindow(HWND hwnd)
{
if (!hwnd)
{
return nullptr;
}
HWND root = GetAncestor(hwnd, GA_ROOT);
if (root && IsWindow(root))
{
return root;
}
return hwnd;
}
这里的Spy++也可以获取到最顶层的窗口:
可以看出父子两个窗口的句柄并不一样,我们的Panel需要展示最顶层的窗口,因此对于传进来的窗口全部用代码处理,以获得最顶层窗口的句柄。欧克啊,现在已经拿到软件窗口的句柄可以进行窗口帧的捕获了,但是这里有个问题,句柄只是帮我们找到个这个窗口,但是windows提供的api是没法直接根据句柄来捕获窗口帧的,所以在这里要做一层桥接,把句柄包装为可以被api识别的对象:
cpp
static winrt::Windows::Graphics::Capture::GraphicsCaptureItem
CreateItemForWindow(HWND hwnd)
{
std::wcout << L"Creating capture item for window. hwnd = " << hwnd << L"\n";
PrintWindowInfo(hwnd, L"CreateItemForWindow target");
auto interopFactory =
winrt::get_activation_factory<
winrt::Windows::Graphics::Capture::GraphicsCaptureItem,
IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item{ nullptr };
HRESULT hr = interopFactory->CreateForWindow(
hwnd,
winrt::guid_of<winrt::Windows::Graphics::Capture::GraphicsCaptureItem>(),
winrt::put_abi(item));
std::wcout << L"IGraphicsCaptureItemInterop::CreateForWindow hr = "
<< HResultToHex(hr) << L"\n";
if (FAILED(hr))
{
std::wcout << L"CreateForWindow message: " << HResultToMessage(hr) << L"\n";
check_hresult(hr);
}
std::wcout << L"CreateForWindow succeeded.\n";
return item;
}
重点就两个,一个是获取激活工厂,这个工厂的类型是 IGraphicsCaptureItemInterop。这个接口不是一个标准的 WinRT 类型,而是一个COM 互操作接口 。它提供了一些底层的、非类型安全的 API,用于从传统的 Win32 句柄(如 HWND)创建对应的 WinRT 对象。interopFactory->CreateForWindow(...) : 这才是真正执行创建操作的方法。winrt::guid_of<T>() 是一个 C++/WinRT 提供的模板函数,它会返回类型 T 所对应的 GUID 值。在这里用来告诉这个工厂我需要创建啥对象。winrt::put_abi(obj) 是一个 C++/WinRT 提供的函数,它返回一个指向对象内部 ABI (Application Binary Interface) 指针的指针。ABI 是 C++/WinRT 对象在底层(即原始 COM 层面)的表示方式,它就是一个普通的 IUnknown* 或派生接口指针,这里是修改内部的指针的指针。
下面有个这个要捕捉的窗口对象了,那还得有摄像头取景器啊,这里Windows.Graphics.Capture采用的是WinRT 的 IDirect3DDevice。但是我们只有现成的创建创建原生 D3D11 设备的函数:
cpp
D3D_FEATURE_LEVEL levels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
};
D3D_FEATURE_LEVEL levelOut{};
HRESULT hr = D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
levels,
ARRAYSIZE(levels),
D3D11_SDK_VERSION,
d3dDevice.put(),
&levelOut,
d3dContext.put());
简单介绍一下Direct3D 11 (D3D11): 是 微软 为 Windows 生态系统打造的"官方御用"图形 API,与 Windows 紧密集成,功能强大,简单来说我们可以用这个来操作底层的显卡。我们先是定义了一个特权级,D3D_FEATURE_LEVEL 描述了 D3D11 API 的不同功能级别。级别越高,支持的 GPU 功能就越强大。这里的D3D11创建函数函数又是很多参数,让我们一一解释:
| 参数名 (Parameter Name) | 参数含义 (Meaning) |
|---|---|
pAdapter |
指定要使用的 GPU。传入 nullptr 表示让系统自动选择默认的图形适配器(通常是性能最强的那个)。 |
DriverType |
指定驱动程序的类型。D3D_DRIVER_TYPE_HARDWARE 表示使用真正的物理显卡来加速。其他选项还有 WARP(一种高质量的软件渲染器)或 REFERENCE(参考光栅化器,仅用于调试)。 |
Software |
如果 DriverType 设置为 D3D_DRIVER_TYPE_SOFTWARE,则在此处指定软件光栅化器的句柄。在这里不使用,所以是 nullptr。 |
Flags |
这是一个由程序员预先定义的变量,用于启用额外的设备创建选项。最常见的用途是启用调试层(Debug Layer),在开发时可以输出详细的错误和性能警告信息。发布版程序通常为 0。 |
pFeatureLevels |
传入上面定义的特性等级列表 (levels 数组) 的首地址。 |
FeatureLevels |
传入特性等级列表的元素个数。ARRAYSIZE(levels) 是一个宏,用于计算数组长度。 |
SDKVersion |
传入一个常量 (D3D11_SDK_VERSION),用于验证头文件和 DLL 版本是否兼容。 |
ppDevice |
输出参数。d3dDevice 很可能是一个 winrt::com_ptr<ID3D11Device> 智能指针。.put() 方法会返回一个指针,以便 D3D11CreateDevice 函数可以将新创建的设备对象指针填入其中。这个设备是创建其他 D3D11 资源(如纹理、着色器等)的工厂。 |
pFeatureLevel |
输出参数。将 levelOut 的地址传入,函数会把实际创建成功的特性等级写入这个变量。 |
ppImmediateContext |
输出参数。d3dContext 很可能是一个 winrt::com_ptr<ID3D11DeviceContext> 智能指针。D3D11CreateDevice 会将新创建的立即上下文(Immediate Context)指针填入。这个上下文是你命令 GPU 执行绘图、设置渲染状态等操作的地方,几乎所有的绘制调用都要通过它来完成。 |
但是winrt并不能直接接收这个IDirect3DDevice,这里又得作一层桥接(所以我觉得win的api的转换麻烦又严谨)。具体代码如下:
cpp
winrtDevice = CreateDirect3DDeviceFromD3D11Device(d3dDevice.get());
static winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice
CreateDirect3DDeviceFromD3D11Device(ID3D11Device* d3dDevice)
{
std::wcout << L"Creating WinRT Direct3D device from D3D11 device...\n";
com_ptr<IDXGIDevice> dxgiDevice;
HRESULT hr = d3dDevice->QueryInterface(__uuidof(IDXGIDevice), dxgiDevice.put_void());
std::wcout << L"QueryInterface(IDXGIDevice) hr = " << HResultToHex(hr) << L"\n";
check_hresult(hr);
com_ptr<::IInspectable> inspectable;
hr = CreateDirect3D11DeviceFromDXGIDevice(
dxgiDevice.get(),
reinterpret_cast<::IInspectable**>(inspectable.put()));
std::wcout << L"CreateDirect3D11DeviceFromDXGIDevice hr = " << HResultToHex(hr) << L"\n";
check_hresult(hr);
return inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}
这段代码是一个典型的 WinRT 与 DirectX 之间进行互操作 (Interoperability / Interop) 的例子。它的核心目的就是:将一个底层的、原生的 ID3D11Device 对象,转换/包装成一个高层的、现代化的 WinRT 接口 IDirect3D11Device 对象。
cpp
com_ptr<IDXGIDevice> dxgiDevice;
HRESULT hr = d3dDevice->QueryInterface(__uuidof(IDXGIDevice), dxgiDevice.put_void());
com_ptr<IDXGIDevice> dxgiDevice;: 创建一个智能指针,用于安全地持有IDXGIDevice接口。d3D11Device->QueryInterface(...): 这是 COM (Component Object Model) 对象的基本方法。它询问这个ID3D11Device实例:"你是否也实现了IDXGIDevice这个接口?"__uuidof(IDXGIDevice): 传入IDXGIDevice接口的唯一 ID (GUID)。dxgiDevice.put_void():put_void()用于获取dxgiDevice内部原始指针的地址(void**),让QueryInterface可以将转型后的指针写入其中。- 结果 :如果成功,
dxgiDevice智能指针现在就持有了与d3dDevice相关联的IDXGIDevice接口。
cpp
com_ptr<::IInspectable> inspectable;
hr = CreateDirect3D11DeviceFromDXGIDevice(
dxgiDevice.get(),
reinterpret_cast<::IInspectable**>(inspectable.put()));
com_ptr<::IInspectable> inspectable;: 创建一个指向IInspectable接口的智能指针。IInspectable是所有 WinRT 对象的根接口,类似于 .NET 中的Object。CreateDirect3D11DeviceFromDXGIDevice: 这是一个 Windows Runtime 提供的函数,它接收一个底层的 DXGI 设备 (IDXGIDevice),并创建一个对应的、可以在 WinRT 中使用的 Direct3D 设备。dxgiDevice.get(): 传入上一步获取的IDXGIDevice接口指针。reinterpret_cast<::IInspectable**>(inspectable.put()): 这是一个比较复杂的类型转换。inspectable.put()返回一个指向其内部原始指针 (IInspectable*) 的指针 (IInspectable**)。由于命名空间的原因(::表示全局命名空间),这里需要强制转换,确保类型匹配。CreateDirect3D11DeviceFromDXGIDevice会将新创建的 WinRT 对象指针放入inspectable中。- 结果 :
inspectable现在持有一个 WinRT 对象,这个对象内部封装了原始的ID3D11Device。
cpp
return inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3D11Device>();
inspectable.as<...>(): 这是winrt::com_ptr的一个方法,它执行一个安全的接口转换(类似QueryInterface)。它告诉系统:"我知道inspectable持有的对象实际上实现了IDirect3D11Device接口,请给我一个该接口的强类型实例。"- 返回值 :函数最终返回一个类型为
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3D11Device的对象,这个对象可以在 C++/WinRT 编写的 UWP 应用或其他 WinRT 组件中无缝使用。
ok啊,现在要获取的窗口句柄有了,用来获取的工具有了,下面得讲解内存共享和数据帧格式的内容了。在共享内存中,数据的格式是很重要的,就像计算机网络传输数据帧一样。对于进程来说,如果没有数据格式,那么内存不过是一块连续的数据空间罢了,只有我们先定义了内存格式,进程间的才能读取并且解析有意义的数据。下面给出经典的数据帧的格式要求:
cpp
#pragma pack(push, 1)
struct FrameHeader
{
uint32_t magic; // 'WGCH'
uint32_t version; // 1
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t format; // 1 = BGRA8
uint64_t frameId;
uint64_t timestampQpc;
uint32_t dataSize;
uint32_t reserved;
};
#pragma pack(pop)
上面这段是我们自定义数据帧的格式,让我们细细讲解这个帧的具体格式。先介绍帧头部的各个字段:
| 字段名 (Field Name) | 类型 (Type) | 说明 (Description) |
|---|---|---|
magic |
uint32_t / char[4] |
帧魔数(例如 'WGCH'),用于快速判断这块内存是否是您定义的协议。 |
version |
uint32_t |
协议版本号,便于未来升级兼容。 |
width |
uint32_t |
图像宽度(像素)。 |
height |
uint32_t |
图像高度(像素)。 |
stride |
uint32_t |
每行有效字节数(当前实现是 width * 4)。 |
format |
uint32_t |
像素格式标识(当前 1 = BGRA8)。 |
frameId |
uint64_t |
单调递增帧序号,消费端可据此判重、跳帧检测。 |
timestampQpc |
uint64_t |
QueryPerformanceCounter 时间戳,便于计算延迟/FPS。 |
dataSize |
uint32_t |
像素数据总字节数(通常 stride * height)。 |
reserved |
uint32_t[2] / uint64_t[1] |
预留字段,给后续扩展用。 |
这里的stride很有用,因为采取的像素存取的格式是BGRA8这种数据格式,三个字节表示颜色,一个字节表示alpha(透明度)。所以这里的一个stride实际是width*4,因为假如宽度是933个像素,那么这一行在内存就占933*4个字节,也就是width*4,这是为了让程序能够知道每一行开头的位置在哪。此外就是#pragma pack(push, 1) 和 #pragma pack(pop)。这是为了接收者和发送者的内存对齐方式是一致的,不会解析出错。帧首部后面跟的就是像素值,这两个函数就是用来获取帧首部和后侧像素值的地址的。
cpp
FrameHeader* Header() const
{
return reinterpret_cast<FrameHeader*>(base);
}
uint8_t* Pixels() const
{
return reinterpret_cast<uint8_t*>(base) + sizeof(FrameHeader);
}
现在帧的格式有了,接下来就是要建立共享内存以存放帧给消费者接收并且解析。这里的代码也是比较复杂:
cpp
struct SharedFrameBuffer
{
HANDLE hMap = nullptr;
HANDLE hEvent = nullptr;
void* base = nullptr;
size_t size = 0;
//......
bool Create(const std::wstring& mapName, const std::wstring& eventName, size_t mappingSize)
{
size = mappingSize;
std::wcout << L"Creating shared memory with mapName: " << mapName
<< L", eventName: " << eventName
<< L", size=" << mappingSize << L"\n";
hMap = CreateFileMappingW(
INVALID_HANDLE_VALUE,
nullptr,
PAGE_READWRITE,
static_cast<DWORD>((mappingSize >> 32) & 0xFFFFFFFF),
static_cast<DWORD>(mappingSize & 0xFFFFFFFF),
mapName.c_str());
if (!hMap)
{
DWORD gle = GetLastError();
std::wcerr << L"CreateFileMappingW failed. GLE=" << gle
<< L", msg=" << GetLastErrorMessage(gle) << L"\n";
return false;
}
std::wcout << L"File mapping created successfully.\n";
base = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, mappingSize);
if (!base)
{
DWORD gle = GetLastError();
std::wcerr << L"MapViewOfFile failed. GLE=" << gle
<< L", msg=" << GetLastErrorMessage(gle) << L"\n";
return false;
}
hEvent = CreateEventW(nullptr, FALSE, FALSE, eventName.c_str());
if (!hEvent)
{
DWORD gle = GetLastError();
std::wcerr << L"CreateEventW failed. GLE=" << gle
<< L", msg=" << GetLastErrorMessage(gle) << L"\n";
return false;
}
ZeroMemory(base, mappingSize);
Header()->magic = FRAME_MAGIC;
Header()->version = FRAME_VERSION;
Header()->format = FRAME_FORMAT_BGRA8;
std::wcout << L"Shared memory initialized successfully.\n";
return true;
}
void Close()
{
if (base)
{
UnmapViewOfFile(base);
base = nullptr;
std::wcout << L"Unmapped view of file.\n";
}
if (hMap)
{
CloseHandle(hMap);
hMap = nullptr;
std::wcout << L"Closed file mapping handle.\n";
}
if (hEvent)
{
CloseHandle(hEvent);
hEvent = nullptr;
std::wcout << L"Closed event handle.\n";
}
}
~SharedFrameBuffer()
{
Close();
}
};
这里最重要的就是Create函数,首先是三个传入参数:
mapName: 一个宽字符串,用于给共享内存区域指定一个全局唯一的名称。其他进程可以通过这个名字来打开这个已经存在的共享内存。eventName: 一个宽字符串,用于给同步事件指定一个全局唯一的名称。其他进程也可以通过这个名字来打开这个事件。mappingSize: 一个size_t类型的整数,指定要创建的共享内存区域的大小(以字节为单位)。
然后是创建命名共享对象:CreateFileMappingW
- 目的:创建一个命名的共享内存对象(在 Windows 中称为"文件映射对象")。
INVALID_HANDLE_VALUE: 第一个参数表示要在系统页文件(Page File)上创建一块匿名的共享内存。如果传入一个真实的文件句柄,则是在该文件上创建映射。nullptr: 安全属性,传入nullptr表示使用默认的安全性。PAGE_READWRITE: 指定内存保护属性,表示创建的内存区域是可读可写的。(mappingSize >> 32) & 0xFFFFFFFF和(mappingSize & 0xFFFFFFFF):CreateFileMappingW使用两个 32 位的参数来指定总大小(因为单个参数最大只能表示 4GB)。这里将size_t类型的mappingSize进行位运算拆分,高位部分作为最大文件大小的高位,低位部分作为低位。mapName.c_str(): 传入共享内存对象的名称。如果系统中已经存在一个同名的共享内存,CreateFileMappingW会返回那个已存在对象的句柄,而不是创建一个新的。
映射到进程虚拟空间:MapViewOfFile
- 目的:将创建的共享内存对象"映射"到当前进程的虚拟地址空间中,使其可以像普通内存一样被访问。
hMap: 传入上一步创建的共享内存对象的句柄。FILE_MAP_ALL_ACCESS: 请求对映射视图的完全访问权限(读、写、执行)。0, 0: 指定映射的起始偏移量,这里从 0 开始。mappingSize: 指定要映射的区域大小。base:MapViewOfFile成功后会返回一个指向映射内存起始地址的指针,并将其赋值给成员变量base。从此,程序就可以通过base指针来读写这块共享内存了。
创建同步命名事件对象:CreateEventW
- 目的:创建一个用于同步的命名事件对象。
nullptr: 安全属性,使用默认。FALSE(bManualReset) :FALSE表示这是一个自动重置事件。当一个等待线程被唤醒后,系统会自动将事件状态重置为无信号。FALSE(bInitialState) :FALSE表示事件的初始状态为无信号。eventName.c_str(): 传入事件对象的名称,用于其他进程打开。hEvent: 如果成功,返回的事件句柄会被保存到成员变量hEvent中。
初始化工作:ZeroMemory以及一下。
- 清零内存 :
ZeroMemory将整块共享内存(从base地址开始,mappingSize大小)清零,避免残留垃圾数据。- 初始化帧头 :接下来三行代码,利用之前定义的
Header()成员函数(它返回指向内存开头的FrameHeader结构体的指针),来设置帧协议的初始信息。设置了魔数、协议版本和像素格式,这样其他进程在读取时就能识别并正确解析数据。
总结一下:这个 Create 函数完成了共享内存 IPC 的准备工作。它创建了一个名为 mapName 的共享内存块和一个名为 eventName 的事件,将内存映射到当前进程的地址空间,初始化了协议头,为后续的生产者-消费者模式(一个进程写入数据,另一个进程读取并等待事件通知)奠定了基础。(有关上述api的参数的详情可看下面的界面,贴个网址,MapViewOfFile function (memoryapi.h) - Win32 apps | Microsoft Learn)
下面继续深入,讨论帧池和会话这两个重要的概念。在 Windows.Graphics.Capture 这套模型里,framePool 和 session 不是重复概念,而是分工非常清晰的两个层次。session 负责"控制面",也就是把哪个窗口作为捕获目标、何时开始/结束捕获这类会话级动作;framePool 负责"数据面",也就是缓存捕获到的帧并提供取帧机制。换句话说:没有 session,捕获不会开始;没有 framePool,即使开始了也没有地方接收帧。这也是代码里必须同时创建两者的根本原因。对应实现中,先用 CreateFreeThreaded(...) 建帧池,再用 framePool.CreateCaptureSession(item) 建会话,这里的item就是我们之前获取的句柄,最后 session.StartCapture() 启动,形成完整链路:
cpp
// CreateFreeThreaded
framePool = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::CreateFreeThreaded(
winrtDevice,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
lastSize);
frameArrivedToken = framePool.FrameArrived({ this, &CaptureApp::OnFrameArrived });
session = framePool.CreateCaptureSession(item);
session.StartCapture();
为什么先创建帧池再创建会话?因为会话启动后帧会持续产生,如果没有帧池承接,数据流就没有落点。另外这里选择的是 CreateFreeThreaded,它去掉了对 DispatcherQueue 的依赖(这里需要了解windows的线程模型),让 FrameArrived 在内部工作线程触发,这对我们这种独立 Host 进程更合适:主线程不需要 UI 消息循环也能工作。代码的主循环也印证了这一点,它只是监控窗口是否还存在,并不承担帧处理职责:
cpp
std::wcout << L"Entering message loop surrogate. Monitoring hwndTop...\n";
while (IsWindow(hwndTop))
{
Sleep(50);
}
std::wcout << L"Target window is no longer valid. Stopping capture.\n";
app.Stop();
有了帧池和会话之后,下一层就是事件管理。事件管理的目的,是把"帧到来"从轮询模式改成通知模式:不是你不断问"有没有新帧",而是系统在有帧时主动触发回调。这样做有三个直接收益:第一,减少空转轮询;第二,天然贴合异步高频场景;第三,方便把处理逻辑集中在一个入口函数里。代码里用 frameArrivedToken 保存订阅句柄,这样 Stop() 时可以精确解绑,避免释放对象后回调还在打进来:
cpp
void Stop()
{
std::wcout << L"Stopping capture session...\n";
if (framePool)
{
framePool.FrameArrived(frameArrivedToken);
}
session = nullptr;
framePool = nullptr;
item = nullptr;
std::wcout << L"Capture session stopped.\n";
}
这段释放顺序其实就是事件管理的一部分:先取消事件,再销毁资源,否则高频回调很容易踩到悬空对象。
OnFrameArrived 是整条链路的核心,它把"显卡中的帧"转成"共享内存可读数据"。这个函数可以按四步理解。第一步,取帧和基本校验:TryGetNextFrame() 可能拿到空帧,ContentSize 可能无效,这两类都直接返回,避免后续无效计算。第二步,处理尺寸变化:如果窗口尺寸改变,就 Recreate(...) 重建帧池,不强行按旧尺寸继续处理。第三步,执行 GPU→CPU 读回:从 frame.Surface() 取 ID3D11Texture2D,创建 D3D11_USAGE_STAGING 纹理,CopyResource 后 Map 到 CPU 地址。第四步,写入共享内存并发信号:逐行 memcpy(按 mapped.RowPitch 读、按 stride 写),更新 FrameHeader,最后 SetEvent 通知消费者来取新帧。
cpp
void OnFrameArrived(
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
winrt::Windows::Foundation::IInspectable const&)
{
// ...日志省略
auto frame = sender.TryGetNextFrame();
if (!frame) return;
auto contentSize = frame.ContentSize();
if (contentSize.Width <= 0 || contentSize.Height <= 0) return;
if (contentSize.Width != lastSize.Width || contentSize.Height != lastSize.Height)
{
lastSize = contentSize;
framePool.Recreate(
winrtDevice,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
contentSize);
return;
}
// ...进入纹理转换与拷贝
auto access = frame.Surface().as<IDirect3DDxgiInterfaceAccess>();
com_ptr<ID3D11Texture2D> srcTexture;
HRESULT hr = access->GetInterface(__uuidof(ID3D11Texture2D), srcTexture.put_void());
check_hresult(hr);
// staging 纹理:让 CPU 可以读取
D3D11_TEXTURE2D_DESC stagingDesc = srcDesc;
stagingDesc.Usage = D3D11_USAGE_STAGING;
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
// ...
d3dContext->CopyResource(staging.get(), srcTexture.get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3dContext->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped);
check_hresult(hr);
// 逐行拷贝,兼容 RowPitch != width*4
for (uint32_t y = 0; y < height; ++y)
{
std::memcpy(dst + y * stride, src + y * mapped.RowPitch, stride);
}
auto* hdr = shared.Header();
hdr->magic = FRAME_MAGIC;
hdr->version = FRAME_VERSION;
hdr->width = width;
hdr->height = height;
hdr->stride = stride;
hdr->format = FRAME_FORMAT_BGRA8;
hdr->dataSize = dataSize;
hdr->timestampQpc = static_cast<uint64_t>(qpc.QuadPart);
hdr->frameId = ++frameCounter;
d3dContext->Unmap(staging.get(), 0);
if (shared.hEvent)
{
SetEvent(shared.hEvent);
}
帧池和会话解决"捕获系统如何建立",事件管理和 OnFrameArrived 解决"每一帧如何被可靠处理并投递给下游"。
最后就是简单的C#启动这个编译好的exe,双方进行通信了,主要是三个层面的对齐:
(1)协议对齐
cs
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct FrameHeader
{
public uint magic; // 'WGCH' = 0x48434757
public uint version; // 1
public uint width;
public uint height;
public uint stride;
public uint format; // 1 = BGRA8
public ulong frameId;
public ulong timestampQpc;
public uint dataSize;
public uint reserved;
}
private const uint FRAME_MAGIC = 0x48434757;
(2)数据对齐(像素格式+按行拷贝)
cs
const uint32_t width = srcDesc.Width;
const uint32_t height = srcDesc.Height;
const uint32_t stride = width * 4;
const uint32_t dataSize = stride * height;
uint8_t* dst = shared.Pixels();
uint8_t* src = reinterpret_cast<uint8_t*>(mapped.pData);
for (uint32_t y = 0; y < height; ++y)
{
std::memcpy(dst + y * stride, src + y * mapped.RowPitch, stride);
}
(3)时序对齐(事件 + 帧号)
cs
_frameEvent = EventWaitHandle.OpenExisting(_eventName);
// ...
if (hdr.frameId == _lastFrameId) return;
// ...
_lastFrameId = hdr.frameId;
ok,看下最终的效果:

终于是可以捕获cursor,kiro这种走gpu渲染路线的窗口了!但是又出现了新的问题,没法捕获浏览器窗口,C++那边的程序没法获取窗口标题,这个bug改天去修正一下,另外虽然C++那边给的事是事件驱动的代码,但是C#这边采用的是轮询机制,我记得当时是因为事件驱动在时序上有bug所以采用了驱动的方式,后续修改完继续上传github。