2.1 COM与设备工厂——一切从枚举适配器开始


2.1 COM 与设备工厂——一切从枚举适配器开始

本节摘要:COM 是 DirectX 全家共用的对象模型:接口按二进制标准布局、生命周期靠引用计数、能力靠接口查询协商。本节先给出它的准确定义,再顺着工厂模式走一遍"枚举适配器 → 创建设备"的初始化第一步。

先给 COM 下个定义

COM,Component Object Model,组件对象模型——微软定下的一套二进制层面的对象协议:规定对象如何向外界暴露功能(接口表)、如何被创建(工厂)、如何消亡(引用计数)、如何协商能力(接口查询)。注意措辞:它是二进制协议而非语言特性,所以 C++、C#、Python 写的程序能毫无障碍地共用同一批 DirectX 组件——这正是它被选作 DirectX 地基的原因。

为什么图形 API 需要这层协议?想象没有它会怎样:显卡驱动每月更新,DirectX 运行时随系统升级,你的程序却是去年编译的。如果接口布局依赖编译器实现,任何一侧的更新都会让二进制失配。COM 把接口定成稳定的内存布局——虚函数表顺序即契约——于是驱动与系统各自演进,编译于任何年代的程序依然能对话。这是它的第一重身份:跨版本稳定的接口冻结术。

引用计数:没有垃圾回收器的世界里如何活

COM 对象的生死由引用计数决定:AddRef 加一,Release 减一,归零即析构。初学者在此栽两个坑。第一个是忘了 Release——对象泄漏,调试器里表现为设备对象数持续上涨;第二个是Release 后继续用——悬垂指针,偶发崩溃难复现。原生写法要求成对管理,好在微软提供智能指针封装(ComPtr 模板),析构时自动 Release,现代项目几乎一律用它:

#include <wrl/client.h> using Microsoft::WRL::ComPtr; ComPtr<IDXGIFactory7> factory; // 枚举适配器前的第一步:拿到 DXGI 工厂 DXGIGetDebugInterface1(0, IID_PPV_ARGS(factory.GetAddressOf())); // 调试构建下可选 ComPtr<IDXGIAdapter4> adapter; // 从工厂按序号取适配器;ordinal 0 未必是独显,需查描述与能力 for (UINT i = 0; factory->EnumAdapters1(i, adapter.ReleaseAndGetAddressOf()) != DXGI_ERROR_NOT_FOUND; ++i) { DXGI_ADAPTER_DESC1 desc; adapter->GetDesc1(&desc); bool isSoftware = desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE; // 排除 WARP 软件渲染 // 检查该适配器是否支持所需的 Direct3D 12 功能级别 if (!isSoftware && SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, _uuidof(ID3D12Device), nullptr))) { break; // 选中第一块支持 12_0 的硬件适配器 } }

这段代码串起了本节后半段的两个概念——工厂与适配器。上面每一步的意图都值得读懂:工厂是"问询窗口",适配器是"候选 GPU",能力检查是"资格审查"。

工厂模式:为什么创建对象要绕一道

DirectX 里你从不直接 new 一个设备,总要向工厂接口要:DXGIFactory 造交换链与适配器,D3D12CreateDevice 造设备。这道弯路的理由有三。其一,解耦实现:工厂返回的指针指向驱动侧的具体实现,你只见接口不见类名,实现可以随驱动版本换血。其二,协商能力:创建过程本身是资格审查——功能级别不达标就拒绝创建,把兼容问题拦在启动时(这个机制下一节展开)。其三,集中管理:同一工厂生出的对象共享配置(如消息过滤、窗口关联),为后续调试与事件处理留了统一入口。

图1 从工厂到设备的装配链

图1 从工厂到设备的装配链

动手:完整的初始化最小路径

把链条接完。选定适配器后创建设备,再从设备要命令队列:

ComPtr<ID3D12Device> device; // 体检通过,正式创建:参数为适配器、功能级别、目标接口 D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(device.GetAddressOf())); // D3D 12 的队列要显式声明类型;11 时代没有这一步——驱动替你选好了 D3D12_COMMAND_QUEUE_DESC qdesc{}; qdesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; // 直接队列:图形与计算通吃 ComPtr<ID3D12CommandQueue> queue; device->CreateCommandQueue(&qdesc, IID_PPV_ARGS(queue.GetAddressOf()));

逐行解读这段最短路径:命令队列类型选 DIRECT 而非 COMPUTE 或 COPY,因为直接队列既能画图也能算账,是一般程序的主干线。队列描述结构体清零后仅设类型——D3D 12 风格的惯例是描述符结构体先清零再填字段,跳过清零会让未初始化内存里的垃圾值闯进创建流程,这是新手排错榜上的常客。

一个案例:笔记本双显卡的选择题

背景:一款独立游戏在测试者笔记本上帧率异常低,但配置表显示明明有独显。操作:开发者在启动时枚举适配器并打印描述,发现程序默认落在核显上——Windows 的电源策略常把普通程序派给核显。结果:在创建工厂前调用显卡偏好 API(声明需要高性能硬件),并保留适配器枚举日志,测试者上传日志即可定位。解读:适配器选择是程序的权利也是责任,系统默认策略面向省电而非性能;枚举到的第一块卡未必是你想要的卡。变式:专业软件反而要提供"手动选卡"界面(建模用独显、待机用核显);多 GPU 协同渲染则要把两个适配器都纳入管理——那是更高级的话题,第六章的多适配器策略会再次提及。

常见疑问:引用计数泄漏了,程序为什么毫无怨言

新手常疑惑:多 AddRef 一次、少 Release 一次,程序既不崩溃也不报错,问题在哪?答案恰恰是引用计数机制的"成功之处"——它是运行时的自洽协议,不是编译期语法,多一次引用只是让对象晚死(或永远不死),错得悄无声息。显存与句柄就这样一点点漏掉,症状是运行时间越长显存占用越高,最后偶发创建资源失败。这也是本节坚持"一律用智能指针托管 COM 对象"的第二个理由:第一次是省心,第二次是防漏。真要排查存量泄漏,第五章的调试层能报告对象存活计数与释放轨迹——把调试层打开跑一遍完整退出流程,看看退出后还有谁没归零,比肉眼审代码高效得多。

本节要点回顾

  • COM 三件套:二进制稳定的接口布局、引用计数的生死管理、QueryInterface 的能力协商,支撑 DirectX 跨版本演进。
  • 工厂模式的理由:隐藏实现、创建即资格审查、集中配置——绕的弯路都是契约的一部分。
  • 枚举适配器要过滤:排除软件渲染、不迷信序号、用空创建先体检。
  • 初始化代码是排错的第一现场:队列类型、描述符清零、适配器选择,错一步后面全是谜。

零件备齐了,下一节看它们如何协作:设备、上下文、交换链——以及 11 与 12 两代装配工艺的差异。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U