DX12-1-DirectX3D初始化
什么是 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); // 栈上放了
// 如果不清理,栈上还有
FunctionB(3, 4, 5); // 栈变成 ← 混乱!
}清理栈就是调整栈指针(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:DirectX 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 的 IIDIID_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 后,可以使用命令队列来执行命令列表。
就像电影导演的剧本
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]