什么是 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++方法 → "我"清理(编译器自动处理)
如果不清理会怎样?- void FunctionA(int x, int y) {
- // 使用x,y...
- }
- void FunctionB(int a, int b, int c) {
- // 使用a,b,c...
- }
- int main() {
- FunctionA(1, 2); // 栈上放了 [1, 2]
- // 如果不清理,栈上还有 [1, 2]
- FunctionB(3, 4, 5); // 栈变成 [3, 4, 5, 1, 2] ← 混乱!
- }
复制代码 清理栈就是调整栈指针(ESP),让栈回到函数调用前的状态:- 调用前:ESP指向位置X
- 调用时:push参数 → ESP移动到位置Y (Y < X)
- 清理后:ESP回到位置X
复制代码- // ✅ 正确:调用Windows API(不用管清理)
- MessageBox(NULL, "Text", "Title", MB_OK);
- // ✅ 正确:写回调函数(用CALLBACK宏)
- LRESULT CALLBACK MyCallback(HWND, UINT, WPARAM, LPARAM);
- // ✅ 正确:调用可变参数函数(编译器自动清理)
- printf("Values: %d %d", x, y);
- // ❌ 错误:回调函数不用__stdcall
- LRESULT MyBadCallback(HWND, UINT, WPARAM, LPARAM); // 会崩溃!
复制代码 机制举例
__cdecl - "我请客,我收拾"- // "我"调用printf(Windows的C运行时库)
- printf("Count: %d %d", 10, 20);
- ; "我"调用printf后
- push offset text ; 参数3
- push value ; 参数2
- push num ; 参数1
- push offset format ; 参数0
- call printf
- add esp, 16 ; ⭐"我"清理栈:4个参数×4字节
复制代码 分工:
"我":放参数 + 清理栈
Windows/CRT:只用参数,不清理
__stdcall - "Windows服务,Windows收拾"- // "我"调用Windows API
- CreateWindow(className, title, style, x, y, width, height, ...);
- ; "我"调用CreateWindow后
- push 0 ; 参数11
- push hInstance ; 参数10
- ; ... 更多参数 ...
- push className ; 参数1
- call CreateWindowEx
- ; ⭐没有add esp! Windows会自己清理
复制代码 分工:
"我":放参数
Windows:用参数 + 清理栈
__stdcall回调 - "Windows调用,我收拾"- // Windows调用"我"的回调
- LRESULT CALLBACK MyWindowProc(HWND, UINT, WPARAM, LPARAM);
- ; Windows调用"我"的WndProc
- _WndProc@16:
- ; "我"的处理逻辑...
- ret 16 ; ⭐"我"清理16字节参数
复制代码 分工:
Windows:放参数
"我":用参数 + 清理栈
__fastcall - "快速服务,Windows收拾"- // 假设是Windows的某个性能API
- int __fastcall FastAPI(int a, int b, int c);
复制代码 分工:
"我":前两个参数放寄存器,其余放栈
Windows:用参数 + 清理栈上的参数
- 性能优化:寄存器比内存快
- Windows负责清理,保持API一致性
__thiscall - "对象方法,我收拾"- // "我"的C++类
- class MyClass {
- public:
- void Method(int param); // 自动__thiscall
- };
复制代码 分工:
"我":this放寄存器,参数放栈
"我":用参数 + 清理栈
- C++对象模型,"我"完全控制自己的类
- 编译器自动处理,对"我"透明
创建一个Windows窗口
- LRESULT CALLBACK MainWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
- {
- // Forward hwnd on because we can get messages (e.g., WM_CREATE)
- // before CreateWindow returns, and thus before mhMainWnd is valid.
- //转发消息给 D3DApp::GetApp()->MsgProc()
- return D3DApp::GetApp()->MsgProc(hwnd, msg, wParam, lParam);
- }
- bool D3DApp::InitMainWindow()
- {
- // 设置窗口类属性
- WNDCLASS wc;
- // 窗口尺寸变化时重绘 ,CS_HREDRAW 宽度改变时重绘 ,CS_VREDRAW 高度改变时重绘
- wc.style = CS_HREDRAW | CS_VREDRAW;
- // 建立消息处理回调机制,所有发送到此窗口的消息都由该函数处理 , 在代码中,MainWndProc 转发消息给 D3DApp::GetApp()->MsgProc()
- wc.lpfnWndProc = MainWndProc;
- // 应用程序实例句柄
- wc.hInstance = mhAppInst;
- // 额外的类内存 ,用于存储自定义数据,这里设为0表示不需要
- wc.cbClsExtra = 0;
- // 额外的窗口内存字节数 ,用于存储自定义数据,这里设为0表示不需要
- wc.cbWndExtra = 0;
- // 加载系统预定义的应用程序图标
- wc.hIcon = LoadIcon(0, IDI_APPLICATION);
- // 加载系统预定义的箭头光标
- wc.hCursor = LoadCursor(0, IDC_ARROW);
- // 窗口背景画刷 NULL_BRUSH 表示不绘制背景,适合DirectX应用(因为DirectX会完全覆盖客户区).
- 避免GDI与DirectX绘制冲突
- wc.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH);
- // 设为0表示此窗口类没有菜单
- wc.lpszMenuName = 0;
- // 窗口类名称,用于在系统中唯一标识此窗口类
- wc.lpszClassName = L"MainWnd";
- //将定义好的窗口类注册到操作系统中. Windows机制:系统内部维护一个窗口类表;注册成功后,该类可用于创建多个窗口实例;失败通常是因为类名重复或参数无效.
- if( !RegisterClass(&wc) )
- {
- MessageBox(0, L"RegisterClass Failed.", 0, 0);
- return false;
- }
- // Compute window rectangle dimensions based on requested client area dimensions.
- /*
- 窗口尺寸计算:
- 客户区 (Client Area):应用程序实际可绘制内容的区域(mClientWidth × mClientHeight)
- 窗口矩形 (Window Rect):包含标题栏、边框、菜单等的完整窗口
- AdjustWindowRect 根据窗口样式自动计算转换关系 .
-
- ┌─────────────────────────┐
- │ 标题栏 (非客户区) │
- ├────────────┬────────────┤
- │ │ │
- │ │ │
- │ 客户区 │ 滚动条 │
- │ │ (非客户区) │
- │ │ │
- └────────────┴────────────┘
- 原始客户区: (0,0) 到 (800,600)
- AdjustWindowRect 调整后: 可能变成 (-8,-31) 到 (808,631)
- 最终窗口尺寸: 816 × 662 像素
- */
- RECT R = { 0, 0, mClientWidth, mClientHeight };
- AdjustWindowRect(&R, WS_OVERLAPPEDWINDOW, false);
- int width = R.right - R.left;
- int height = R.bottom - R.top;
- /* 窗口创建:
- L"MainWnd":窗口类名,必须与注册的类名一致 | WS_OVERLAPPEDWINDOW:窗口样式,包含标题栏、系统菜单、最小化/最大化按钮、可调整边框
- CW_USEDEFAULT, CW_USEDEFAULT:窗口初始位置,让系统自动选择
- Windows创建机制:系统分配内部窗口数据结构 | 发送 WM_CREATE 等初始化消息 | 返回唯一的窗口句柄 mhMainWnd
- */
- mhMainWnd = CreateWindow(L"MainWnd", mMainWndCaption.c_str(),
- WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, width, height, 0, 0, mhAppInst, 0);
- if( !mhMainWnd )
- {
- MessageBox(0, L"CreateWindow Failed.", 0, 0);
- return false;
- }
- //改变窗口可视状态为显示
- ShowWindow(mhMainWnd, SW_SHOW);
- //强制发送 WM_PAINT 消息,立即重绘窗口
- UpdateWindow(mhMainWnd);
- return true;
- }
复制代码
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标志表示从队列中移除该消息- LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
- {
- switch(msg)
- {
- if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
- {
- //...
- }
- // 各种消息处理 case
- // ...
- }
- return DefWindowProc(hwnd, msg, wParam, lParam);
- }
复制代码 窗口消息
WM_ACTIVATE- 窗口激活状态变化- case WM_ACTIVATE:
- //LOWORD(wParam):低16位表示激活状态
- //WA_INACTIVE:窗口变为非活动状态
- //失活时暂停,激活时恢复,智能暂停机制提升系统整体性能
- if( LOWORD(wParam) == WA_INACTIVE )
- {
- mAppPaused = true;
- mTimer.Stop();
- }
- else
- {
- mAppPaused = false;
- mTimer.Start();
- }
- return 0;
复制代码 WM_SIZE- 窗口尺寸变化
LOWORD 和 HIWORD 是 Windows API 中的宏定义,用于从一个 32 位值中提取低 16 位和高 16 位部分。- case WM_SIZE:
- mClientWidth = LOWORD(lParam); // 新宽度
- mClientHeight = HIWORD(lParam); // 新高度
复制代码 • 低 16 位 (LOWORD) 存储新宽度(以像素为单位)
• 高 16 位 (HIWORD) 存储新高度(以像素为单位)
SIZE_MINIMIZED最小化情况,完全暂停应用程序,不进行任何渲染
SIZE_MAXIMIZED最大化情况,立即调整D3D资源适应新尺寸
SIZE_RESTORED恢复情况(最复杂)- else if( wParam == SIZE_RESTORED )
- {
- // 从最小化恢复
- if( mMinimized )
- {
- mAppPaused = false;
- mMinimized = false;
- OnResize();
- }
- // 从最大化恢复
- else if( mMaximized )
- {
- mAppPaused = false;
- mMaximized = false;
- OnResize();
- }
- // 用户正在拖拽调整大小
- //拖拽时不立即调整,避免频繁资源重建
- else if( mResizing )
- {
- // 故意不调用OnResize() - 性能优化
- }
- // 程序化尺寸改变
- else
- {
- OnResize();
- }
- }
复制代码 WM_ENTERSIZEMOVE / WM_EXITSIZEMOVE - 调整大小过程管理
WM_ENTERSIZEMOVE:开始拖拽,设置标志位暂停调整
WM_EXITSIZEMOVE:结束拖拽,清除标志位并执行最终调整- case WM_ENTERSIZEMOVE:
- mAppPaused = true;
- mResizing = true;
- mTimer.Stop();
- return 0;
- case WM_EXITSIZEMOVE:
- mAppPaused = false;
- mResizing = false;
- mTimer.Start();
- OnResize(); // 拖拽结束后一次性调整
- return 0;
复制代码 WM_DESTROY - 窗口销毁
发送 WM_QUIT 到消息队列,导致 Run() 中的主循环退出
PostQuitMessage(0) → 系统消息队列 → 线程消息队列 → PeekMessage() → msg变量
Windows消息系统架构
系统消息队列 (全局)
↓
线程消息队列 (每个线程独立)
↓
PeekMessage/GetMessage (应用程序检索)- case WM_DESTROY:
- PostQuitMessage(0);
- return 0;
复制代码- int D3DApp::Run()
- {
- MSG msg = {0};
-
- mTimer.Reset();
- while(msg.message != WM_QUIT)
- {
- if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
- {
- TranslateMessage( &msg );
- DispatchMessage( &msg );
- }
- return (int)msg.wParam;
- }
复制代码 WM_MENUCHAR- 菜单字符处理
处理 Alt+Enter 等组合键,避免系统蜂鸣声
MNC_CLOSE表示关闭菜单而不发出蜂鸣- case WM_MENUCHAR:
- return MAKELRESULT(0, MNC_CLOSE);
复制代码 WM_GETMINMAXINFO- 窗口尺寸限制
设置窗口最小尺寸为 200×200,防止窗口过小导致渲染问题- case WM_GETMINMAXINFO:
- ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 200;
- ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 200;
- return 0;
复制代码 鼠标消息处理
GET_X_LPARAM(lParam):从 lParam 提取 X 坐标
GET_Y_LPARAM(lParam):从 lParam 提取 Y 坐标
wParam:按键状态(Ctrl、Shift 等)
设计模式:使用虚函数提供扩展点,派生类可重写鼠标处理- case WM_LBUTTONDOWN:
- case WM_MBUTTONDOWN:
- case WM_RBUTTONDOWN:
- OnMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
- return 0;
- // 类似的鼠标抬起和移动处理
复制代码 WM_KEYUP- 键盘按键处理
ESC键:优雅退出应用程序
F2键:运行时切换4倍多重采样抗锯齿状态- case WM_KEYUP:
- if(wParam == VK_ESCAPE)
- {
- PostQuitMessage(0); // ESC键退出
- }
- else if((int)wParam == VK_F2)
- Set4xMsaaState(!m4xMsaaState); // F2切换抗锯齿
- return 0;
复制代码 初始化Direct3D
COM
COM(Component Object Model)是 Microsoft 的组件对象模型,它是 Windows 生态系统的基石
COM 的核心思想是二进制级别的兼容性:
- 不同编译器生成的 COM 组件可以互操作
- 不同语言(C++、C#、VB、Delphi)可以互相调用
- 进程内(DLL)和进程外(EXE)组件统一模型
IUnknown- // IUnknown 是每个 COM 接口都必须继承的基接口
- class IUnknown {
- public:
- virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
- // 当新的代码段获得接口指针时,必须调用 AddRef() 来增加引用计数。
- virtual ULONG AddRef() = 0;
- // 当不再需要接口指针时调用,减少引用计数。当计数为 0 时,对象自我销毁。
- virtual ULONG Release() = 0;
- };
复制代码 QueryInterface - 接口查询- // 检查对象是否支持特定接口,如果支持则返回该接口指针
- virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
- // 假设我们有一个 IUnknown 指针
- IUnknown* pUnknown = ...;
- // 查询 ID3D12Device 接口
- ID3D12Device* pDevice = nullptr;
- HRESULT hr = pUnknown->QueryInterface(IID_ID3D12Device, (void**)&pDevice);
- if (SUCCEEDED(hr)) {
- // 可以使用 ID3D12Device 接口
- pDevice->CreateCommandQueue(...);
- pDevice->Release(); // 使用完后释放
- }
复制代码 COM 对象生命周期管理
引用计数规则- class ReferenceCountingExample {
- public:
- void CorrectReferenceCounting() {
- // 场景1:创建新对象
- IUnknown* pObj = CreateNewObject(); // 引用计数 = 1
-
- // 场景2:复制指针
- IUnknown* pCopy = pObj;
- pCopy->AddRef(); // 现在引用计数 = 2
-
- // 场景3:使用完副本
- pCopy->Release(); // 引用计数 = 1
-
- // 场景4:使用完原始指针
- pObj->Release(); // 引用计数 = 0 → 对象销毁
- }
-
- void CommonMistakes() {
- // 错误:忘记 AddRef
- IUnknown* pOriginal = CreateNewObject(); // 计数 = 1
- IUnknown* pCopy = pOriginal; // 计数还是 1!
- pOriginal->Release(); // 计数 = 0 → 对象销毁!
- // pCopy 现在指向已销毁的对象!
-
- // 错误:忘记 Release - 内存泄漏!
- IUnknown* pObj = CreateNewObject(); // 计数 = 1
- // 使用对象...
- // 忘记调用 pObj->Release() - 对象永远不会销毁!
- }
- };
复制代码 COM接口定义
接口标识符 (IID)
每个 COM 接口都有一个全局唯一标识符 (GUID):- // IID 定义示例
- // {接口名称}-{唯一标识符}
- DEFINE_GUID(IID_IMyInterface,
- 0x12345678, 0x1234, 0x1234, 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc);
复制代码 COM 在 DirectX 12 中的应用
DirectX COM 接口使用- class D3D12COMUsage {
- public:
- void TypicalD3D12Usage() {
- // 1. 创建 DXGI 工厂
- IDXGIFactory4* pFactory = nullptr;
- CreateDXGIFactory1(IID_PPV_ARGS(&pFactory));
-
- // 2. 创建设备
- ID3D12Device* pDevice = nullptr;
- D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&pDevice));
-
- // 3. 创建命令队列
- ID3D12CommandQueue* pCommandQueue = nullptr;
- D3D12_COMMAND_QUEUE_DESC queueDesc = {};
- pDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&pCommandQueue));
-
- // 使用对象...
-
- // 4. 按创建顺序的逆序释放
- pCommandQueue->Release();
- pDevice->Release();
- pFactory->Release();
- }
- };
复制代码 现代 C++ COM 编程
在现代 DirectX 12 编程中,虽然我们使用 ComPtr 等智能指针来简化 COM 编程,但理解底层的 COM 机制对于调试和理解系统行为仍然至关重要。- // 使用 ComPtr 简化 COM 编程
- void ModernCOMProgramming()
- {
- // 自动管理引用计数
- ComPtr<IDXGIFactory4> factory;
- ComPtr<ID3D12Device> device;
- ComPtr<ID3D12CommandQueue> commandQueue;
-
- // 创建对象(自动管理引用计数)
- CreateDXGIFactory1(IID_PPV_ARGS(&factory));
- D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device));
-
- D3D12_COMMAND_QUEUE_DESC queueDesc = {};
- device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue));
-
- // 不需要手动调用 Release() - ComPtr 自动处理!
-
- // 接口查询也很简单
- ComPtr<IDXGIDevice> dxgiDevice;
- device.As(&dxgiDevice); // 自动 QueryInterface
- }
复制代码 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 接口指针操作
- #include <wrl.h>
- using namespace Microsoft::WRL;
- // 基本用法
- ComPtr<ID3D12Device> device;
复制代码- // 传统方式 - 容易出错
- ID3D12Device* pDevice = nullptr;
- ID3D12CommandQueue* pQueue = nullptr;
- HRESULT hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0,
- IID_PPV_ARGS(&pDevice));
- if (SUCCEEDED(hr)) {
- // 创建命令队列
- hr = pDevice->CreateCommandQueue(..., IID_PPV_ARGS(&pQueue));
- }
- // 必须手动释放 - 容易忘记!
- if (pQueue) pQueue->Release();
- if (pDevice) pDevice->Release(); // 容易漏掉!
复制代码- // ComPtr 方式 - 自动管理生命周期
- ComPtr<ID3D12Device> device;
- ComPtr<ID3D12CommandQueue> queue;
- ThrowIfFailed(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0,
- IID_PPV_ARGS(&device)));
- ThrowIfFailed(device->CreateCommandQueue(..., IID_PPV_ARGS(&queue)));
- // 不需要手动 Release() - 自动处理!
复制代码 ComPtr内部维护一个指针- template <typename T>
- class ComPtr
- {
- public:
- using InterfaceType = T ;
- protected:
- InterfaceType *ptr_;
- public:
- T* Get() const throw()
- {
- return ptr_;
- }
- T** GetAddressOf() throw()
- {
- return &ptr_;
- }
- InterfaceType* operator->() const throw()
- {
- return ptr_;
- }
- }
复制代码 异常检测
ThrowIfFailed宏- #define FAILED(hr) (((HRESULT)(hr)) < 0)
- #define ThrowIfFailed(x) \
- { \
- HRESULT hr__ = (x); \
- std::wstring wfn = AnsiToWString(__FILE__); \
- if(FAILED(hr__)) { throw DxException(hr__, L#x, wfn, __LINE__); } \
- }
- DxException(hr__, // 1. HRESULT 错误代码
- L#x, // 2. 失败的函数调用表达式
- wfn, // 3. 文件名
- __LINE__); // 4. 行号
复制代码 FILE:预定义宏,展开为当前源文件的完整路径(ANSI 字符串)
DxException- class DxException
- {
- public:
- DxException() = default;
- DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber);
- std::wstring ToString()const;
- HRESULT ErrorCode = S_OK;
- std::wstring FunctionName;
- std::wstring Filename;
- int LineNumber = -1;
- };
- DxException::DxException(HRESULT hr, const std::wstring& functionName, const std::wstring& filename, int lineNumber) :
- ErrorCode(hr),FunctionName(functionName),Filename(filename),LineNumber(lineNumber)
- {}
复制代码 在WinMain中使用:- int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
- PSTR cmdLine, int showCmd)
- {
- try
- {
- InitDirect3DApp theApp(hInstance);
- if(!theApp.Initialize())
- return 0;
- return theApp.Run();
- }
- catch(DxException& e)
- {
- MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
- return 0;
- }
- }
复制代码 Debug
- ComPtr<ID3D12Debug> debugController;
- ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
- debugController->EnableDebugLayer();
复制代码 ID3D12Debug 是一个接口,专门用于启用和控制 Direct3D 12 的调试功能
它不是渲染管线的一部分,而是开发时的辅助工具
主要功能:
- 错误检查:验证参数是否正确,捕获非法调用
- 资源追踪:检测资源泄漏和错误使用
- 状态验证:确保资源状态转换正确
- 性能分析:识别性能瓶颈
- 详细输出:在 Visual Studio 输出窗口显示详细信息
DXGIFactory
工厂模式:用于创建其他图形相关对象的"工厂"
DXGI irectX Graphics Infrastructure- ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
- 应用程序
- ↓ 使用
- IDXGIFactory4 (图形工厂)
- ├── 创建交换链 (IDXGISwapChain)
- ├── 枚举适配器 (显卡)
- └── 查询显示模式
复制代码 ID3D12Device
该设备是一个虚拟适配器,使用它来创建命令列表、管道状态对象、根签名、命令分配器、命令队列、围栏、资源、描述符和描述符堆。
计算机可能具有多个 GPU,因此可以使用 DXGI 工厂枚举设备,并查找第一个功能级别 11(与 direct3d 12 兼容)的设备
找到要使用的适配器索引后,通过调用 D3D12CreateDevice() 创建设备。
IID_PPV_ARGS- #define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
复制代码 __uuidof 运算符:Microsoft 特有的编译器扩展,在编译时获取 COM 接口的 GUID/IID,不需要运行时查询- &device // 类型: ComPtr<ID3D12Device>*
- *(&device) // 类型: ComPtr<ID3D12Device>&
- **(&device) // 类型: ID3D12Device*
- __uuidof(**(&device))
- → __uuidof(ID3D12Device*)
- → 编译时得到 ID3D12Device 的 IID
复制代码 IID_PPV_ARGS_Helper 函数的参数是ComPtrRef 而不是ComPtr.
ComPtrRef 包装ComPtr的引用,用于安全地传递 COM 接口指针
返回值:void** COM 方法通常需要 void** 参数来接收接口指针- //COM 方法通常需要 void** 参数来接收接口指针, 例如:
- HRESULT QueryInterface(REFIID riid, void** ppvObject);
- HRESULT D3D12CreateDevice(..., REFIID riid, void** ppDevice);
- // Overloaded global function to provide to IID_PPV_ARGS that support Details::ComPtrRef
- template<typename T>
- void** IID_PPV_ARGS_Helper(_Inout_ ::Microsoft::WRL::Details::ComPtrRef<T> pp) throw()
- {
- static_assert(__is_base_of(IUnknown, typename T::InterfaceType), "T has to derive from IUnknown");
- return pp;
- }
复制代码
- 确保模板参数 T 的接口类型继承自 IUnknown
- 所有 COM 接口都必须继承自 IUnknown
- 编译时失败,避免运行时错误
- // 错误用法:非 COM 接口
- ComPtr<std::string> badPtr; // 编译错误!
- D3D12CreateDevice(..., IID_PPV_ARGS(&badPtr));
- // static_assert 失败:std::string 不继承自 IUnknown
复制代码- // 使用示例:
- // 原始 COM 调用(繁琐且容易出错)
- ID3D12Device* rawDevice = nullptr;
- HRESULT hr = D3D12CreateDevice(
- nullptr,
- D3D_FEATURE_LEVEL_11_0,
- IID_ID3D12Device,
- reinterpret_cast<void**>(&rawDevice) // 需要显式转换
- );
- // 使用宏(类型安全且简洁)
- ComPtr<ID3D12Device> device;
- HRESULT hr = D3D12CreateDevice(
- nullptr,
- D3D_FEATURE_LEVEL_11_0,
- IID_PPV_ARGS(&device) // 自动处理所有细节
- );
复制代码 Device- HRESULT WINAPI D3D12CreateDevice(_In_opt_ IUnknown* pAdapter,
- D3D_FEATURE_LEVEL MinimumFeatureLevel,
- _In_ REFIID riid, // Expected: ID3D12Device
- _COM_Outptr_opt_ void** ppDevice );
- // 尝试硬件设备
- HRESULT hardwareResult = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&md3dDevice));
- ID3D12Device (设备对象 - 最重要的对象)
- ├── 创建命令队列和列表
- ├── 创建资源(纹理、缓冲区)
- ├── 创建管线状态对象
- ├── 创建描述符堆
- └── 查询设备能力
复制代码 SAL
详情:https://learn.microsoft.com/zh-cn/cpp/code-quality/understanding-sal?view=msvc-170- HRESULT WINAPI D3D12CreateDevice(
- _In_opt_ IUnknown* pAdapter,
- D3D_FEATURE_LEVEL MinimumFeatureLevel,
- _In_ REFIID riid, // Expected: ID3D12Device
- _COM_Outptr_opt_ void** ppDevice );
复制代码 这个函数签名中有一些奇怪的宏,_In_opt_ _In_ _COM_Outptr_opt_- // e.g. void SetPoint( _In_ const POINT* pPT );
- #define _In_ _SAL2_Source_(_In_, (), _Pre1_impl_(__notnull_impl_notref) _Pre_valid_impl_ _Deref_pre1_impl_(__readaccess_impl_notref))
- #define _In_opt_ _SAL2_Source_(_In_opt_, (), _Pre1_impl_(__maybenull_impl_notref) _Pre_valid_impl_ _Deref_pre_readonly_)
- #define _COM_Outptr_opt_ __allowed(on_parameter)
复制代码 SAL 是 Microsoft 源代码注释语言,注释是在头文件 中定义的。
通过使用源代码注释,可以在代码中明确表明你的意图。 这些注释还能使自动化静态分析工具更准确地分析代码,明显减少假正和假负情况。- void * memcpy(
- void *dest,
- const void *src,
- size_t count
- );
复制代码 你能判断出此函数的作用吗? 实现或调用函数时,必须维护某些属性以确保程序正确性。
只看本示例中的这类声明无法判断它们是什么。
如果没有 SAL 注释,就只能依赖于文档或代码注释。
- HRESULT WINAPI D3D12CreateDevice(
- _In_opt_ IUnknown* pAdapter,
- D3D_FEATURE_LEVEL MinimumFeatureLevel,
- _In_ REFIID riid, // Expected: ID3D12Device
- _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 后,可以使用命令队列来执行命令列表。
就像电影导演的剧本
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |