通用GUI编程技术——图形渲染实战(四十七)——D3D12与D3D11互操作及选型建议 通用GUI编程技术——图形渲染实战四十七——D3D12与D3D11互操作及选型建议仓库已经开源喜欢的话点个⭐仓库Win32和Win32图形栈的部分目前已完成教程力争做一个完备的GUI教程欢迎各位大佬前来参观https://github.com/Charliechen114514/anatomy_gui上一篇我们拆解了 D3D12 的描述符堆和根签名——描述符是 GPU 端的指针根签名定义了 Shader 如何找到这些指针两者配合构成了 D3D12 资源绑定模型的完整图景。到此为止我们已经掌握了 D3D12 的核心基础设施命令系统、资源管理、描述符绑定。但在实际项目中你几乎不可能只使用 D3D12——你可能需要用 Direct2D 画 UI 文字或者在一个已有的 D3D11 项目中逐步引入 D3D12。微软提供了 D3D11On12 互操作层来支持这种混合使用的场景今天我们就来拆解它。前言为什么需要互操作说句实话如果你做一个纯 3D 游戏引擎D3D12 完全够用不需要任何互操作。但在 GUI 编程的场景下情况不太一样。你可能已经有一个基于 D3D11 或 Direct2D 的 UI 渲染框架文字渲染、矢量图形、控件绘制现在想引入 D3D12 来处理一些高性能的 3D 渲染任务比如在 D3D12 中渲染一个 3D 预览窗口同时保留原有的 2D UI 不变。或者更常见的场景你想在 D3D12 的渲染帧上叠加 Direct2D 绘制的 UI 元素文字、按钮、进度条等。Direct2D 在 Windows 上是通过 D3D11 的设备来创建渲染目标的它不能直接在 D3D12 的命令队列上工作。所以你需要一个桥接层——D3D11On12 就是这个桥接。根据 Microsoft Learn - D3D11On12 的官方描述D3D11On12 互操作层允许你在 D3D12 的命令队列上使用 D3D11 的接口和运行时。这意味着你可以在同一个渲染管线中混用 D3D12 和 D3D11 的 API 调用而不需要维护两套完全独立的渲染设备。环境说明操作系统: Windows 11 Pro 10.0.26200编译器: MSVC (Visual Studio 2022, v143 工具集)Windows SDK: 10.0.26100 或更高版本依赖:d3d12.h、d3d11.h、d2d1.h、dwrite.h链接库:d3d12.lib、d3d11.lib、d2d1.lib、dwrite.lib前置知识: 文章 44命令队列、文章 46描述符堆与根签名D3D11On12 的架构原理在深入代码之前我们先理解一下 D3D11On12 在底层是怎么工作的。当你通过D3D11On12CreateDevice创建了一个互操作设备后你得到了一个 D3D11 的设备对象ID3D11Device和一个 D3D11 的设备上下文ID3D11DeviceContext。这些 D3D11 对象在底层并不是独立的——它们实际上是 D3D12 设备和命令队列的代理。当你通过 D3D11 的接口提交渲染命令时D3D11On12 层会把这些命令翻译成 D3D12 的命令然后在你的 D3D12 命令队列上执行。你可以把这个过程类比为翻译官——D3D11On12 把 D3D11 的 API 调用翻译成 D3D12 的命令然后通过你的 D3D12 命令队列提交给 GPU。这样D3D12 和 D3D11 的命令就共享同一条 GPU 执行通道资源的共享也变得自然而然。但这个翻译是有开销的。D3D11On12 层需要维护 D3D11 的状态跟踪、资源管理等功能这些在原生 D3D12 中是不需要的。所以互操作不适合高性能的渲染路径——如果你需要对每帧数百万个三角形做 D3D12 渲染然后又通过 D3D11On12 层做额外的处理翻译开销可能成为瓶颈。第一步——创建互操作设备创建互操作设备的核心函数是D3D11On12CreateDevice。根据 Microsoft Learn - D3D11On12CreateDevice 的文档它的参数列表如下#included3d11on12.h#pragmacomment(lib,d3d11.lib)// 假设你已经有了 D3D12 设备和命令队列// ComPtrID3D12Device g_d3d12Device;// ComPtrID3D12CommandQueue g_commandQueue;ComPtrID3D11Deviceg_d3d11Device;ComPtrID3D11DeviceContextg_d3d11Context;ComPtrID3D11On12Deviceg_d3d11On12Device;HRESULT hrD3D11On12CreateDevice(g_d3d12Device.Get(),// D3D12 设备D3D11_CREATE_DEVICE_FLAG_NONE,// D3D11 创建标志nullptr,// Feature LevelsNULL 默认0,// Feature Levels 数量reinterpret_castIUnknown**(g_commandQueue),// 命令队列数组1,// 命令队列数量0,// 节点掩码单 GPU 0g_d3d11Device,// 输出 D3D11 设备g_d3d11Context,// 输出 D3D11 设备上下文nullptr// 返回的 Feature Level可选);if(FAILED(hr)){// 互操作设备创建失败returnfalse;}// 查询 D3D11On12 接口g_d3d11Device.As(g_d3d11On12Device);D3D11On12CreateDevice的关键参数是第一个D3D12 设备和第五个命令队列数组。它们建立了 D3D11 到 D3D12 的桥接关系——所有通过这个 D3D11 设备提交的命令最终都会被翻译后在指定的 D3D12 命令队列上执行。创建成功后我们通过AsCOM 的QueryInterface获取了ID3D11On12Device接口。这个接口提供了 D3D11 和 D3D12 资源之间的包装和解包操作——后面马上会用到。第二步——包装 D3D12 资源给 D3D11 使用D3D12 的渲染目标交换链的后台缓冲区是ID3D12Resource对象D3D11 不认识它。我们需要通过ID3D11On12Device::CreateWrappedResource把 D3D12 资源包装成 D3D11 可以使用的资源// 为每个后台缓冲区创建 D3D11 包装资源ComPtrID3D11Resourceg_wrappedResources[2];for(UINT i0;i2;i){D3D11_RESOURCE_FLAGS resourceFlags{};resourceFlags.BindFlagsD3D11_BIND_RENDER_TARGET;hrg_d3d11On12Device-CreateWrappedResource(g_renderTargets[i].Get(),// D3D12 资源resourceFlags,// D3D11 资源标志D3D12_RESOURCE_STATE_RENDER_TARGET,// D3D12 输入状态D3D12_RESOURCE_STATE_PRESENT,// D3D12 输出状态nullptr,IID_PPV_ARGS(g_wrappedResources[i]));if(FAILED(hr)){// 包装资源创建失败returnfalse;}}CreateWrappedResource的几个关键参数值得展开说说。D3D11_RESOURCE_FLAGS定义了这个资源在 D3D11 中的用途——D3D11_BIND_RENDER_TARGET表示它作为 D3D11 的渲染目标使用。第三个参数D3D12_RESOURCE_STATE_RENDER_TARGET是 D3D11 获得资源控制权时D3D12 端资源应该处于的状态。第四个参数D3D12_RESOURCE_STATE_PRESENT是 D3D11 释放资源控制权后D3D12 端资源应该转换到的状态。这两个状态参数本质上是在告诉 D3D11On12 层“当 D3D11 要用这个资源时自动把它转到 RENDER_TARGET 状态当 D3D11 用完了自动把它转回 PRESENT 状态。”第三步——创建 D2D 渲染目标有了包装后的 D3D11 资源我们就可以按照标准的 D2D D3D11 集成流程来创建 Direct2D 的渲染目标了。首先从 D3D11 设备创建 D2D 设备#included2d1_3.h#pragmacomment(lib,d2d1.lib)ComPtrID2D1Factory3g_d2dFactory;ComPtrID2D1Device2g_d2dDevice;ComPtrID2D1DeviceContext2g_d2dContext;// 创建 D2D 工厂D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,g_d2dFactory);// 从 D3D11 设备获取 DXGI 设备创建 D2D 设备ComPtrIDXGIDevicedxgiDevice;g_d3d11Device.As(dxgiDevice);g_d2dFactory-CreateDevice(dxgiDevice.Get(),g_d2dDevice);g_d2dDevice-CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,g_d2dContext);然后为每个后台缓冲区创建 D2D 的位图渲染目标ComPtrID2D1Bitmap1g_d2dRenderTargets[2];D2D1_BITMAP_PROPERTIES1 bitmapProps{};bitmapProps.bitmapOptionsD2D1_BITMAP_OPTIONS_TARGET|D2D1_BITMAP_OPTIONS_CANNOT_DRAW;bitmapProps.pixelFormat.formatDXGI_FORMAT_R8G8B8A8_UNORM;bitmapProps.pixelFormat.alphaModeD2D1_ALPHA_MODE_PREMULTIPLIED;for(UINT i0;i2;i){ComPtrIDXGISurfacesurface;g_wrappedResources[i].As(surface);g_d2dContext-CreateBitmapFromDxgiSurface(surface.Get(),bitmapProps,g_d2dRenderTargets[i]);}到这里我们已经建立了一条完整的桥接链路D3D12 资源 → D3D11 包装资源 → DXGI Surface → D2D 位图渲染目标。D2D 可以直接在 D3D12 的后台缓冲区上绘制 2D 图形了。第四步——D2D 绘制 UI 的完整流程渲染一帧的流程分为三个阶段先用 D3D12 渲染 3D 场景然后用 D2D 绘制 2D UI最后 Present。关键操作是AcquireWrappedResources和ReleaseWrappedResources——它们分别获取和释放 D3D11 对 D3D12 资源的控制权voidRenderFrame(){UINT frameIndexg_swapChain-GetCurrentBackBufferIndex();// 阶段 1D3D12 渲染 3D 场景 // ... 录制 D3D12 命令渲染 3D 场景到后台缓冲区 ...// ... ResourceBarrier: RENDER_TARGET → PRESENT给 D2D 用...// 等待 D3D12 命令完成WaitForGPU();// 阶段 2D2D 绘制 UI // 获取包装资源的控制权自动将资源转到 RENDER_TARGET 状态ID3D11Resource*ppResources[]{g_wrappedResources[frameIndex].Get()};g_d3d11On12Device-AcquireWrappedResources(ppResources,1);// 设置 D2D 渲染目标g_d2dContext-SetTarget(g_d2dRenderTargets[frameIndex].Get());// 开始 D2D 绘制g_d2dContext-BeginDraw();// 绘制 UI 元素文字、按钮、进度条等g_d2dContext-Clear(D2D1::ColorF(0,0));// 清除为全透明保留 D3D12 的渲染结果// 示例绘制一段文字ComPtrID2D1SolidColorBrushpBrush;g_d2dContext-CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White),pBrush);ComPtrIDWriteTextFormatpTextFormat;// ... 创建文字格式 ...g_d2dContext-DrawTextW(LD3D12 D2D Interop,18,pTextFormat.Get(),D2D1::RectF(10,10,400,50),pBrush.Get());// 结束 D2D 绘制g_d2dContext-EndDraw();// 释放包装资源的控制权自动将资源转回 PRESENT 状态g_d3d11On12Device-ReleaseWrappedResources(ppResources,1);// 刷新 D3D11 命令确保 D2D 的绘制被提交到 D3D12 命令队列g_d3d11Context-Flush();// 阶段 3Present g_swapChain-Present(1,0);}这段代码中有几个关键步骤值得仔细理解。AcquireWrappedResources把 D3D12 资源的控制权交给 D3D11。它会自动在 D3D12 命令队列上插入一个资源屏障把资源从我们在CreateWrappedResource时指定的输出状态PRESENT转换到输入状态RENDER_TARGET。这样 D2D 就可以在资源上绘制了。ReleaseWrappedResources把控制权还回 D3D12。它会在 D3D12 命令队列上插入反向的屏障把资源从RENDER_TARGET转回PRESENT。g_d3d11Context-Flush()确保 D3D11 端所有待处理的命令都被提交到 D3D12 命令队列。没有这一步D2D 的绘制可能还留在 D3D11 的内部缓冲区中Present 时看不到。⚠️ 这里有一个非常容易踩的坑AcquireWrappedResources和ReleaseWrappedResources必须成对调用。如果你只 Acquire 了但忘了 Release资源就永远处于 D3D11 控制下的 RENDER_TARGET 状态D3D12 在 Present 时会因为状态不对而出问题。反过来如果你 Release 了但没 AcquireD3D11 会在没有控制权的情况下尝试操作资源导致未定义行为。互操作的同步问题上面的流程中有一个隐含的同步问题D3D12 渲染和 D2D 渲染之间必须正确同步。D2D 必须等 D3D12 渲染完成后才能开始在同一个后台缓冲区上绘制。在我们的简化示例中通过WaitForGPU()来同步——CPU 等待 GPU 执行完 D3D12 的命令后再开始 D2D 绘制。这是一个安全但不太高效的方案因为它引入了一次 CPU-GPU 同步。在实际项目中更高效的做法是使用 Fence 来做更细粒度的同步——D3D12 渲染完成后 Signal 一个 Fence 值D2D 绘制前等待这个 Fence 值。或者更简单地利用 D3D11On12 内部的隐式同步机制AcquireWrappedResources内部会自动等待关联的 D3D12 命令完成但要注意这可能导致 CPU 侧的额外等待。选型建议D3D11 vs D3D12讨论了这么多互操作的细节一个自然的问题是我到底应该用 D3D11 还是 D3D12应用/工具类项目 → D3D11 足够如果你的项目是一个工具软件、编辑器界面、数据可视化应用D3D11配合 Direct2D完全足够。这类项目的渲染负载通常不高几十到几百个 Draw CallD3D11 的驱动开销不会成为瓶颈。D3D11 的 API 更简洁开发效率更高调试也更方便。典型场景包括图片编辑器的预览窗口、CAD 软件的 2D 视图、音频可视化工具、简单的图表应用。引擎/高性能渲染 → D3D12如果你在开发一个游戏引擎、实时渲染引擎、或者需要处理数万 Draw Call 的高性能应用D3D12 的显式控制可以带来显著的 CPU 端性能提升。多线程命令录制、显式资源状态管理、手动同步控制——这些特性在 Draw Call 数量极大的时候会体现出真正的价值。典型场景包括游戏引擎、GPU 粒子系统、大规模场景渲染、需要 Compute Shader 做复杂计算的应用。学习/教学 → D3D11 先行如果你是在学习图形编程建议从 D3D11 开始。D3D11 的 API 更接近图形管线本身——你关注的是顶点着色器、像素着色器、纹理采样、混合这些渲染概念。D3D12 的额外复杂度命令管理、资源状态、描述符会分散你对渲染核心概念的理解。掌握了 D3D11 的渲染管线后再学 D3D12 就只需要理解基础设施的变化——渲染概念是一样的只是管理方式从隐式变成了显式。迁移/混合 → 互操作如果你有一个已有的 D3D11 项目想逐步迁移到 D3D12或者需要在 D3D12 项目中保留 D3D11/D2D 的 UI 渲染D3D11On12 互操作层就是你的过渡方案。它允许你在同一个渲染管线中混用两种 API按模块逐步迁移而不是一次性重写。常见问题D3D11On12CreateDevice 失败最常见的失败原因是 D3D12 设备不支持互操作。确保你的 D3D12 设备是在支持 D3D12 的 GPU 上创建的不是 WARP 软件光栅化器并且 Windows 版本支持 D3D11On12Windows 10 1607 及以上。D2D 绘制内容看不到检查你是否在 D2D 绘制前调用了AcquireWrappedResources绘制后调用了ReleaseWrappedResources以及最后是否调用了g_d3d11Context-Flush()。三个调用缺一不可。画面闪烁或撕裂可能是 D3D12 渲染和 D2D 绘制之间的同步不正确。确保 D3D12 渲染完成后Fence 信号到达再开始 D2D 绘制。如果你省略了同步步骤D2D 可能会在 D3D12 还在写入后台缓冲区的时候就开始绘制导致画面混乱。性能比纯 D3D11 还差这通常是因为互操作的同步开销过大。每次AcquireWrappedResources和ReleaseWrappedResources都涉及 CPU-GPU 同步如果频繁调用比如每一帧都创建新的包装资源开销会累积。建议在初始化时创建好所有包装资源在渲染循环中只做 Acquire/Release 操作。总结这篇我们拆解了 D3D12 与 D3D11 互操作的完整机制。D3D11On12 互操作层本质上是一个翻译官——它把 D3D11 的 API 调用翻译成 D3D12 的命令通过 D3D12 命令队列提交给 GPU。核心流程包括通过D3D11On12CreateDevice创建互操作设备通过CreateWrappedResource把 D3D12 资源包装成 D3D11 可用的资源在渲染时通过AcquireWrappedResources/ReleaseWrappedResources管理资源的控制权切换。我们还讨论了 D3D11 和 D3D12 的选型建议——应用/工具用 D3D11引擎/高性能用 D3D12学习用 D3D11 先行迁移/混合用互操作。选型没有绝对的对错关键是根据项目需求做出合理的权衡。到此为止我们的 D3D12 部分告一段落。接下来我们把视线从 GPU 加速的 3D 渲染拉回到 Win32 的控件世界——下一篇要聊的是 Owner-Draw 控件如何利用WM_DRAWITEM消息让系统控件ListBox、ComboBox、Button焕然一新。练习创建一个 D3D12 项目使用 D3D11On12 互操作层在 D3D12 渲染的 3D 场景上叠加 D2D 绘制的文字和半透明矩形 UI。确保 Acquire/Release 配对正确。研究 ImGui 的 D3D11 和 D3D12 后端实现GitHub - ocornut/imgui。阅读imgui_impl_dx11.cpp和imgui_impl_dx12.cpp的源码写一段文字总结两者在资源管理和渲染流程上的主要差异。实验互操作的性能影响分别测量纯 D3D12 渲染和 D3D12D2D 互操作渲染的帧时间对比引入互操作后的额外开销。提示使用QueryPerformanceCounter测量每帧耗时。尝试在互操作场景中正确使用 Fence 进行同步替代简单的WaitForGPU方案。思考在什么情况下 Fence 同步比直接等待更高效参考资料:Direct3D 11 on 12 - Microsoft LearnD3D11On12CreateDevice function - Microsoft LearnID3D11On12Device interface - Microsoft LearnD2D1DeviceContext - Microsoft LearnD3D12 and D2D interop sample - Microsoft LearnWorking with Direct3D 11 and Direct2D - Microsoft Learn相关阅读通用GUI编程技术——图形渲染实战四十三——D3D12设计哲学显式控制与性能解锁 - 相似度 100%通用GUI编程技术——Win32 原生编程实战五十三——子类化与超类化 - 相似度 82%04. OF API 基础与验证——从 DTS 到代码的桥梁 - 相似度 82%