通用GUI编程技术——图形渲染实战(二十九)——Direct2D架构与资源体系:GPU加速2D渲染入门
通用GUI编程技术——图形渲染实战二十九——Direct2D架构与资源体系GPU加速2D渲染入门仓库已经开源喜欢的话点个⭐包含Win32的目前已完成教程力争做一个完备的GUI教程欢迎各位大佬前来参观https://github.com/Charliechen114514/anatomy_guiPS: 笔者马上出差回来要开始更忙了所以相关文章为了确保质量更新频率做调整GUI/Qt的教程会二选一更新嵌入式现代C工程实践和嵌入式Linux也会进行交替的更新确保质量到这里我们已经把 Windows 上 CPU 时代的 2D 图形 API 都过了一遍——从原始的 GDI 到封装更优雅的 GDI。说实话GDI 在功能上已经能满足很多需求了但如果你尝试用它做高频率刷新的动画或者在高 DPI 显示器上绘制复杂场景你会立刻感受到性能瓶颈。GDI 和 GDI 都是 CPU 绘制——每一条线、每一个像素都由 CPU 逐个计算GPU 在旁边闲着看热闹。今天我们要跨入一个全新的领域Direct2D。这是微软在 Windows 7 时代推出的 GPU 加速 2D 渲染 API。它的设计哲学和 GDI 完全不同——所有的绘制操作都由 GPU 执行CPU 只负责下达指令。这意味着你可以轻松实现 60fps 的流畅动画同时保持高质量的抗锯齿渲染。但天下没有免费的午餐GPU 加速带来了新的复杂性资源管理模型变了设备丢失需要处理COM 接口的引用计数得操心。今天我们就来拆解 Direct2D 的架构从初始化到第一次绘制。环境说明操作系统: Windows 10/11Direct2D 需要 Windows 7 SP1 平台更新或更高编译器: MSVC (Visual Studio 2022, v17.x 工具集)目标平台: Win32 原生桌面应用图形库: Direct2D链接d2d1.lib头文件d2d1.h字符集: Unicode⚠️ 注意Direct2D 是基于 COMComponent Object Model的 API。所有接口如ID2D1Factory、ID2D1HwndRenderTarget都是 COM 接口通过引用计数管理生命周期。你在使用完之后必须调用Release()方法释放而不是delete。这一点和 GDI 的 C 类自动析构完全不同。Direct2D 的定位与设计哲学Direct2D 的官方定位是GDI 的现代替代品。但如果你用过 GDI你会发现 Direct2D 的编程模型和 GDI 差异巨大几乎是从零开始学。Direct2D 底层通过 Direct3D 10.1 的 API 与 GPU 通信所有的 2D 绘制操作最终都被转换成 GPU 能理解的三角形和纹理采样操作。这意味着 Direct2D 天然支持硬件加速绘制性能远超 CPU 方案。同时Direct2D 也提供了软件回退WARP — Windows Advanced Rasterization Platform在没有 GPU 或 GPU 驱动异常的环境下仍能正常工作。Direct2D 的设计遵循两个核心原则第一是硬件加速优先所有渲染都通过 GPU 完成第二是高 DPI 感知API 内部使用浮点坐标自动处理 DPI 缩放。资源的两级分类理解 Direct2D 资源管理的关键在于区分两类资源设备无关资源和设备相关资源。设备无关资源不依赖于特定的 GPU 硬件或渲染配置。创建一次可以重复使用。典型的设备无关资源包括ID2D1FactoryDirect2D 的工厂接口用于创建所有其他对象、ID2D1StrokeStyle线条样式、ID2D1Geometry及其子类矩形、椭圆、路径几何体等。这些资源通常在程序启动时创建程序退出时释放。设备相关资源依赖于特定的渲染目标Render Target与 GPU 硬件绑定。渲染目标本身就是最重要的设备相关资源。ID2D1HwndRenderTarget窗口渲染目标、ID2D1Brush画刷包括纯色、渐变、位图画刷、ID2D1Bitmap位图都属于这一类。设备相关资源有一个致命特性当设备丢失时比如 GPU 驱动崩溃恢复、显示设置变更所有设备相关资源都会失效必须全部重建。⚠️ 注意这个设备丢失不是理论上的极端情况。在笔记本外接显示器热插拔、远程桌面连接断开、GPU 驱动更新等场景下都会触发。如果你的程序不处理设备丢失轻则画面消失重则崩溃。我们后面会专门讲怎么处理这个问题。Direct2D 初始化标准骨架让我们从最基础的初始化开始一步步搭建一个完整的 Direct2D 程序。创建 ID2D1FactoryID2D1Factory是 Direct2D 的入口点所有其他 Direct2D 对象都通过它创建。创建方式如下ID2D1Factory*pFactoryNULL;HRESULT hrD2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,// 单线程模式pFactory);if(FAILED(hr)){// 创建失败处理return;}D2D1CreateFactory的第一个参数指定工厂类型。D2D1_FACTORY_TYPE_SINGLE_THREADED表示你的程序只在一个线程中使用 Direct2D这是最常见的情况性能也最好。D2D1_FACTORY_TYPE_MULTI_THREADED表示多线程使用Direct2D 内部会加锁保护但会带来性能开销。除非你确实需要从多个线程同时调用 Direct2D否则始终用单线程模式。创建 HwndRenderTarget有了工厂之后下一步是创建窗口渲染目标HwndRenderTarget它负责将 GPU 渲染结果呈现到指定的窗口上RECT rc;GetClientRect(hwnd,rc);ID2D1HwndRenderTarget*pRenderTargetNULL;hrpFactory-CreateHwndRenderTarget(D2D1::RenderTargetProperties(),// 默认渲染目标属性D2D1::HwndRenderTargetProperties(hwnd,// 目标窗口句柄D2D1::SizeU(rc.right-rc.left,rc.bottom-rc.top)// 像素尺寸),pRenderTarget);if(FAILED(hr)){// 创建失败处理return;}RenderTargetProperties控制渲染目标的一般属性比如像素格式、DPI 设置、渲染模式等。默认参数RenderTargetProperties()无参版本会使用 32 位 BGRA 格式、系统默认 DPI 和硬件加速渲染。大多数情况下默认就够了。HwndRenderTargetProperties是窗口渲染目标的专用属性需要你提供窗口句柄和初始像素尺寸。这里的尺寸使用像素单位不是逻辑单位。创建画刷在 Direct2D 中所有填充和描边操作都需要画刷Brush。最基本的画刷是纯色画刷ID2D1SolidColorBrush*pBrushNULL;hrpRenderTarget-CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White),// 白色pBrush);D2D1::ColorF是一个辅助类可以用多种方式指定颜色。除了使用预定义的颜色常量如ColorF::White、ColorF::Red你也可以传入 RGB 值// 自定义颜色浅灰蓝色 (R:0.85, G:0.90, B:0.95, Alpha:1.0)pRenderTarget-CreateSolidColorBrush(D2D1::ColorF(0.85f,0.90f,0.95f,1.0f),pBrush);⚠️ 注意Direct2D 的颜色分量范围是 0.0 到 1.0 的浮点数不是 GDI 那种 0-255 的整数。如果你习惯了 GDI 的RGB(255, 128, 0)写法需要手动除以 255 转换。绘制循环BeginDraw / EndDrawDirect2D 的绘制操作必须在BeginDraw和EndDraw之间进行。这是一个硬性要求在BeginDraw之外调用任何绘制方法都会静默失败不报错但也不绘制。voidOnPaint(ID2D1HwndRenderTarget*pRenderTarget,ID2D1SolidColorBrush*pBrush){pRenderTarget-BeginDraw();// 清除背景为深灰色pRenderTarget-Clear(D2D1::ColorF(D2D1::ColorF::DarkSlateGray));// 绘制一个填充的圆角矩形D2D1_ROUNDED_RECT roundedRectD2D1::RoundedRect(D2D1::RectF(50.0f,50.0f,350.0f,200.0f),// 矩形区域10.0f,// X 方向圆角半径10.0f// Y 方向圆角半径);pBrush-SetColor(D2D1::ColorF(0.2f,0.6f,0.9f,1.0f));// 蓝色pRenderTarget-FillRoundedRectangle(roundedRect,pBrush);// 结束绘制HRESULT hrpRenderTarget-EndDraw();if(hrD2DERR_RECREATE_TARGET){// 设备丢失需要重建所有设备相关资源// 后面会详细讲解}}BeginDraw会标记渲染目标的内部状态为正在绘制EndDraw会提交所有绘制命令并呈现结果。如果在BeginDraw和EndDraw之间发生了错误EndDraw会返回对应的HRESULT。这里有一个非常重要的返回值需要处理D2DERR_RECREATE_TARGET。这个返回值表示渲染目标关联的 GPU 设备已经丢失比如驱动崩溃后恢复当前的渲染目标和所有设备相关资源画刷、位图等都已失效。你必须在收到这个错误后重建所有设备相关资源。处理窗口大小变化当窗口大小改变时你需要调用Resize方法通知渲染目标更新其后备缓冲区的大小caseWM_SIZE:{if(pRenderTarget!NULL){UINT widthLOWORD(lParam);UINT heightHIWORD(lParam);pRenderTarget-Resize(D2D1::SizeU(width,height));InvalidateRect(hwnd,NULL,FALSE);// 触发重绘}return0;}如果你不调用Resize渲染目标的后备缓冲区大小不会自动跟随窗口变化结果就是窗口变大后只有一部分区域有内容或者窗口变小后渲染目标浪费内存。设备丢失的完整处理模式现在我们来处理D2DERR_RECREATE_TARGET。标准的处理模式是在EndDraw之后检查返回值如果收到重建信号就释放所有设备相关资源并重建// 全局变量ID2D1Factory*g_pFactoryNULL;ID2D1HwndRenderTarget*g_pRenderTargetNULL;ID2D1SolidColorBrush*g_pBrushNULL;voidCreateDeviceResources(HWND hwnd){if(g_pRenderTarget!NULL)return;// 已存在跳过RECT rc;GetClientRect(hwnd,rc);HRESULT hrg_pFactory-CreateHwndRenderTarget(D2D1::RenderTargetProperties(),D2D1::HwndRenderTargetProperties(hwnd,D2D1::SizeU(rc.right-rc.left,rc.bottom-rc.top)),g_pRenderTarget);if(FAILED(hr))return;// 创建画刷等设备相关资源g_pRenderTarget-CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White),g_pBrush);}voidDiscardDeviceResources(){// 按创建的逆序释放SafeRelease(g_pBrush);SafeRelease(g_pRenderTarget);}voidOnPaint(HWND hwnd){CreateDeviceResources(hwnd);if(g_pRenderTargetNULL)return;g_pRenderTarget-BeginDraw();g_pRenderTarget-Clear(D2D1::ColorF(0.15f,0.15f,0.15f,1.0f));// ... 绘制代码 ...HRESULT hrg_pRenderTarget-EndDraw();if(hrD2DERR_RECREATE_TARGET){DiscardDeviceResources();InvalidateRect(hwnd,NULL,FALSE);// 立即触发重绘以重建资源}}// SafeRelease 辅助函数templateclassTvoidSafeRelease(T**ppT){if(*ppT){(*ppT)-Release();*ppTNULL;}}这个模式的核心思想是延迟创建 按需重建。CreateDeviceResources在每次OnPaint时检查资源是否存在不存在才创建。DiscardDeviceResources在设备丢失时释放所有设备相关资源。下次OnPaint时CreateDeviceResources发现资源为NULL会自动重建。注意ID2D1Factory是设备无关资源不需要在设备丢失时重建。完整的最小 Direct2D 程序下面是一个可以直接编译运行的完整示例#includewindows.h#included2d1.h#includecmath#pragmacomment(lib,d2d1.lib)templateclassTvoidSafeRelease(T**ppT){if(*ppT){(*ppT)-Release();*ppTNULL;}}ID2D1Factory*g_pFactoryNULL;ID2D1HwndRenderTarget*g_pRTNULL;ID2D1SolidColorBrush*g_pBrushNULL;floatg_angle0.0f;voidCreateDeviceResources(HWND hwnd){if(g_pRT)return;RECT rc;GetClientRect(hwnd,rc);g_pFactory-CreateHwndRenderTarget(D2D1::RenderTargetProperties(),D2D1::HwndRenderTargetProperties(hwnd,D2D1::SizeU(rc.right,rc.bottom)),g_pRT);if(g_pRT)g_pRT-CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White),g_pBrush);}voidDiscardDeviceResources(){SafeRelease(g_pBrush);SafeRelease(g_pRT);}voidOnPaint(HWND hwnd){CreateDeviceResources(hwnd);if(!g_pRT)return;g_pRT-BeginDraw();g_pRT-Clear(D2D1::ColorF(0.1f,0.1f,0.15f,1.0f));RECT rc;GetClientRect(hwnd,rc);floatcx(rc.right-rc.left)/2.0f;floatcy(rc.bottom-rc.top)/2.0f;floatr80.0f;// 绘制旋转的三角形D2D1_POINT_2F points[3];for(inti0;i3;i){floatag_anglei*(2.0f*3.14159f/3.0f);points[i]D2D1::Point2F(cxr*cosf(a),cyr*sinf(a));}g_pBrush-SetColor(D2D1::ColorF(0.3f,0.7f,1.0f,0.8f));g_pRT-DrawLine(points[0],points[1],g_pBrush,3.0f);g_pRT-DrawLine(points[1],points[2],g_pBrush,3.0f);g_pRT-DrawLine(points[2],points[0],g_pBrush,3.0f);HRESULT hrg_pRT-EndDraw();if(hrD2DERR_RECREATE_TARGET){DiscardDeviceResources();InvalidateRect(hwnd,NULL,FALSE);}}LRESULT CALLBACKWndProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam){switch(uMsg){caseWM_PAINT:OnPaint(hwnd);ValidateRect(hwnd,NULL);return0;caseWM_TIMER:g_angle0.05f;InvalidateRect(hwnd,NULL,FALSE);return0;caseWM_SIZE:if(g_pRT){g_pRT-Resize(D2D1::SizeU(LOWORD(lParam),HIWORD(lParam)));InvalidateRect(hwnd,NULL,FALSE);}return0;caseWM_DESTROY:DiscardDeviceResources();SafeRelease(g_pFactory);KillTimer(hwnd,1);PostQuitMessage(0);return0;}returnDefWindowProc(hwnd,uMsg,wParam,lParam);}intWINAPIwWinMain(HINSTANCE hInstance,HINSTANCE,PWSTR,intnCmdShow){D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,g_pFactory);constwchar_tcls[]LD2DDemo;WNDCLASS wc{};wc.lpfnWndProcWndProc;wc.hInstancehInstance;wc.lpszClassNamecls;wc.hCursorLoadCursor(NULL,IDC_ARROW);RegisterClass(wc);HWND hwndCreateWindow(cls,LDirect2D 旋转三角形,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,600,500,NULL,NULL,hInstance,NULL);ShowWindow(hwnd,nCmdShow);SetTimer(hwnd,1,16,NULL);// ~60fpsMSG msg;while(GetMessage(msg,NULL,0,0)){TranslateMessage(msg);DispatchMessage(msg);}return0;}常见问题与调试问题1D2D1CreateFactory 返回 E_FAIL这通常意味着系统中没有安装 Direct2D 运行时。Direct2D 需要 Windows 7 SP1 平台更新或更高版本。在 Windows Vista 或未打补丁的 Windows 7 上D2D1CreateFactory会失败。检查你的目标系统版本要求。问题2BeginDraw / EndDraw 外调用绘制方法这是一个静默失败的场景——不会报错不会崩溃但画面上什么都不会出现。如果你发现DrawRectangle、FillEllipse等方法不工作首先检查你是在BeginDraw和EndDraw之间调用它们的。问题3画面闪烁如果你在WM_PAINT处理中既用 GDIBeginPaint/EndPaint又用 Direct2D可能会出现闪烁。因为 GDI 的BeginPaint会擦除背景而 Direct2D 的BeginDraw不会。解决办法是只使用 Direct2D 绘制或者处理WM_ERASEBKGND返回TRUE阻止背景擦除。总结到这里我们已经把 Direct2D 的基础架构梳理清楚了。核心概念是资源的两级分类设备无关 vs 设备相关、BeginDraw/EndDraw的绘制框架、以及D2DERR_RECREATE_TARGET的设备丢失处理。这些是你使用 Direct2D 时每天都在打交道的模式。和 GDI 相比Direct2D 的初始化确实更复杂——需要创建工厂、创建渲染目标、创建画刷而且还要处理 COM 引用计数和设备丢失。但这些都是一次性的模板代码复制粘贴即可。真正的好处在于从今天开始你所有的 2D 绘制都由 GPU 加速了。下一步我们要深入 Direct2D 的几何体系统。Direct2D 提供了比 GDI 丰富得多的几何体类型——从简单的矩形和椭圆到贝塞尔曲线和布尔运算组合的复杂路径。更重要的是Direct2D 的几何体支持精确的命中测试这是构建交互式图形应用的基础。练习用 Direct2D 重写之前 GDI 的双缓冲动画示例弹跳球对比两者的代码结构和运行性能。实现一个渐变背景窗口使用ID2D1LinearGradientBrush从窗口顶部到底部绘制蓝色到紫色的渐变。处理WM_DPICHANGED消息在 DPI 变化时正确调整渲染目标的大小和画刷参数。实现设备丢失的自动恢复在EndDraw返回D2DERR_RECREATE_TARGET后重建所有资源并重绘确保用户看不到任何闪烁或中断。参考资料:D2D1CreateFactory function - Microsoft LearnID2D1HwndRenderTarget interface - Microsoft LearnID2D1RenderTarget::BeginDraw - Microsoft LearnID2D1SolidColorBrush - Microsoft LearnHandling Device Loss in Direct2D - Microsoft LearnDirect2D and GDI Interoperability Overview - Microsoft Learn相关阅读通用GUI编程技术——图形渲染实战二十五——Alpha混合与透明效果分层窗口实战 - 相似度 100%通用GUI编程技术——图形渲染实战二十六——GDI与GDI架构差异抗锯齿与渐变 - 相似度 100%通用GUI编程技术——Win32 原生编程实战二十二——GDI 位图操作BitBlt、StretchBlt 与图像处理 - 相似度 80%
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2506319.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!