钦娅芬 发表于 2025-10-25 23:05:01

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]
查看完整版本: DX12-1-DirectX3D初始化