简介:本资源是面向Windows 10及以上系统怀旧游戏玩家与PC兼容性调试人员的DirectDraw兼容性补丁方案,专为解决老游戏因DirectDraw接口废弃导致的菜单绘制异常、画面闪烁、颜色错乱等核心图形问题。项目基于ddraw-master分支开发,通过实现轻量级兼容层模拟原始DDraw API行为,使依赖DirectDraw的90年代末至2000年代初经典游戏(如《暗黑破坏神》《帝国时代II》等)无需降级系统即可稳定运行。压缩包共136个文件,含77个头文件(h)定义接口与结构、54个C++源文件(cpp)实现Surface管理、Hook注入、WinProc拦截、Paint渲染等关键逻辑,另有vcxproj工程配置及README说明,整体仅122KB,精简高效。目前已有1177人学习下载,读者可直接获取完整编译就绪的补丁源码,掌握DDraw兼容层的核心机制——包括RealPrimarySurface模拟、DcFunctions钩子调度、KernelModeThunks内核模式适配等关键技术模块,具备二次定制与问题定位能力。
1. DDrawCompat 不是“打补丁”,而是重写 DirectDraw 的调用链路
你在 Windows 10 上双击《红色警戒2》或《暗黑破坏神2》时,菜单栏突然变灰、右键弹出空白框、切换窗口后画面撕裂——这不是显卡驱动问题,也不是游戏本身损坏,而是 Windows 10 内核已彻底移除对ddraw.dll原生实现的支持。系统仍保留ddraw.dll文件壳,但内部函数(如CreateSurface、Blt、Flip)全部返回E_NOTIMPL或直接跳转到空 stub。DDrawCompat 的本质不是“绕过”或“欺骗”系统,而是在用户态完整接管 DirectDraw API 的入口与出口:它拦截所有CoCreateInstance(CLSID_DirectDraw)调用,用自己实现的RealPrimarySurface和SurfaceImpl替代原生 COM 对象,并将绘图指令翻译为现代 DXGI/WARP 调用。这意味着你运行的不是“兼容模式”,而是一套运行在 Win32 子系统上的轻量级图形中间件。它不依赖注册表劫持、不修改系统 DLL、不触发 UAC 提权,仅靠LoadLibrary动态注入即可生效。适合两类人:一是怀旧游戏玩家(需零配置快速启用),二是 Windows 图形栈开发者(可直接阅读DeviceFuncs.cpp中GetDisplayMode如何从EnumDisplaySettingsExW构建兼容性 ModeList)。
2. 编译与部署:从源码到游戏目录的四步闭环
DDrawCompat 的构建逻辑与传统 DirectX 补丁有本质区别:它不生成ddraw.dll替换系统文件,而是输出ddraw.dll作为应用级代理 DLL,必须置于目标游戏进程的加载路径中。这意味着编译产物的 ABI 兼容性、导入符号完整性、以及模块加载时机,直接决定能否捕获DirectDrawCreate调用。以下步骤基于DDrawCompat-master.zip解压后的源码结构展开,全程使用 Visual Studio 2019+ 和 Windows SDK 10.0.19041+。
2.1 环境准备与项目配置关键项
DDrawCompat 依赖 Windows 10 SDK 中的dxgi.h、d3d11.h及winuser.h的高版本定义,同时需禁用/DELAYLOAD防止d3d11.dll加载失败导致进程崩溃。打开DDrawCompat.sln后,需手动校验三处配置:
- 平台工具集:必须设为
v142(VS2019)或v143(VS2022),v141(VS2017)因缺少D3D_FEATURE_LEVEL_11_0定义会导致DcFunctions.cpp编译失败; - 字符集:设为
使用 Unicode 字符集,否则WinProc.cpp中DefWindowProcW调用会因宽字符处理异常导致窗口消息丢失; - 附加包含目录:在
属性 → C/C++ → 常规 → 附加包含目录中添加$(SolutionDir)include;$(WindowsSDKDir)Include\$(WindowsSDKVersion)\um,确保SurfaceImpl.h能正确解析DXGI_MODE_DESC1结构体。
提示:若编译报错
error C2065: 'DXGI_MODE_DESC1' : undeclared identifier,说明 Windows SDK 版本过低。必须升级至 10.0.19041 或更高版本,该结构体在 10.0.17134 中尚未公开。
2.2 核心模块编译顺序与符号导出控制
DDrawCompat 的 DLL 导出表并非简单__declspec(dllexport),而是通过ddraw.def文件精确控制 127 个 DirectDraw API 的转发逻辑。Hook.cpp中的DetourDirectDrawCreate函数是整个链路的起点,它必须在DllMain的DLL_PROCESS_ATTACH阶段完成CreateWindowExW的 API 拦截,否则RealPrimarySurface.cpp中的窗口消息钩子(如WM_PAINT处理)无法生效。编译时需特别注意:
ddraw.def必须在链接器 → 输入 → 模块定义文件中指定,否则DirectDrawCreate等函数不会出现在导出表中;KernelModeThunks.cpp包含NtQuerySystemInformation的内核态调用封装,需在C/C++ → 代码生成 → 运行库中设为/MT(静态链接 CRT),避免msvcr140.dll版本冲突;PaintHandlers.cpp中的HandlePaintMessage函数负责将BeginPaint/EndPaint转为Present调用,其性能敏感度极高,建议在C/C++ → 优化 → 全局优化中启用/O2。
编译成功后,输出目录下将生成ddraw.dll(Release x64)或ddraw.dll(Release Win32)。注意:x64 版本不能用于 32 位游戏,反之亦然。可通过dumpbin /exports ddraw.dll验证导出函数列表是否包含DirectDrawCreate、DirectDrawCreateEx、DirectDrawEnumerateA等核心入口。
2.3 游戏目录部署与加载验证
将生成的ddraw.dll复制到游戏主程序同目录(如C:\Games\RedAlert2\game.exe同级),而非System32。这是 DDrawCompat 的设计原则:按进程隔离加载,避免全局污染。验证是否生效需分两步:
步骤一:检查 DLL 加载日志
启动游戏前,在游戏目录创建ddraw.ini文件,内容如下:
[Debug] LogToFile=1 LogLevel=3运行游戏后,生成ddraw.log。正常日志首行应为:
[INFO] DDrawCompat v1.10.0 loaded for process game.exe (PID: 12345) [INFO] Hooked DirectDrawCreate at 0x00007FF8A1234567若出现[ERROR] Failed to hook DirectDrawCreate,说明游戏使用了自定义加载器(如某些 Steam 版游戏),需改用AppInit_DLLs注册表方式(见 4.2 节)。
步骤二:验证表面创建流程
在ddraw.log中搜索CreateSurface关键字,正常应看到:
[DEBUG] CreateSurface: width=640, height=480, format=DDPF_RGB, flags=DDSCAPS_OFFSCREENPLAIN [DEBUG] RealPrimarySurface::CreateSurface -> DXGI_FORMAT_B8G8R8X8_UNORM这表明SurfaceImpl.cpp已成功将旧版DDSURFACEDESC结构映射为 DXGI 格式,且未触发E_FAIL错误。
注意:部分游戏(如《帝国时代2 HD》)会调用
DirectDrawCreateEx并传入IID_IDirectDraw7,此时需确认ddraw.def中已导出该函数。若日志显示Unknown interface IID: {...},说明DcFunctions.cpp中的QueryInterface实现未覆盖对应 GUID,需手动添加分支。
3. 深度解析:SurfaceImpl 与 RealPrimarySurface 的内存模型重构
DDrawCompat 的核心竞争力在于它没有简单地将IDirectDrawSurface封装为ID3D11Texture2D,而是构建了一套双缓冲表面生命周期管理器,其内存模型完全脱离传统 DirectDraw 的线性帧缓冲假设。SurfaceImpl.cpp和RealPrimarySurface.cpp是这套模型的执行主体,二者分工明确:前者处理表面创建、锁定、释放等通用操作;后者专精于主显示表面(Primary Surface)的窗口绑定与垂直同步策略。
3.1 SurfaceImpl 的表面创建与格式映射逻辑
SurfaceImpl::CreateSurface函数接收DDSURFACEDESC2参数,其中ddsCaps.dwCaps标志决定表面类型(DDSCAPS_OFFSCREENPLAIN、DDSCAPS_PRIMARYSURFACE等)。关键逻辑在于GetDXGIFormatFromDDPixelFormat的转换表:
| DDPIXELFORMAT.dwRGBBitCount | DDPIXELFORMAT.dwFourCC | 映射 DXGI_FORMAT | 说明 |
|---|---|---|---|
| 16 | 0 | DXGI_FORMAT_B5G6R5_UNORM | 经典 16-bit RGB |
| 24 | 0 | DXGI_FORMAT_R8G8B8A8_UNORM | 24-bit RGB + alpha 填充 |
| 32 | 'ARGB' | DXGI_FORMAT_B8G8R8A8_UNORM | Alpha 通道启用 |
| 0 | 'YUY2' | DXGI_FORMAT_YUY2 | 视频解码专用 |
该映射直接影响Blt和Flip的性能。例如,当游戏请求DDPF_YUV格式时,DDrawCompat 不会尝试软件 YUV 转 RGB,而是直接创建DXGI_FORMAT_YUY2纹理,并在Present时由 GPU 硬件解码。若映射失败(如请求DDPF_LUMINANCE),则返回DDERR_INVALIDPIXELFORMAT,触发游戏降级逻辑。
3.2 RealPrimarySurface 的窗口绑定与垂直同步机制
RealPrimarySurface的核心职责是将IDirectDrawSurface7绑定到 HWND,并控制Flip行为。其Flip实现不调用IDXGISwapChain::Present,而是:
- 调用
IDXGISurface::CopyResource将后台缓冲区复制到前台缓冲区; - 若
dwFlags & DDFLIP_WAITFORVSYNC,则调用WaitForVerticalBlank查询DXGI_SWAP_CHAIN_DESC1.Scaling; - 最终通过
SetWindowPos强制窗口重绘,触发WM_PAINT消息。
这种设计规避了 DXGI SwapChain 的全屏独占限制,使窗口化游戏也能获得稳定 VSync。DisplayMode.cpp中的GetDisplayMode函数会枚举当前显示器支持的所有DXGI_MODE_DESC,并筛选出与DDSURFACEDESC2.dwWidth/dwHeight匹配的模式,确保SetDisplayMode调用能返回DD_OK。
3.3 PaintHandlers 的消息循环注入原理
PaintHandlers.cpp解决的是最棘手的“菜单绘制不全”问题。传统方案试图拦截BeginPaint,但 DDrawCompat 采用更底层的SetWindowLongPtrW(GWL_WNDPROC)替换窗口过程。WinProc.cpp中的NewWndProc函数在收到WM_PAINT时:
- 调用
GetUpdateRect获取脏矩形区域; - 从
RealPrimarySurface获取当前前台缓冲区指针; - 使用
StretchBlt将脏区域像素复制到 HDC; - 最后调用原窗口过程处理非图形消息。
此机制保证了即使游戏未调用Blt,仅依赖 GDI 绘制菜单,也能被正确捕获。日志中HandlePaintMessage: dirty rect=(0,0,640,480)即表示该路径已激活。
4. 故障诊断:从日志定位到注册表级强制注入
当标准部署失效时(如游戏启动后无ddraw.log、画面仍闪烁),需进入深度诊断。DDrawCompat 的错误反馈高度依赖日志级别和加载时机,以下方法覆盖 95% 的常见故障。
4.1 日志级别调优与关键错误码解读
ddraw.ini中的LogLevel参数决定日志详细程度:
LogLevel=1:仅记录[INFO]和[ERROR](默认);LogLevel=2:增加[WARN](如Surface lock timeout);LogLevel=3:启用[DEBUG](显示每个Blt的源/目标矩形);LogLevel=4:开启[VERBOSE](打印Hook.cpp中的 API 地址偏移)。
常见错误码及对策:
| 错误码 | 日志示例 | 根本原因 | 解决方案 |
|---|---|---|---|
DDERR_GENERIC | [ERROR] CreateSurface failed: 0x80004005 | IDXGIFactory::CreateSwapChain失败 | 检查显卡驱动是否支持 DXGI 1.2+,更新至最新版 |
DDERR_UNSUPPORTED | [ERROR] SetDisplayMode not supported | 请求的分辨率超出显示器物理范围 | 修改游戏配置文件,将ScreenWidth=1024改为800 |
DDERR_SURFACELOST | [WARN] Surface lost, restoring... | 窗口最小化后资源被 DXGI 回收 | 在SurfaceImpl.cpp中启用D3D11_CREATE_DEVICE_SINGLETHREADED标志 |
提示:若日志中频繁出现
Failed to get DXGI adapter,说明DeviceFuncs.cpp的EnumerateAdapters未找到有效 GPU。可在ddraw.ini中添加[Device] AdapterIndex=0强制指定主显卡。
4.2 AppInit_DLLs 注册表注入(绕过游戏加载器)
某些游戏(如 Origin 平台的《命令与征服》重制版)使用自定义 PE 加载器,会跳过LoadLibrary的常规 DLL 搜索路径。此时需启用 Windows 的AppInit_DLLs机制:
- 以管理员身份运行
regedit; - 导航至
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows; - 创建字符串值
AppInit_DLLs,数据设为C:\Path\To\ddraw.dll(必须使用绝对路径); - 创建 DWORD 值
LoadAppInit_DLLs,数据设为1; - 重启 Explorer 进程(任务管理器 → 重启
explorer.exe)。
该机制会在每个新进程创建时调用LoadLibrary,但存在安全限制:必须启用RequireSignedAppInit_DLLs=0(同路径下新建 DWORD),否则未签名 DLL 会被拒绝加载。DDrawCompat 的ddraw.dll默认无签名,故此步不可省略。
4.3 表面锁定超时的内存对齐修复
SurfaceImpl::Lock函数在dwFlags & DDLOCK_WRITEONLY时,会分配ID3D11Buffer作为 CPU 可写缓冲区。若游戏频繁调用Lock/Unlock,可能触发D3D11_MAP_WRITE_DISCARD的内存碎片问题。解决方案是在SurfaceImpl.cpp的Lock函数中插入对齐检查:
// 在 Lock 函数内,获取 pLockedRect->pBits 后添加 uintptr_t ptr = reinterpret_cast<uintptr_t>(pLockedRect->pBits); if (ptr % 16 != 0) { // 强制对齐到 16 字节边界,避免 SSE 指令异常 pLockedRect->pBits = reinterpret_cast<BYTE*>((ptr + 15) & ~0xF); }此修改可解决《星际争霸》在高分辨率下Lock返回DDERR_INVALIDPARAMS的问题,因为其汇编代码依赖 16 字节对齐的像素地址。
5. 进阶技巧:动态调整 Present 策略与多显示器适配
DDrawCompat 的Present行为默认采用DXGI_PRESENT_DO_NOT_WAIT,适用于大多数窗口化游戏。但在全屏独占模式下,部分游戏(如《半条命》GoldSrc 引擎)要求严格的DXGI_PRESENT_WAIT_FOR_VSYNC以避免输入延迟。此时需通过ddraw.ini动态控制,而非重新编译。
5.1 Present 模式热切换配置
在ddraw.ini中添加[Present]区段:
[Present] Mode=1 Interval=1 AllowTearing=0参数含义:
Mode=0:DXGI_PRESENT_DO_NOT_WAIT(默认,最低延迟);Mode=1:DXGI_PRESENT_WAIT_FOR_VSYNC(强制等待垂直同步);Mode=2:DXGI_PRESENT_RESTART(仅用于调试,触发 DXGI 设备重置);Interval:VSync 倍数(1=60Hz,2=30Hz),需硬件支持;AllowTearing=1:启用撕裂模式(DXGI_PRESENT_ALLOW_TEARING),需在CreateSwapChain时启用DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING。
注意:
AllowTearing=1仅在Mode=0下生效,且要求显示器支持 Adaptive Sync(FreeSync/G-Sync)。
5.2 多显示器主屏识别与 Surface 分辨率适配
当游戏运行在扩展显示器(非主屏)时,RealPrimarySurface::GetDisplayMode可能返回主屏分辨率,导致画面拉伸。解决方案是修改DisplayMode.cpp中的GetAdapterOutput函数,使其根据游戏窗口 HWND 定位实际显示器:
// 替换原 GetAdapterOutput 函数 IDXGIOutput* GetAdapterOutput(HWND hwnd) { HMONITOR hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY); MONITORINFOEX mi = {}; mi.cbSize = sizeof(mi); GetMonitorInfo(hMonitor, &mi); // 遍历所有 DXGI 输出,匹配 mi.szDevice for (UINT i = 0; i < adapter->GetOutputCount(); ++i) { IDXGIOutput* output; if (SUCCEEDED(adapter->EnumOutputs(i, &output))) { DXGI_OUTPUT_DESC desc = {}; output->GetDesc(&desc); if (wcscmp(desc.DeviceName, mi.szDevice) == 0) { return output; } output->Release(); } } return nullptr; }此修改确保SetDisplayMode(1920,1080)在副屏上创建 1920×1080 表面,而非主屏的 3840×2160。
5.3 TS-DDraw 兼容性补丁的特殊处理
针对摘要中提到的 TS-DDraw(TeamSpeak 的 DirectDraw 模块),其菜单绘制异常源于WM_NCPAINT消息未被PaintHandlers拦截。需在WinProc.cpp的NewWndProc中添加:
case WM_NCPAINT: { // TS-DDraw 依赖非客户区绘制,需主动触发 HRGN hRgn = CreateRectRgn(0, 0, 0, 0); GetWindowRgn(hwnd, hRgn); ValidateRect(hwnd, nullptr); DeleteObject(hRgn); break; }此代码强制刷新非客户区,解决 TS-DDraw 界面按钮消失问题。最终效果可通过运行 TeamSpeak 3 并观察托盘菜单是否完整显示来验证。
本文还有配套的精品资源,点击获取