简介:本资源面向C++游戏开发初学者与DirectX图形编程学习者,聚焦解决DirectX硬件加速游戏无法使用系统截屏键(如PrtScn)截图的核心痛点——因GPU直出渲染与硬件覆盖层(Hardware Overlay)机制导致常规抓屏失效。提供一套基于Visual C++编写的轻量级Direct3D截屏实现方案,涵盖设备初始化、帧缓冲区读取、纹理数据映射与BMP格式保存等完整流程。压缩包共12个文件,含4个头文件(.h)定义接口与结构体、2个源码文件(.cpp)实现核心逻辑、1个解决方案(.sln)与1个工程配置(.vcproj),辅以图标(.ico)、资源脚本(.rc)及说明文本(.txt),总大小仅22KB,结构精简、即开即学。已有558人学习下载,读者可直接编译运行参考源码,理解Direct3D渲染管线中截屏的关键Hook时机与API调用范式,掌握绕过硬件加速限制的底层截图技术,为游戏调试、画面分享与教学演示提供可靠工具支撑。
1. DirectX 游戏截屏不是“按 PrintScreen 就完事”——硬件加速渲染绕过了 GDI,必须用 DXGI 或 D3D API 在帧缓冲层抓取
你刚买的新游戏,按键盘 PrintScreen 键毫无反应;用 Snipaste、QQ 截图、甚至 Windows 自带的 Xbox Game Bar 也只捕到黑屏或花屏。这不是软件故障,而是 DirectX(尤其是 D3D9/D3D11/D3D12)渲染管线的底层设计使然:GPU 直接将画面写入显存中的后台缓冲区(back buffer),全程不经过 Windows GDI 或桌面窗口管理器(DWM)的合成路径。传统截屏工具依赖BitBlt、PrintWindow或Desktop Duplication API,对这类独占式、全屏、无窗口句柄或启用D3DPRESENT_DONOTWAIT的 DirectX 应用完全失效。真正能稳定抓取的,只有两类方案:一是通过 DXGI Desktop Duplication(仅限 D3D11+ 且非独占全屏)、二是注入并 hook DirectX 设备上下文,在 Present/EndScene 后立即读取渲染目标纹理。本篇聚焦后者——用 Visual C++ 编写轻量级 DirectX 截屏模块,不依赖第三方框架,兼容 D3D9/D3D11,可嵌入游戏主循环或作为独立 DLL 注入,附完整可编译源码结构与关键参数说明。适合游戏辅助开发、录屏工具二次开发、自动化测试截图等需精确帧捕获的场景。
2. 为什么必须用 DirectX API 截屏:从渲染管线看 GDI 失效的根本原因
2.1 DirectX 渲染路径彻底绕过桌面合成器
Windows 桌面截图工具(如 Snipping Tool、Greenshot)依赖 GDI 的GetDC(GetDesktopWindow())获取桌面设备上下文,再用BitBlt拷贝屏幕像素。但 DirectX 应用在创建交换链(Swap Chain)时,若指定D3DPRESENT_PARAMETERS中的Windowed = FALSE(全屏)或启用D3DPRESENT_DONOTWAIT,驱动会直接将帧数据写入显存专属缓冲区,并跳过 DWM 合成步骤。此时GetDesktopWindow()返回的 DC 指向的是 DWM 合成后的“虚拟桌面”,而 DirectX 渲染内容根本未进入该路径——它被 GPU 直接输出到显示器,中间不经过系统内存拷贝。实测中,调用PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT)对典型 D3D9 全屏游戏窗口返回FALSE,GetWindowDC(hwnd)获取的 DC 为空白,证实了这一隔离机制。
提示:即使游戏以“窗口化全屏”(Borderless Windowed)运行,若其使用
IDXGISwapChain::Present而非GdiFlush,仍可能绕过 DWM。是否绕过取决于驱动实现与 DXGI 标志组合,不能仅凭窗口样式判断。
2.2 DXGI Desktop Duplication 的适用边界与硬伤
Windows 8+ 提供IDesktopDuplication接口,理论上可捕获任意 DirectX 应用画面。但它要求目标应用必须运行在 D3D11 或更高版本,且不能处于独占全屏模式(Exclusive Fullscreen)。当游戏调用SetFullscreenState(TRUE, nullptr)进入独占模式时,DXGI 会释放桌面资源控制权,CreateDesktopDuplication直接失败并返回DXGI_ERROR_UNSUPPORTED。此外,该 API 在多显卡(尤其核显+独显混合)环境下易出现帧延迟或绿屏,且无法获取 Alpha 通道(所有像素强制不透明)。对于 D3D9 游戏(大量老游戏、Unity 5.x 以下项目),此接口根本不可用。
2.3 最可靠路径:在 Present/EndScene 钩子中读取渲染目标
唯一跨版本、跨模式、高精度的方案,是在 DirectX 设备的呈现函数执行后,立即读取当前渲染目标(Render Target)的纹理数据。具体分两步:
- D3D9:Hook
IDirect3DDevice9::Present,在函数返回前调用GetRenderTargetData将后台缓冲区拷贝到系统内存纹理; - D3D11:Hook
ID3D11DeviceContext::DrawIndexed或Present,用Map+CopyResource将ID3D11Texture2D(后台缓冲区)映射到 CPU 可读内存; - 统一处理:无论 D3D9/D3D11,最终都得到
RGB24或BGRA32格式的原始像素数组,可直接保存为 BMP/PNG 或送入编码器。
此方案成功率接近 100%,因为它是渲染管线的“最后一公里”干预,不依赖桌面状态,不关心窗口模式,且像素数据与显示器输出完全一致(无 DWM 合成带来的色彩偏移或缩放失真)。
3. Visual C++ 实现 DirectX 截屏模块:D3D9 与 D3D11 双路径代码详解
3.1 工程配置与依赖项设置(Visual Studio 2019+)
新建空 Win32 控制台项目,关闭预编译头(Project Properties → C/C++ → Precompiled Headers → Not Using Precompiled Headers)。在Configuration Properties → General → Windows SDK Version选择 10.0 或更高。关键依赖项:
- D3D9:链接
d3d9.lib,包含<d3d9.h>,需确保目标系统安装Microsoft Visual C++ Redistributable(因 D3D9.dll 由系统提供,但部分旧系统需补丁); - D3D11:链接
d3d11.lib和dxgi.lib,包含<d3d11.h>和<dxgi.h>; - 通用:添加
#pragma comment(lib, "d3d9.lib")等指令避免手动配置。
注意:若编译报错
error: microsoft visual c++ 14.0 or greater is required,说明缺少 VS2015+ 工具集。请安装Microsoft Visual C++ Build Tools或完整 Visual Studio,并在项目属性中设置Platform Toolset为v142(VS2019)或v143(VS2022)。
3.2 D3D9 截屏核心逻辑(Present Hook)
// 全局变量声明 static IDirect3DDevice9* g_pDevice = nullptr; static LPDIRECT3DTEXTURE9 g_pStagingTex = nullptr; // 原始 Present 函数指针 typedef HRESULT (WINAPI *PresentFunc)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*); static PresentFunc oPresent = nullptr; // Hook 后的 Present 函数 HRESULT WINAPI hkPresent(IDirect3DDevice9* pDevice, const RECT* pSourceRect, const RECT* pDestRect, HWND hDestWindow, const RGNDATA* pDirtyRegion) { // 1. 调用原函数完成渲染 HRESULT hr = oPresent(pDevice, pSourceRect, pDestRect, hDestWindow, pDirtyRegion); // 2. 检查是否需要截屏(例如检测热键 Ctrl+Shift+P) if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(VK_SHIFT) & GetAsyncKeyState('P')) { // 3. 创建暂存纹理(仅首次调用时创建) if (!g_pStagingTex) { D3DSURFACE_DESC desc; IDirect3DSurface9* pBackBuffer = nullptr; pDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackBuffer); pBackBuffer->GetDesc(&desc); pDevice->CreateOffscreenPlainSurface( desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, // 关键:必须 SYSTEMMEM 才能 LockRect &g_pStagingTex, nullptr ); pBackBuffer->Release(); } // 4. 拷贝后台缓冲区到暂存纹理 IDirect3DSurface9* pBackBuffer = nullptr; pDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackBuffer); pDevice->GetRenderTargetData(pBackBuffer, g_pStagingTex); pBackBuffer->Release(); // 5. 锁定暂存纹理获取像素指针 D3DLOCKED_RECT lockedRect; if (SUCCEEDED(g_pStagingTex->LockRect(0, &lockedRect, nullptr, D3DLOCK_READONLY))) { // lockedRect.pBits 指向 RGB24 像素数据(D3DFMT_X8R8G8B8 格式) // 此处可调用 SaveToBMP(lockedRect.pBits, desc.Width, desc.Height, desc.Pitch); g_pStagingTex->UnlockRect(0); } } return hr; }参数说明与关键点:
D3DPOOL_SYSTEMMEM:必须指定此池类型,否则LockRect失败。D3DPOOL_DEFAULT在显存中,CPU 不可直接访问;GetRenderTargetData:D3D9 唯一能将 GPU 缓冲区拷贝到 CPU 内存的 API,性能开销可控(单次调用约 0.5~2ms);D3DFMT_X8R8G8B8:最常用后台缓冲格式,对应 32 位 BGRA,lockedRect.Pitch是每行字节数(通常 = width * 4),需按此步长遍历像素;- 热键检测用
GetAsyncKeyState而非GetKeyState,避免阻塞主线程。
3.3 D3D11 截屏核心逻辑(Present Hook + Map)
// 全局变量 static ID3D11Device* g_pDevice11 = nullptr; static ID3D11DeviceContext* g_pContext11 = nullptr; static ID3D11Texture2D* g_pStagingTex11 = nullptr; typedef HRESULT (WINAPI *Present11Func)(IDXGISwapChain*, UINT, UINT); static Present11Func oPresent11 = nullptr; HRESULT WINAPI hkPresent11(IDXGISwapChain* pSwapChain, UINT SyncInterval, UINT Flags) { HRESULT hr = oPresent11(pSwapChain, SyncInterval, Flags); if (GetAsyncKeyState(VK_CONTROL) & GetAsyncKeyState(VK_SHIFT) & GetAsyncKeyState('P')) { // 1. 获取后台缓冲区纹理 ID3D11Texture2D* pBackBuffer = nullptr; pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (LPVOID*)&pBackBuffer); // 2. 创建 CPU 可读的暂存纹理(D3D11_USAGE_STAGING) if (!g_pStagingTex11) { D3D11_TEXTURE2D_DESC desc; pBackBuffer->GetDesc(&desc); desc.Usage = D3D11_USAGE_STAGING; // 关键:仅 STAGING 可 Map desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; desc.BindFlags = 0; desc.MiscFlags = 0; g_pDevice11->CreateTexture2D(&desc, nullptr, &g_pStagingTex11); } // 3. 拷贝数据(GPU -> CPU 可读纹理) g_pContext11->CopyResource(g_pStagingTex11, pBackBuffer); // 4. 映射内存获取像素指针 D3D11_MAPPED_SUBRESOURCE mapped; if (SUCCEEDED(g_pContext11->Map(g_pStagingTex11, 0, D3D11_MAP_READ, 0, &mapped))) { // mapped.pData 指向 BGRA32 数据,mapped.RowPitch 是每行字节数 // SaveToPNG((BYTE*)mapped.pData, mapped.RowPitch, desc.Width, desc.Height); g_pContext11->Unmap(g_pStagingTex11, 0); } pBackBuffer->Release(); } return hr; }参数说明与关键点:
D3D11_USAGE_STAGING:唯一允许Map的用法,D3D11_USAGE_DEFAULT不可映射;CopyResource:比GetRenderTargetData更高效,是 D3D11 推荐方式;D3D11_MAP_READ:指定只读映射,避免写冲突;RowPitch:必须用此值计算行偏移,不能直接用width * 4,因 GPU 可能对齐到 256 字节边界。
3.4 Hook 实现:Detours 或 MinHook 注入
使用 Microsoft Detours(需下载并引用detours.h)进行 API Hook:
#include "detours.h" #pragma comment(lib, "detours.lib") void InstallD3D9Hook() { // 获取 D3D9 设备指针(需在游戏初始化后调用) // 此处省略设备查找逻辑(可通过枚举窗口、扫描内存或等待 CreateDevice 返回) // 假设已获得 g_pDevice // Hook Present 函数 oPresent = (PresentFunc)DetourFindFunction("d3d9.dll", "Present"); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)oPresent, hkPresent); DetourTransactionCommit(); } // D3D11 Hook 类似,Hook dxgi.dll 中的 Present常见失败排查:
- 若
DetourFindFunction返回nullptr,检查d3d9.dll是否已加载(EnumProcessModules验证); - Hook 失败常因权限不足,需以管理员身份运行注入器;
- D3D11 Hook 必须 Hook
dxgi.dll的Present,而非d3d11.dll的函数。
4. 编译、注入与运行时调试:解决 “DirectX 修复工具” 无法解决的底层问题
4.1 编译输出与运行环境校验
生成 Release x64 版本 DLL(游戏多为 64 位),用Dependency Walker检查是否缺失d3d9.dll或dxgi.dll。若提示API-MS-WIN-CRT-RUNTIME-L1-1-0.DLL缺失,说明目标机未安装Microsoft Visual C++ Redistributable—— 此时需打包vcredist_x64.exe与你的 DLL 一同部署,或静态链接 CRT(Project Properties → C/C++ → Code Generation → Runtime Library →/MT)。注意:静态链接会增大 EXE 体积,但避免 redistributable 依赖。
提示:“directx repair 增强版” 或 “dx修复工具” 仅修复系统级 DirectX 运行库(如
d3dcompiler_47.dll),无法解决你的代码因缺少vcruntime140.dll导致的启动失败。务必区分Visual C++ Redistributable(C++ 运行时)与DirectX End-User Runtimes(图形 API)。
4.2 游戏进程注入与设备指针获取
注入本身不难,难点在于准确获取 IDirect3DDevice9* 或 ID3D11Device*。常见方法:
- 枚举窗口法:
FindWindow获取游戏主窗口,GetWindowThreadProcessId得到 PID,再OpenProcess读取内存; - 内存扫描法:D3D9 设备指针通常存储在全局变量或 vtable 中,用 Cheat Engine 扫描
0x00000000到0x7FFFFFFF区域,搜索D3DDEVICE字符串附近指针; - 回调注入法:编写
AppInit_DLL(注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs),在游戏加载时自动注入,于DllMain中遍历模块找d3d9.dll地址。
推荐实践:对 Unity 游戏,直接 HookUnityPlayer.dll中的UnityGetD3D11Device导出函数;对老游戏(如《魔兽争霸3》),用ScanPattern在d3d9.dll模块内搜索mov eax, [esi+0x1C]指令(D3D9 设备 vtable 偏移)。
4.3 截屏文件保存与格式转换
获取到lockedRect.pBits或mapped.pData后,保存为 BMP 最简:
bool SaveToBMP(BYTE* pData, int width, int height, int pitch) { BITMAPFILEHEADER bmfh = {0}; BITMAPINFOHEADER bmih = {0}; bmfh.bfType = 0x4D42; // 'BM' bmfh.bfSize = sizeof(bmfh) + sizeof(bmih) + height * pitch; bmfh.bfOffBits = sizeof(bmfh) + sizeof(bmih); bmih.biSize = sizeof(bmih); bmih.biWidth = width; bmih.biHeight = -height; // top-down DIB bmih.biPlanes = 1; bmih.biBitCount = 32; bmih.biCompression = BI_RGB; HANDLE hFile = CreateFile(L"screen.bmp", GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, 0, nullptr); DWORD written; WriteFile(hFile, &bmfh, sizeof(bmfh), &written, nullptr); WriteFile(hFile, &bmih, sizeof(bmih), &written, nullptr); // 逐行写入(BMP 行需 4 字节对齐,pitch 已满足) for (int y = 0; y < height; y++) { WriteFile(hFile, pData + y * pitch, pitch, &written, nullptr); } CloseHandle(hFile); return true; }关键参数表:
| 参数 | 说明 | 典型值 | 注意事项 |
|---|---|---|---|
pitch | 每行字节数 | width * 4(BGRA32) | 必须用 API 返回值,不可硬编码 |
biHeight | BMP 高度 | -height | 负值表示 top-down 存储,避免图像翻转 |
biBitCount | 位深度 | 32 | D3D9 默认D3DFMT_X8R8G8B8,D3D11 为DXGI_FORMAT_B8G8R8A8_UNORM |
CreateFile路径 | 保存位置 | L"screen_%d.bmp" | 使用绝对路径,避免游戏工作目录不可写 |
4.4 性能优化:避免每帧截屏导致卡顿
截屏操作有 I/O 和 GPU-CPU 同步开销,频繁调用会导致帧率骤降。优化策略:
- 双缓冲队列:用
std::queue<std::vector<BYTE>>缓存最近 5 帧像素数据,另起线程异步保存,主线程只负责拷贝; - 分辨率降采样:截屏前用
StretchRect将后台缓冲区缩小到 1280×720 再拷贝,减少pitch * height数据量; - 条件触发:不依赖热键,改为监听游戏内事件(如
ID3D11DeviceContext::DrawIndexed调用次数突增,标志新场景加载)。
5. 验证截屏有效性:三步定位“黑屏”根源与修复路径
5.1 第一步:确认 DirectX 版本与渲染模式
运行游戏,打开Process Explorer(Sysinternals 工具),右键游戏进程 → Properties → Image → 查看d3d9.dll、d3d11.dll或dxgi.dll是否在Loaded Modules列表中。若仅见d3d9.dll,则必须走 D3D9 Hook 路径;若见dxgi.dll但无d3d11.dll,可能是 D3D10(需d3d10.dll);若三者皆无,游戏可能用 OpenGL 或 Vulkan,本方案不适用。
注意:“directx 12 is not supported on your system. try running without the -dx12” 错误表明游戏尝试初始化 D3D12 失败,但实际可能回退到 D3D11,此时仍可用 D3D11 Hook。
5.2 第二步:Hook 后验证设备指针有效性
在hkPresent或hkPresent11开头插入日志:
OutputDebugString(L"[D3D9] Present called, device=0x"); WCHAR buf[32]; swprintf_s(buf, L"%p", pDevice); OutputDebugString(buf); OutputDebugString(L"\n");用DebugView(Sysinternals)捕获输出。若日志中device为0x00000000,说明设备指针未正确获取,需检查注入时机(是否在CreateDevice之后)或内存扫描逻辑。
5.3 第三步:像素数据完整性检查
保存 BMP 后,用十六进制编辑器(如 HxD)打开,跳转到像素数据起始位置(bfOffBits值),检查前 16 字节是否为有效颜色值(非全00或FF)。若全为00 00 00 00,说明GetRenderTargetData或CopyResource失败,常见原因:
- D3D9:
g_pStagingTex创建失败(检查CreateOffscreenPlainSurface返回值); - D3D11:
Map返回E_ACCESSDENIED(检查D3D11_USAGE_STAGING和CPUAccessFlags是否匹配); - 游戏启用
D3DPRESENT_DONOTWAIT且缓冲区未就绪,需加Sleep(1)重试。
最终验证技巧:在SaveToBMP前,将第一行像素(pData开头 16 字节)写入日志,例如OutputDebugString(L"Pixel[0]=0x");+swprintf_s(buf, L"%02X%02X%02X%02X", pData[0], pData[1], pData[2], pData[3]);。若日志显示0000FF00(绿色),说明数据有效;若为00000000,则源头无数据。
本文还有配套的精品资源,点击获取