找回密码
 立即注册
首页 业界区 业界 DX12-1-DirectX3D初始化

DX12-1-DirectX3D初始化

钦娅芬 2025-10-25 23:05:01
1.png

什么是 Direct3D 12?
DirectX 12 引入了 Direct3D 的下一个版本,即 DirectX 的核心 3D 图形 API。
此版本的 Direct3D 比任何以前的版本更快、更高效。
Direct3D 12 可实现更丰富的场景、更多的对象、更复杂的效果,以及充分利用现代 GPU 硬件。
若要为 Windows 10 和 Windows 10 移动版编写 3D 游戏和应用,必须了解 Direct3D 12 技术的基础知识,以及如何准备在游戏和应用中使用它。
D3DApp初始化

Windows函数调用过程

调用者(caller) → 参数准备 → call指令 → 被调用者(callee)执行 → 返回 → 栈清理
栈的基本原理
想象栈就像一个临时工作台:

  • 调用函数时:把参数"放"到工作台上
  • 函数执行时:从工作台"拿"参数使用
  • 函数结束后:需要把工作台"清理干净"
规则:

  • 调用Windows API时:不用管清理,Windows会处理
  • 写回调函数时:必须用__stdcall,并在返回时清理栈
  • 调用C运行时函数时:编译器会自动帮"我"清理栈
  • 写C++成员函数时:编译器自动处理,不用操心
  • "我"调用Windows → Windows清理
  • Windows调用"我" → "我"清理
  • "我"调用C运行时 → "我"清理(编译器帮忙)
  • "我"的C++方法 → "我"清理(编译器自动处理)
如果不清理会怎样?
  1. void FunctionA(int x, int y) {
  2.     // 使用x,y...
  3. }
  4. void FunctionB(int a, int b, int c) {
  5.     // 使用a,b,c...
  6. }
  7. int main() {
  8.     FunctionA(1, 2);    // 栈上放了 [1, 2]
  9.     // 如果不清理,栈上还有 [1, 2]
  10.     FunctionB(3, 4, 5); // 栈变成 [3, 4, 5, 1, 2] ← 混乱!
  11. }
复制代码
清理栈就是调整栈指针(ESP),让栈回到函数调用前的状态:
  1. 调用前:ESP指向位置X
  2. 调用时:push参数 → ESP移动到位置Y (Y < X)
  3. 清理后:ESP回到位置X
复制代码
  1. // ✅ 正确:调用Windows API(不用管清理)
  2. MessageBox(NULL, "Text", "Title", MB_OK);
  3. // ✅ 正确:写回调函数(用CALLBACK宏)
  4. LRESULT CALLBACK MyCallback(HWND, UINT, WPARAM, LPARAM);
  5. // ✅ 正确:调用可变参数函数(编译器自动清理)
  6. printf("Values: %d %d", x, y);
  7. // ❌ 错误:回调函数不用__stdcall
  8. LRESULT MyBadCallback(HWND, UINT, WPARAM, LPARAM);  // 会崩溃!
复制代码
机制举例

__cdecl - "我请客,我收拾"
  1. // "我"调用printf(Windows的C运行时库)
  2. printf("Count: %d %d", 10, 20);
  3. ; "我"调用printf后
  4. push offset text    ; 参数3
  5. push value          ; 参数2  
  6. push num            ; 参数1
  7. push offset format  ; 参数0
  8. call printf
  9. add esp, 16         ; ⭐"我"清理栈:4个参数×4字节
复制代码
分工:
"我":放参数 + 清理栈
Windows/CRT:只用参数,不清理
__stdcall - "Windows服务,Windows收拾"
  1. // "我"调用Windows API
  2. CreateWindow(className, title, style, x, y, width, height, ...);
  3. ; "我"调用CreateWindow后
  4. push 0              ; 参数11
  5. push hInstance      ; 参数10
  6. ; ... 更多参数 ...
  7. push className      ; 参数1
  8. call CreateWindowEx
  9. ; ⭐没有add esp! Windows会自己清理
复制代码
分工:
"我":放参数
Windows:用参数 + 清理栈
__stdcall回调 - "Windows调用,我收拾"
  1. // Windows调用"我"的回调
  2. LRESULT CALLBACK MyWindowProc(HWND, UINT, WPARAM, LPARAM);
  3. ; Windows调用"我"的WndProc
  4. _WndProc@16:
  5.     ; "我"的处理逻辑...
  6.     ret 16          ; ⭐"我"清理16字节参数
复制代码
分工:
Windows:放参数
"我":用参数 + 清理栈
__fastcall - "快速服务,Windows收拾"
  1. // 假设是Windows的某个性能API
  2. int __fastcall FastAPI(int a, int b, int c);
复制代码
分工:
"我":前两个参数放寄存器,其余放栈
Windows:用参数 + 清理栈上的参数

  • 性能优化:寄存器比内存快
  • Windows负责清理,保持API一致性
__thiscall - "对象方法,我收拾"
  1. // "我"的C++类
  2. class MyClass {
  3. public:
  4.     void Method(int param);  // 自动__thiscall
  5. };
复制代码
分工:
"我":this放寄存器,参数放栈
"我":用参数 + 清理栈

  • C++对象模型,"我"完全控制自己的类
  • 编译器自动处理,对"我"透明
创建一个Windows窗口

2.png
  1. LRESULT CALLBACK MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
  2. {
  3.         // Forward hwnd on because we can get messages (e.g., WM_CREATE)
  4.         // before CreateWindow returns, and thus before mhMainWnd is valid.
  5.     //转发消息给 D3DApp::GetApp()->MsgProc()
  6.     return D3DApp::GetApp()->MsgProc(hwnd, msg, wParam, lParam);
  7. }
  8. bool D3DApp::InitMainWindow()
  9. {
  10.     // 设置窗口类属性
  11.     WNDCLASS wc;
  12.     // 窗口尺寸变化时重绘 ,CS_HREDRAW 宽度改变时重绘 ,CS_VREDRAW 高度改变时重绘
  13.     wc.style = CS_HREDRAW | CS_VREDRAW;      
  14.      // 建立消息处理回调机制,所有发送到此窗口的消息都由该函数处理 , 在代码中,MainWndProc 转发消息给 D3DApp::GetApp()->MsgProc()              
  15.     wc.lpfnWndProc = MainWndProc;   
  16.     // 应用程序实例句柄                     
  17.     wc.hInstance = mhAppInst;   
  18.     // 额外的类内存 ,用于存储自定义数据,这里设为0表示不需要                          
  19.     wc.cbClsExtra = 0;   
  20.      // 额外的窗口内存字节数 ,用于存储自定义数据,这里设为0表示不需要                                 
  21.     wc.cbWndExtra = 0;                  
  22.     // 加载系统预定义的应用程序图标                  
  23.     wc.hIcon = LoadIcon(0, IDI_APPLICATION);     
  24.     // 加载系统预定义的箭头光标         
  25.     wc.hCursor = LoadCursor(0, IDC_ARROW);               
  26.     // 窗口背景画刷 NULL_BRUSH 表示不绘制背景,适合DirectX应用(因为DirectX会完全覆盖客户区).
  27.     避免GDI与DirectX绘制冲突
  28.     wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
  29.      // 设为0表示此窗口类没有菜单
  30.     wc.lpszMenuName = 0;         
  31.     // 窗口类名称,用于在系统中唯一标识此窗口类                        
  32.     wc.lpszClassName = L"MainWnd";                       
  33.     //将定义好的窗口类注册到操作系统中. Windows机制:系统内部维护一个窗口类表;注册成功后,该类可用于创建多个窗口实例;失败通常是因为类名重复或参数无效.
  34.         if( !RegisterClass(&wc) )
  35.         {
  36.                 MessageBox(0, L"RegisterClass Failed.", 0, 0);
  37.                 return false;
  38.         }
  39.         // Compute window rectangle dimensions based on requested client area dimensions.
  40.     /*
  41.       窗口尺寸计算:
  42.       客户区 (Client Area):应用程序实际可绘制内容的区域(mClientWidth × mClientHeight)
  43.       窗口矩形 (Window Rect):包含标题栏、边框、菜单等的完整窗口
  44.       AdjustWindowRect 根据窗口样式自动计算转换关系 .
  45.       
  46.       ┌─────────────────────────┐
  47.       │ 标题栏 (非客户区)        │
  48.       ├────────────┬────────────┤
  49.       │            │            │
  50.       │            │            │
  51.       │  客户区    │  滚动条    │
  52.       │            │ (非客户区) │
  53.       │            │            │
  54.       └────────────┴────────────┘
  55.       原始客户区: (0,0) 到 (800,600)
  56.       AdjustWindowRect 调整后: 可能变成 (-8,-31) 到 (808,631)
  57.       最终窗口尺寸: 816 × 662 像素
  58.      */
  59.         RECT R = { 0, 0, mClientWidth, mClientHeight };
  60.     AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
  61.         int width  = R.right - R.left;
  62.         int height = R.bottom - R.top;
  63.     /* 窗口创建:
  64.        L"MainWnd":窗口类名,必须与注册的类名一致 | WS_OVERLAPPEDWINDOW:窗口样式,包含标题栏、系统菜单、最小化/最大化按钮、可调整边框
  65.        CW_USEDEFAULT, CW_USEDEFAULT:窗口初始位置,让系统自动选择
  66.       Windows创建机制:系统分配内部窗口数据结构 | 发送 WM_CREATE 等初始化消息 | 返回唯一的窗口句柄 mhMainWnd
  67.     */
  68.         mhMainWnd = CreateWindow(L"MainWnd", mMainWndCaption.c_str(),
  69.                 WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
  70.         if( !mhMainWnd )
  71.         {
  72.                 MessageBox(0, L"CreateWindow Failed.", 0, 0);
  73.                 return false;
  74.         }
  75.     //改变窗口可视状态为显示
  76.         ShowWindow(mhMainWnd, SW_SHOW);
  77.     //强制发送 WM_PAINT 消息,立即重绘窗口
  78.         UpdateWindow(mhMainWnd);
  79.         return true;
  80. }
复制代码
3.png

MainWndProc 相关API
https://learn.microsoft.com/zh-cn/windows/win32/winmsg/window-notifications
https://learn.microsoft.com/zh-cn/windows/win32/api/_winmsg/
PeekMessage: 检查线程消息队列中是否有消息,如果有消息 将消息复制到提供的msg结构中,PM_REMOVE标志表示从队列中移除该消息
  1. LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
  2. {
  3.     switch(msg)
  4.     {
  5.         if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
  6.         {
  7.             //...
  8.         }
  9.         // 各种消息处理 case
  10.         // ...
  11.     }
  12.     return DefWindowProc(hwnd, msg, wParam, lParam);
  13. }
复制代码
窗口消息

WM_ACTIVATE- 窗口激活状态变化
  1. case WM_ACTIVATE:
  2. //LOWORD(wParam):低16位表示激活状态
  3. //WA_INACTIVE:窗口变为非活动状态
  4. //失活时暂停,激活时恢复,智能暂停机制提升系统整体性能
  5.     if( LOWORD(wParam) == WA_INACTIVE )
  6.     {
  7.         mAppPaused = true;
  8.         mTimer.Stop();
  9.     }
  10.     else
  11.     {
  12.         mAppPaused = false;
  13.         mTimer.Start();
  14.     }
  15.     return 0;
复制代码
WM_SIZE- 窗口尺寸变化
LOWORD 和 HIWORD 是 Windows API 中的宏定义,用于从一个 32 位值中提取低 16 位和高 16 位部分。
  1. case WM_SIZE:
  2.     mClientWidth  = LOWORD(lParam);  // 新宽度
  3.     mClientHeight = HIWORD(lParam);  // 新高度
复制代码
•        低 16 位 (LOWORD) 存储新宽度(以像素为单位)
•        高 16 位 (HIWORD) 存储新高度(以像素为单位)
SIZE_MINIMIZED最小化情况,完全暂停应用程序,不进行任何渲染
SIZE_MAXIMIZED最大化情况,立即调整D3D资源适应新尺寸
SIZE_RESTORED恢复情况(最复杂)
  1. else if( wParam == SIZE_RESTORED )
  2. {
  3.     // 从最小化恢复
  4.     if( mMinimized )
  5.     {
  6.         mAppPaused = false;
  7.         mMinimized = false;
  8.         OnResize();
  9.     }
  10.     // 从最大化恢复  
  11.     else if( mMaximized )
  12.     {
  13.         mAppPaused = false;
  14.         mMaximized = false;
  15.         OnResize();
  16.     }
  17.     // 用户正在拖拽调整大小
  18.    //拖拽时不立即调整,避免频繁资源重建
  19.     else if( mResizing )
  20.     {
  21.         // 故意不调用OnResize() - 性能优化
  22.     }
  23.     // 程序化尺寸改变
  24.     else
  25.     {
  26.         OnResize();
  27.     }
  28. }
复制代码
WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE - 调整大小过程管理
WM_ENTERSIZEMOVE:开始拖拽,设置标志位暂停调整
WM_EXITSIZEMOVE:结束拖拽,清除标志位并执行最终调整
  1. case WM_ENTERSIZEMOVE:
  2.     mAppPaused = true;
  3.     mResizing  = true;
  4.     mTimer.Stop();
  5.     return 0;
  6. case WM_EXITSIZEMOVE:
  7.     mAppPaused = false;
  8.     mResizing  = false;
  9.     mTimer.Start();
  10.     OnResize();  // 拖拽结束后一次性调整
  11.     return 0;
复制代码
WM_DESTROY - 窗口销毁
发送 WM_QUIT 到消息队列,导致 Run() 中的主循环退出
PostQuitMessage(0) → 系统消息队列 → 线程消息队列 → PeekMessage() → msg变量
Windows消息系统架构
系统消息队列 (全局)

线程消息队列 (每个线程独立)

PeekMessage/GetMessage (应用程序检索)
  1. case WM_DESTROY:
  2.     PostQuitMessage(0);
  3.     return 0;
复制代码
  1. int D3DApp::Run()
  2. {
  3.         MSG msg = {0};
  4.         mTimer.Reset();
  5.         while(msg.message != WM_QUIT)
  6.         {
  7.            if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
  8.           {
  9.             TranslateMessage( &msg );
  10.             DispatchMessage( &msg );
  11.           }
  12.         return (int)msg.wParam;
  13. }
复制代码
WM_MENUCHAR- 菜单字符处理
处理 Alt+Enter 等组合键,避免系统蜂鸣声
MNC_CLOSE表示关闭菜单而不发出蜂鸣
  1. case WM_MENUCHAR:
  2.     return MAKELRESULT(0, MNC_CLOSE);
复制代码
WM_GETMINMAXINFO- 窗口尺寸限制
设置窗口最小尺寸为 200×200,防止窗口过小导致渲染问题
  1. case WM_GETMINMAXINFO:
  2.     ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
  3.     ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
  4.     return 0;
复制代码
鼠标消息处理
GET_X_LPARAM(lParam):从 lParam 提取 X 坐标
GET_Y_LPARAM(lParam):从 lParam 提取 Y 坐标
wParam:按键状态(Ctrl、Shift 等)
设计模式:使用虚函数提供扩展点,派生类可重写鼠标处理
  1. case WM_LBUTTONDOWN:
  2. case WM_MBUTTONDOWN:
  3. case WM_RBUTTONDOWN:
  4.     OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
  5.     return 0;
  6. // 类似的鼠标抬起和移动处理
复制代码
WM_KEYUP- 键盘按键处理
ESC键:优雅退出应用程序
F2键:运行时切换4倍多重采样抗锯齿状态
  1. case WM_KEYUP:
  2.     if(wParam == VK_ESCAPE)
  3.     {
  4.         PostQuitMessage(0);  // ESC键退出
  5.     }
  6.     else if((int)wParam == VK_F2)
  7.         Set4xMsaaState(!m4xMsaaState);  // F2切换抗锯齿
  8.     return 0;
复制代码
初始化Direct3D

COM

COM(Component Object Model)是 Microsoft 的组件对象模型,它是 Windows 生态系统的基石
COM 的核心思想是二进制级别的兼容性:

  • 不同编译器生成的 COM 组件可以互操作
  • 不同语言(C++、C#、VB、Delphi)可以互相调用
  • 进程内(DLL)和进程外(EXE)组件统一模型
IUnknown
  1. // IUnknown 是每个 COM 接口都必须继承的基接口
  2. class IUnknown {
  3. public:
  4.     virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
  5.     // 当新的代码段获得接口指针时,必须调用 AddRef() 来增加引用计数。
  6.     virtual ULONG AddRef() = 0;
  7.     // 当不再需要接口指针时调用,减少引用计数。当计数为 0 时,对象自我销毁。
  8.     virtual ULONG Release() = 0;
  9. };
复制代码
QueryInterface - 接口查询
  1. // 检查对象是否支持特定接口,如果支持则返回该接口指针
  2. virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
  3. // 假设我们有一个 IUnknown 指针
  4. IUnknown* pUnknown = ...;
  5. // 查询 ID3D12Device 接口
  6. ID3D12Device* pDevice = nullptr;
  7. HRESULT hr = pUnknown->QueryInterface(IID_ID3D12Device, (void**)&pDevice);
  8. if (SUCCEEDED(hr)) {
  9.     // 可以使用 ID3D12Device 接口
  10.     pDevice->CreateCommandQueue(...);
  11.     pDevice->Release();  // 使用完后释放
  12. }
复制代码
COM 对象生命周期管理
引用计数规则
  1. class ReferenceCountingExample {
  2. public:
  3.     void CorrectReferenceCounting() {
  4.         // 场景1:创建新对象
  5.         IUnknown* pObj = CreateNewObject();  // 引用计数 = 1
  6.         
  7.         // 场景2:复制指针
  8.         IUnknown* pCopy = pObj;
  9.         pCopy->AddRef();  // 现在引用计数 = 2
  10.         
  11.         // 场景3:使用完副本
  12.         pCopy->Release(); // 引用计数 = 1
  13.         
  14.         // 场景4:使用完原始指针
  15.         pObj->Release();  // 引用计数 = 0 → 对象销毁
  16.     }
  17.    
  18.     void CommonMistakes() {
  19.         // 错误:忘记 AddRef
  20.         IUnknown* pOriginal = CreateNewObject();  // 计数 = 1
  21.         IUnknown* pCopy = pOriginal;              // 计数还是 1!
  22.         pOriginal->Release();                     // 计数 = 0 → 对象销毁!
  23.         // pCopy 现在指向已销毁的对象!
  24.         
  25.         // 错误:忘记 Release - 内存泄漏!
  26.         IUnknown* pObj = CreateNewObject();  // 计数 = 1
  27.         // 使用对象...
  28.         // 忘记调用 pObj->Release() - 对象永远不会销毁!
  29.     }
  30. };
复制代码
COM接口定义
接口标识符 (IID)
每个 COM 接口都有一个全局唯一标识符 (GUID):
  1. // IID 定义示例
  2. // {接口名称}-{唯一标识符}
  3. DEFINE_GUID(IID_IMyInterface,
  4.     0x12345678, 0x1234, 0x1234, 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc);
复制代码
COM 在 DirectX 12 中的应用
DirectX COM 接口使用
  1. class D3D12COMUsage {
  2. public:
  3.     void TypicalD3D12Usage() {
  4.         // 1. 创建 DXGI 工厂
  5.         IDXGIFactory4* pFactory = nullptr;
  6.         CreateDXGIFactory1(IID_PPV_ARGS(&pFactory));
  7.         
  8.         // 2. 创建设备
  9.         ID3D12Device* pDevice = nullptr;
  10.         D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&pDevice));
  11.         
  12.         // 3. 创建命令队列
  13.         ID3D12CommandQueue* pCommandQueue = nullptr;
  14.         D3D12_COMMAND_QUEUE_DESC queueDesc = {};
  15.         pDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&pCommandQueue));
  16.         
  17.         // 使用对象...
  18.         
  19.         // 4. 按创建顺序的逆序释放
  20.         pCommandQueue->Release();
  21.         pDevice->Release();
  22.         pFactory->Release();
  23.     }
  24. };
复制代码
现代 C++ COM 编程
在现代 DirectX 12 编程中,虽然我们使用 ComPtr 等智能指针来简化 COM 编程,但理解底层的 COM 机制对于调试和理解系统行为仍然至关重要。
  1. // 使用 ComPtr 简化 COM 编程
  2. void ModernCOMProgramming()
  3. {
  4.     // 自动管理引用计数
  5.     ComPtr<IDXGIFactory4> factory;
  6.     ComPtr<ID3D12Device> device;
  7.     ComPtr<ID3D12CommandQueue> commandQueue;
  8.    
  9.     // 创建对象(自动管理引用计数)
  10.     CreateDXGIFactory1(IID_PPV_ARGS(&factory));
  11.     D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device));
  12.    
  13.     D3D12_COMMAND_QUEUE_DESC queueDesc = {};
  14.     device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue));
  15.    
  16.     // 不需要手动调用 Release() - ComPtr 自动处理!
  17.    
  18.     // 接口查询也很简单
  19.     ComPtr<IDXGIDevice> dxgiDevice;
  20.     device.As(&dxgiDevice);  // 自动 QueryInterface
  21. }
复制代码
ComPtr

ComPtr 是 Microsoft::WRL (Windows Runtime C++ Template Library) 中的智能指针模板类,专门用于管理 COM 对象的生命周期。
详情:https://learn.microsoft.com/zh-cn/cpp/cppcx/wrl/comptr-class?view=msvc-170
核心功能:

  • 自动引用计数管理
  • 异常安全的资源清理
  • 简化 COM 接口指针操作
  1. #include <wrl.h>
  2. using namespace Microsoft::WRL;
  3. // 基本用法
  4. ComPtr<ID3D12Device> device;
复制代码
  1. // 传统方式 - 容易出错
  2. ID3D12Device* pDevice = nullptr;
  3. ID3D12CommandQueue* pQueue = nullptr;
  4. HRESULT hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0,
  5.                               IID_PPV_ARGS(&pDevice));
  6. if (SUCCEEDED(hr)) {
  7.     // 创建命令队列
  8.     hr = pDevice->CreateCommandQueue(..., IID_PPV_ARGS(&pQueue));
  9. }
  10. // 必须手动释放 - 容易忘记!
  11. if (pQueue) pQueue->Release();
  12. if (pDevice) pDevice->Release();  // 容易漏掉!
复制代码
  1. // ComPtr 方式 - 自动管理生命周期
  2. ComPtr<ID3D12Device> device;
  3. ComPtr<ID3D12CommandQueue> queue;
  4. ThrowIfFailed(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0,
  5.                                IID_PPV_ARGS(&device)));
  6. ThrowIfFailed(device->CreateCommandQueue(..., IID_PPV_ARGS(&queue)));
  7. // 不需要手动 Release() - 自动处理!
复制代码
ComPtr内部维护一个指针
  1. template <typename T>
  2. class ComPtr
  3. {
  4. public:
  5.     using InterfaceType = T ;
  6. protected:
  7.     InterfaceType *ptr_;
  8. public:
  9.     T* Get() const throw()
  10.     {
  11.         return ptr_;
  12.     }
  13.     T** GetAddressOf() throw()
  14.     {
  15.         return &ptr_;
  16.     }
  17.      InterfaceType* operator->() const throw()
  18.     {
  19.         return ptr_;
  20.     }
  21. }
复制代码
异常检测

ThrowIfFailed宏
  1. #define FAILED(hr) (((HRESULT)(hr)) < 0)
  2. #define ThrowIfFailed(x)                                              \
  3. {                                                                     \
  4.     HRESULT hr__ = (x);                                               \
  5.     std::wstring wfn = AnsiToWString(__FILE__);                       \
  6.     if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
  7. }
  8. DxException(hr__,      // 1. HRESULT 错误代码
  9.             L#x,       // 2. 失败的函数调用表达式
  10.             wfn,       // 3. 文件名
  11.             __LINE__); // 4. 行号
复制代码
FILE:预定义宏,展开为当前源文件的完整路径(ANSI 字符串)
DxException
  1. class DxException
  2. {
  3. public:
  4.     DxException() = default;
  5.     DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
  6.     std::wstring ToString()const;
  7.     HRESULT ErrorCode = S_OK;
  8.     std::wstring FunctionName;
  9.     std::wstring Filename;
  10.     int LineNumber = -1;
  11. };
  12. DxException::DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber) :
  13.     ErrorCode(hr),FunctionName(functionName),Filename(filename),LineNumber(lineNumber)
  14. {}
复制代码
在WinMain中使用:
  1. int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
  2.                                    PSTR cmdLine, int showCmd)
  3. {
  4.     try
  5.     {
  6.         InitDirect3DApp theApp(hInstance);
  7.         if(!theApp.Initialize())
  8.             return 0;
  9.         return theApp.Run();
  10.     }
  11.     catch(DxException& e)
  12.     {
  13.         MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
  14.         return 0;
  15.     }
  16. }
复制代码
Debug
  1. ComPtr<ID3D12Debug> debugController;
  2. ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
  3. debugController->EnableDebugLayer();
复制代码
ID3D12Debug 是一个接口,专门用于启用和控制 Direct3D 12 的调试功能
它不是渲染管线的一部分,而是开发时的辅助工具
主要功能:

  • 错误检查:验证参数是否正确,捕获非法调用
  • 资源追踪:检测资源泄漏和错误使用
  • 状态验证:确保资源状态转换正确
  • 性能分析:识别性能瓶颈
  • 详细输出:在 Visual Studio 输出窗口显示详细信息
DXGIFactory

工厂模式:用于创建其他图形相关对象的"工厂"
DXGIirectX Graphics Infrastructure
  1. ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
  2. 应用程序
  3.     ↓ 使用
  4. IDXGIFactory4 (图形工厂)
  5.     ├── 创建交换链 (IDXGISwapChain)
  6.     ├── 枚举适配器 (显卡)
  7.     └── 查询显示模式
复制代码
ID3D12Device

该设备是一个虚拟适配器,使用它来创建命令列表、管道状态对象、根签名、命令分配器、命令队列、围栏、资源、描述符和描述符堆。
计算机可能具有多个 GPU,因此可以使用 DXGI 工厂枚举设备,并查找第一个功能级别 11(与 direct3d 12 兼容)的设备
找到要使用的适配器索引后,通过调用 D3D12CreateDevice() 创建设备。
IID_PPV_ARGS
  1. #define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
复制代码
__uuidof 运算符:Microsoft 特有的编译器扩展,在编译时获取 COM 接口的 GUID/IID,不需要运行时查询
  1. &device           // 类型: ComPtr<ID3D12Device>*
  2. *(&device)        // 类型: ComPtr<ID3D12Device>&  
  3. **(&device)       // 类型: ID3D12Device*
  4. __uuidof(**(&device))
  5. → __uuidof(ID3D12Device*)
  6. → 编译时得到 ID3D12Device 的 IID
复制代码
IID_PPV_ARGS_Helper 函数的参数是ComPtrRef 而不是ComPtr.
ComPtrRef 包装ComPtr的引用,用于安全地传递 COM 接口指针
返回值:void** COM 方法通常需要 void** 参数来接收接口指针
  1. //COM 方法通常需要 void** 参数来接收接口指针, 例如:
  2. HRESULT QueryInterface(REFIID riid, void** ppvObject);
  3. HRESULT D3D12CreateDevice(..., REFIID riid, void** ppDevice);
  4. // Overloaded global function to provide to IID_PPV_ARGS that support Details::ComPtrRef
  5. template<typename T>
  6. void** IID_PPV_ARGS_Helper(_Inout_ ::Microsoft::WRL::Details::ComPtrRef<T> pp) throw()
  7. {
  8.     static_assert(__is_base_of(IUnknown, typename T::InterfaceType), "T has to derive from IUnknown");
  9.     return pp;
  10. }
复制代码

  • 确保模板参数 T 的接口类型继承自 IUnknown
  • 所有 COM 接口都必须继承自 IUnknown
  • 编译时失败,避免运行时错误
  1. // 错误用法:非 COM 接口
  2. ComPtr<std::string> badPtr;  // 编译错误!
  3. D3D12CreateDevice(..., IID_PPV_ARGS(&badPtr));
  4. // static_assert 失败:std::string 不继承自 IUnknown
复制代码
  1. // 使用示例:
  2. // 原始 COM 调用(繁琐且容易出错)
  3. ID3D12Device* rawDevice = nullptr;
  4. HRESULT hr = D3D12CreateDevice(
  5.     nullptr,
  6.     D3D_FEATURE_LEVEL_11_0,
  7.     IID_ID3D12Device,
  8.     reinterpret_cast<void**>(&rawDevice)  // 需要显式转换
  9. );
  10. // 使用宏(类型安全且简洁)
  11. ComPtr<ID3D12Device> device;
  12. HRESULT hr = D3D12CreateDevice(
  13.     nullptr,
  14.     D3D_FEATURE_LEVEL_11_0,
  15.     IID_PPV_ARGS(&device)  // 自动处理所有细节
  16. );
复制代码
Device
  1. HRESULT WINAPI D3D12CreateDevice(_In_opt_ IUnknown* pAdapter,
  2.     D3D_FEATURE_LEVEL MinimumFeatureLevel,
  3.     _In_ REFIID riid, // Expected: ID3D12Device
  4.     _COM_Outptr_opt_ void** ppDevice );
  5. // 尝试硬件设备
  6. HRESULT hardwareResult = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&md3dDevice));
  7. ID3D12Device (设备对象 - 最重要的对象)
  8.     ├── 创建命令队列和列表
  9.     ├── 创建资源(纹理、缓冲区)
  10.     ├── 创建管线状态对象
  11.     ├── 创建描述符堆
  12.     └── 查询设备能力
复制代码
SAL
详情:https://learn.microsoft.com/zh-cn/cpp/code-quality/understanding-sal?view=msvc-170
  1. HRESULT WINAPI D3D12CreateDevice(
  2.     _In_opt_ IUnknown* pAdapter,
  3.     D3D_FEATURE_LEVEL MinimumFeatureLevel,
  4.     _In_ REFIID riid, // Expected: ID3D12Device
  5.     _COM_Outptr_opt_ void** ppDevice );
复制代码
这个函数签名中有一些奇怪的宏,_In_opt_  _In_  _COM_Outptr_opt_
  1. // e.g. void SetPoint( _In_ const POINT* pPT );
  2. #define _In_             _SAL2_Source_(_In_, (), _Pre1_impl_(__notnull_impl_notref) _Pre_valid_impl_ _Deref_pre1_impl_(__readaccess_impl_notref))
  3. #define _In_opt_         _SAL2_Source_(_In_opt_, (), _Pre1_impl_(__maybenull_impl_notref) _Pre_valid_impl_ _Deref_pre_readonly_)
  4. #define _COM_Outptr_opt_                               __allowed(on_parameter)
复制代码
SAL 是 Microsoft 源代码注释语言,注释是在头文件  中定义的。
通过使用源代码注释,可以在代码中明确表明你的意图。 这些注释还能使自动化静态分析工具更准确地分析代码,明显减少假正和假负情况。
  1. void * memcpy(
  2.    void *dest,
  3.    const void *src,
  4.    size_t count
  5. );
复制代码
你能判断出此函数的作用吗? 实现或调用函数时,必须维护某些属性以确保程序正确性。
只看本示例中的这类声明无法判断它们是什么。
如果没有 SAL 注释,就只能依赖于文档或代码注释。
4.png

5.png
  1. HRESULT WINAPI D3D12CreateDevice(
  2.     _In_opt_ IUnknown* pAdapter,
  3.     D3D_FEATURE_LEVEL MinimumFeatureLevel,
  4.     _In_ REFIID riid, // Expected: ID3D12Device
  5.     _COM_Outptr_opt_ void** ppDevice ); ComPtr device;    // 根据注解,我们知道:    // - 第一个参数可以传 nullptr(使用默认显卡)    // - 第二个参数必须传有效的功能级别      // - 第三个参数必须传有效的 IID    // - 第四个参数可以传 nullptr(如果我们不想要设备对象)        HRESULT hr = D3D12CreateDevice(        nullptr,                    // _In_opt_ → 可选,传 nullptr 使用默认        D3D_FEATURE_LEVEL_11_0,     // 必须的有效枚举值        IID_PPV_ARGS(&device)       // _In_ + _COM_Outptr_opt_ → 必须有效的 IID,输出设备    );
复制代码
理解这些注解可以帮助你:

  • 正确使用 API 函数
  • 编写更安全的代码
  • 更好地理解 Microsoft 的 API 设计哲学
GPU命令

命令列表 (Command List) - 录制器
ComPtr mCommandList;
命令列表由 ID3D12CommandList 接口表示,并由设备接口使用 CreateCommandList() 方法创建。
使用命令列表来分配要在 GPU 上执行的命令。命令可能包括设置管道状态、设置资源、转换资源状态( 资源屏障 )、设置顶点/索引缓冲区、绘制、清除渲染目标、设置渲染目标视图、执行捆绑包 (命令组)等。
命令列表与命令分配器相关联,命令分配器将命令存储在 GPU 上。
当第一次创建命令列表时,需要使用标志指定D3D12_COMMAND_LIST_TYPE它是什么类型的命令列表,并提供与该列表关联的命令分配器。有 4 种类型的命令列表;直接、捆绑、计算和复制。
直接命令列表是 GPU 可以执行的命令列表。直接命令列表需要与直接命令分配器(使用D3D12_COMMAND_LIST_TYPE_DIRECT标志创建的命令分配器)相关联。
当完成命令列表的填充后,必须调用 close() 方法将命令列表设置为未记录状态。调用 close 后,可以使用命令队列来执行命令列表。

就像电影导演的剧本
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册