Direct3D 11图形绘制从零到三角形:GPU渲染管线、HLSL着色器与调试技巧
发布时间:2026/9/29 1:22:04来源:尧图网络
简介一套基于VC与Direct3D的图形绘制示例工程面向三维图形初学者及希望快速掌握DirectX渲染开发的程序员。工程采用MFC程序框架并搭配Ribbon菜单界面适合触屏操作功能按模块集中组织避免了传统级联菜单的繁琐查找便于开发时快速调用。资源压缩包共35个文件大小仅279KB主要包含.cpp源码、.h头文件、.bmp位图纹理、.ico图标以及.sln、.vcxproj、.rc等工程配置文件覆盖了程序入口、文档视图、主框架、资源定义等完整组成。通过阅读源码可学习Direct3D基础绘制流程与图元渲染方法参考MFC如何集成Direct3D以及Ribbon界面的实现方式具备不错的迁移价值。目前已有86人浏览学习适合在Visual C环境下开展三维图形编程练习与功能扩展。1. Direct3D图形绘制空有 GPU 却画不出一个三角形问题出在哪有没有过这样的时刻程序跑起来窗口正常打开回调函数也都执行了但你想用 Direct3D 图形绘制画一个三角形屏幕上就是一片黑没有报错没有异常只有一种“我到底该调哪个函数”的茫然。其实 Direct3D 不是“画一个矩形”那样的 2D API而是一条完整的 GPU 渲染流水线设备与交换链、顶点缓冲、着色器、输入布局、绘制状态哪一个环节没接上结果都是黑屏。下面就是照着这条流水线拆告诉你每步的参数含义、常见失败长什么样以及如何用抓帧工具把黑匣子打开。适合刚接触渲染的 C 开发者也适合做工业可视化、CAD 预览和游戏编辑器基础模块的人。2. 初始化 Direct3D 11 设备从窗口句柄到清屏的完整配置在 Direct3D 图形绘制里设备Device和交换链SwapChain是所有工作的地基。设备负责创建 GPU 资源交换链负责把渲染结果呈现到窗口。两者通常一次创建后面很少动所以花点时间把参数吃透非常值得。2.1 为什么选 Direct3D 11保留模式是多数图形绘制的默认起点常见问题是“我要做 Direct3D 图形绘制该选 11 还是 12”我的建议是除非你有明确的底层需求否则先用 11。Direct3D 11 是保留模式创建资源、绑定渲染状态这些操作由运行时和驱动帮着管理你只需要按顺序调用 API状态错误会由调试层给出提示。Direct3D 12 需要自己管理命令列表、资源屏障、描述符堆和内存分配性能上限更高但开发复杂度也高了一个量级。从实际项目看游戏编辑器、工业可视化、桌面端渲染工具大量直接基于 11 或它的后继版本开发教程和技术资料最多。OpenGL 虽然跨平台但 Windows 上不同显卡厂商的驱动实现差异很让人头疼尤其是旧显卡。Vulkan 更底层适合写引擎而不是做业务。所以在一个产品里做 Direct3D 图形绘制D3D11 是低成本、兼容性好的起点。2.2 创建设备和交换链D3D11CreateDeviceAndSwapChain 的参数细节创建部分用一个函数完成代码并不长但参数需要逐项理解。这是一个最简的创建设备和交换链的片段DXGI_SWAP_CHAIN_DESC sd {}; sd.BufferDesc.Width 800; sd.BufferDesc.Height 600; sd.BufferDesc.Format DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator 60; sd.BufferDesc.RefreshRate.Denominator 1; sd.SampleDesc.Count 1; sd.SampleDesc.Quality 0; sd.BufferUsage DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.BufferCount 2; sd.OutputWindow hwnd; sd.Windowed TRUE; sd.SwapEffect DXGI_SWAP_EFFECT_DISCARD; D3D_FEATURE_LEVEL levels[] { D3D_FEATURE_LEVEL_11_0 }; ID3D11Device* device nullptr; ID3D11DeviceContext* context nullptr; IDXGISwapChain* swapChain nullptr; HRESULT hr D3D11CreateDeviceAndSwapChain( nullptr, // pAdapter传 nullptr 用默认适配器 D3D_DRIVER_TYPE_HARDWARE, // 使用硬件 GPU nullptr, // 软件驱动模块硬件模式不需要 D3D11_CREATE_DEVICE_DEBUG, // 打开调试层 levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, sd, swapChain, device, nullptr, context); if (FAILED(hr)) { // 不要把 hr 吞掉至少用 OutputDebugString 打印 return hr; }这里有几个关键参数值得记。BufferDesc.Format 用 R8G8B8A8_UNORM这是最常见的 8 位 RGBA 颜色格式窗口显示和后面的着色器都认它。BufferCount 是后台缓冲数量2 就是双缓冲可以降低画面撕裂出现的概率。SampleDesc.Count 是多重采样数如果不需要 MSAA设为 1 就是 1x 采样。OutputWindow 必须是你真正创建出来的窗口句柄不能为空。D3D11_CREATE_DEVICE_DEBUG 这个标志只在开发阶段使用。打开后调试层会把不合法调用、资源绑定错误等写成一条条 D3D11 ERROR 输出到 OutputDebugString。很多黑屏问题在 Release 模式下看起来毫无证据打开这个标志后才会看到线索。如果你需要兼顾老显卡FeatureLevel 可以设置成 D3D_FEATURE_LEVEL_10_0 甚至更早的级别但代价是有些新着色器功能不可用。现代机器上直接请求 11_0 就够如果你的显卡不支持 11D3D11CreateDeviceAndSwapChain 会返回 E_INVALIDARG 或类似错误。另一个常见报错是 DXGI_SWAP_CHAIN_DESC 里有字段没清零所以声明时用{}初始化很重要否则结构体里的随机值会让函数返回莫名奇妙的失败。2.3 绑定渲染目标视图与视口让清屏颜色真正落到窗口设备建好后你还看不到任何东西必须把交换链里的后台缓冲“变成”一个渲染目标视图Render Target View简称 RTV并设置视口。否则就算清了屏窗口仍然是黑的。所以这段代码要紧跟创建// 取交换链的第 0 张后台缓冲纹理 ID3D11Texture2D* backBuffer nullptr; swapChain-GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)backBuffer); // 为这张纹理创建渲染目标视图 ID3D11RenderTargetView* rtv nullptr; device-CreateRenderTargetView(backBuffer, nullptr, rtv); backBuffer-Release(); // 纹理引用已经不再需要尽早释放 // 把 RTV 绑定到输出合并阶段 context-OMSetRenderTargets(1, rtv, nullptr); // 设置视口默认覆盖整个窗口客户区 D3D11_VIEWPORT vp { 0.0f, 0.0f, 800.0f, 600.0f, 0.0f, 1.0f }; context-RSSetViewports(1, vp); // 清屏。颜色值在 [0,1] 范围内 float clearColor[4] { 0.1f, 0.2f, 0.4f, 1.0f }; context-ClearRenderTargetView(rtv, clearColor); // 把后台缓冲交换到前台参数 1 表示等待垂直同步 swapChain-Present(1, 0);GetBuffer 返回的是后台缓冲所在的纹理对象CreateRenderTargetView 会从这张纹理里创建视图。注意 backBuffer 要 Release否则这个引用会一直把资源留在显存里。RTV 在后续每次 Resize 后都要重建可以先留着但窗口大小变化时必须处理。OMSetRenderTargets 是输出合并阶段它告诉渲染管线“最终像素写到哪个视图”。参数里的 nullptr 是深度模板视图这时还没有创建。如果你的场景马上需要 3D 遮挡必须创建一张同分辨率的深度纹理和深度模板视图并把它的指针放在 OMSetRenderTargets 的第二个参数里否则深度测试永远通过后面的遮挡关系会完全错乱。RSSetViewports 决定像素从 NDC 坐标映射到窗口坐标的范围。如果视口宽度、高度和实际客户区不匹配画面会拉伸或偏移。清屏后的 Present 才会把颜色刷到窗口。如果这时的窗口还不是目标颜色问题大概率在下钻设备句柄无效、没有调用 Present、窗口句柄传错。这一步跑通后你的 Direct3D 图形绘制就有了一个“会清屏”的窗口。接下来就可以让 GPU 画点真正的东西。3. 绘制三角形顶点缓冲、输入布局与 HLSL 着色器绑定有了窗口和渲染目标下一步是让画面出现三角形。这一章是 Direct3D 图形绘制的核心节奏CPU 顶点数据 → GPU 顶点缓冲 → 输入布局 → 顶点/像素着色器 → Draw 调用。3.1 顶点结构位置和颜色怎么排进一块 GPU 内存在 Direct3D 中顶点不是简单的“x, y, z”三个 float。你可以给顶点附加颜色、法线、UV 等任何属性但代价是你必须告诉 GPU 这些属性怎么排列。这里先定义最常见的“位置 颜色”结构struct Vertex { DirectX::XMFLOAT3 pos; // 位置 DirectX::XMFLOAT4 color; // 颜色 RGBA };为什么位置用 XMFLOAT3 而颜色用 XMFLOAT4位置在后续会补上 w1颜色需要包含 alpha所以四个分量。这个结构体在内存里是连续的 3×4 4×4 28 字节后面输入布局会根据这个排布来取数。接着把三个顶点放进 GPU 内存。顶点数据一般用 D3D11_USAGE_DEFAULT表示内容不变一次上传如果每帧都要改位置后面再用 DYNAMIC。创建顶点缓冲的代码如下Vertex vertices[] { { { -0.5f, -0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } }, // 左下红 { { 0.0f, 0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } }, // 上方绿 { { 0.5f, -0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }, // 右下蓝 }; D3D11_BUFFER_DESC vbDesc {}; vbDesc.ByteWidth sizeof(vertices); // 整个缓冲区的字节数 vbDesc.Usage D3D11_USAGE_DEFAULT; // 静态数据不常更新 vbDesc.BindFlags D3D11_BIND_VERTEX_BUFFER; D3D11_SUBRESOURCE_DATA initData {}; initData.pSysMem vertices; // 指向 CPU 内存的初始数据 ID3D11Buffer* vertexBuffer nullptr; device-CreateBuffer(vbDesc, initData, vertexBuffer);这段代码里最关键的是 ByteWidth 和 BindFlags。很多人在内存里把顶点数组准备好但 ByteWidth 算错导致画出来的图形缺顶点或多顶点。这里使用 sizeof(vertices)也就是 3 × 28 84 字节。BindFlags 明确这是一块顶点缓冲不能用同一资源既当顶点缓冲又当索引缓冲除非把 D3D11_BIND_INDEX_BUFFER 也加进标记但没必要。顶点坐标在 NDC规范设备坐标里取值从 -1 到 1超出这个范围的部分会被裁剪掉。上面的三个坐标正好构成一个等腰三角形刚好落在可见区域内所以你能看到它。3.2 编译 HLSL 着色器与创建输入布局让 GPU 知道数据从哪里取顶点缓冲只是内存GPU 还不知道每个分量应该从哪里读。这就是输入布局Input Layout的作用把 C 结构体的内存布局和 HLSL 着色器的输入语义一一对应起来。先写一个最简的 HLSL 文件 shader.hlslstruct VSIn { float3 pos : POSITION; float4 color : COLOR; }; struct PSIn { float4 pos : SV_POSITION; float4 color : COLOR; }; PSIn VS(VSIn input) { PSIn output; output.pos float4(input.pos, 1.0f); output.color input.color; return output; } float4 PS(PSIn input) : SV_TARGET { return input.color; }这个 shader 没有做任何变换只是把位置扩展成齐次坐标把颜色透传。接下来要在 C 中编译它并创建输入布局ID3DBlob* vsBlob nullptr; ID3DBlob* errBlob nullptr; HRESULT hrVS D3DCompileFromFile( Lshader.hlsl, nullptr, nullptr, VS, vs_5_0, // 入口点 VS着色器模型 5.0 D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, vsBlob, errBlob); if (FAILED(hrVS)) { if (errBlob) OutputDebugStringA((char*)errBlob-GetBufferPointer()); return hrVS; } ID3DBlob* psBlob nullptr; HRESULT hrPS D3DCompileFromFile( Lshader.hlsl, nullptr, nullptr, PS, ps_5_0, D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, 0, psBlob, errBlob); if (FAILED(hrPS)) { if (errBlob) OutputDebugStringA((char*)errBlob-GetBufferPointer()); return hrPS; } ID3D11VertexShader* vs nullptr; ID3D11PixelShader* ps nullptr; device-CreateVertexShader(vsBlob-GetBufferPointer(), vsBlob-GetBufferSize(), nullptr, vs); device-CreatePixelShader(psBlob-GetBufferPointer(), psBlob-GetBufferSize(), nullptr, ps); // 输入布局两个元素分别对应 VSIn 的 POSITION 和 COLOR D3D11_INPUT_ELEMENT_DESC layout[] { { POSITION, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { COLOR, 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; ID3D11InputLayout* inputLayout nullptr; device-CreateInputLayout(layout, 2, vsBlob-GetBufferPointer(), vsBlob-GetBufferSize(), inputLayout);这里有两个容易翻车的点。第一入口名必须和 HLSL 里的函数名一致D3DCompileFromFile 如果找不到入口返回的错误信息会误导你可能只给一个 E_FAIL。第二输入布局中的语义名称必须和 HLSL 结构体里的语义匹配大小写也要一致。如果 POSITION 写成了 POS顶点会丢失或被跳过结果通常是画面不出东西但调试层会告诉你“未绑定的语义”。Offset 12 是因为 XMFLOAT3 占 12 字节颜色从第 12 字节开始。如果你改了顶点结构这两个 offset 必须同步改这也是一个常见的黑屏来源。vsBlob 在创建完 VertexShader 和 InputLayout 后就可以 Release这里为了简洁没有写真实项目里要记得释放。3.3 一次 Draw 调用的完整拼装图形绘制的最小闭环现在资源和 shader 都有了可以发起绘制。但 Direct3D 要求把这些资源按阶段绑定好顺序不能乱。UINT stride sizeof(Vertex); UINT offset 0; // 输入装配器绑顶点缓冲、输入布局、图元拓扑 context-IASetVertexBuffers(0, 1, vertexBuffer, stride, offset); context-IASetInputLayout(inputLayout); context-IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 绑定着色器 context-VSSetShader(vs, nullptr, 0); context-PSSetShader(ps, nullptr, 0); // 绘制 3 个顶点0 表示起始索引 context-Draw(3, 0);Draw(3, 0) 看起来简单但前面每一步都影响它是否能正确运行。顶点缓冲没有绑定Draw 调用会认为没有顶点数据结果是屏幕保持清屏颜色输入布局没有设置数据读取会错乱甚至崩溃图元拓扑如果设置成 POINTLIST你会看到三个孤立的点而不是三角形。这些状态都是持久的所以调试时经常出现“这次改了前一次的状态没重置”的诡异现象。到这里你已经用 Direct3D 图形绘制完成了一个真正的 GPU 渲染闭环从顶点数据到像素输出。跑起来后窗口里应该出现一个彩色三角形。如果没出现别急着改矩阵先回到 3.1 检查顶点坐标和输入布局因为大多数黑屏问题出在数据格式没对齐而不是 GPU 坏了。4. 让图形动起来矩阵变换、常量缓冲与帧循环三角形能显示接下来一定想让场景动起来或者至少让物体有透视效果。Direct3D 图形绘制里“动”是所有顶点在 GPU 上乘上矩阵的过程。4.1 世界/视图/投影矩阵的坐标约定和 HLSL 侧写法D3D11 使用左手坐标系即 Z 指向屏幕内。常规的变换分三步世界矩阵把物体放到场景里视图矩阵把摄像机对准目标投影矩阵把视锥内的物体映射到 NDC。用 DirectXMath 可以直接写出using namespace DirectX; XMMATRIX world XMMatrixIdentity(); XMMATRIX view XMMatrixLookAtLH( XMVectorSet(0.0f, 0.0f, -3.0f, 1.0f), // 眼睛位置 XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f), // 目标点 XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)); // 上方向 XMMATRIX proj XMMatrixPerspectiveFovLH( XMConvertToRadians(60.0f), // 垂直视场角 800.0f / 600.0f, // 宽高比 0.1f, 100.0f); // 近裁面、远裁面view 矩阵里的 eye 位置在 -3表示摄像机往后挪 3 个单位这样原本在 z0 的三角形会落在可见范围内。PerspectiveFovLH 的第一个参数是视场角超过 90 度会产生严重的透视畸变nearZ 不要设成 0否则深度计算会出问题。宽高比应该来自窗口当前客户区的宽高硬编码 800/600 只是演示。HLSL 侧需要声明一个常量缓冲Constant Buffer用来接收这些矩阵。习惯上把它放在 register(b0)对应 C 绑定的 0 号常量缓冲槽cbuffer MatrixBuffer : register(b0) { float4x4 world; float4x4 view; float4x4 projection; };然后在顶点着色器里把矩阵乘到顶点位置output.pos mul(float4(input.pos, 1.0f), mul(world, mul(view, projection)));这里用的是行主序乘法把向量当作行向量从左往右乘。DirectXMath 里的 XMMATRIX 默认也是行主序所以这种写法在概念上是顺的。如果你在 HLSL 里改成列主序写法所有矩阵需要先转置再传这一点后面会专门讲。4.2 动态常量缓冲每帧更新矩阵的正确姿势常量缓冲不能像普通 CPU 变量那样直接传必须先创建 GPU 资源然后在每帧用 Map/Unmap 更新。对于每帧变化的矩阵一般用 D3D11_USAGE_DYNAMIC 并配合 D3D11_MAP_WRITE_DISCARD意思是让驱动丢弃旧内容直接给你一块新内存struct MatrixData { XMMATRIX world; XMMATRIX view; XMMATRIX proj; }; D3D11_BUFFER_DESC cbDesc {}; cbDesc.ByteWidth sizeof(MatrixData); cbDesc.Usage D3D11_USAGE_DYNAMIC; cbDesc.BindFlags D3D11_BIND_CONSTANT_BUFFER; cbDesc.CPUAccessFlags D3D11_CPU_ACCESS_WRITE; ID3D11Buffer* constantBuffer nullptr; device-CreateBuffer(cbDesc, nullptr, constantBuffer); // 绑定到顶点着色器的 b0 槽 context-VSSetConstantBuffers(0, 1, constantBuffer);每帧更新时先把矩阵数据填到 CPU 结构体再映射到 GPU 地址MatrixData data; data.world XMMatrixTranspose(world); data.view XMMatrixTranspose(view); data.proj XMMatrixTranspose(proj); D3D11_MAPPED_SUBRESOURCE mapped {}; context-Map(constantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, mapped); memcpy(mapped.pData, data, sizeof(data)); context-Unmap(constantBuffer, 0);Map 之后必须 Unmap否则后续帧会一直占用这块显存严重时造成每帧显存上涨。WRITE_DISCARD 只对 DYNAMIC 缓冲有效这也是为什么创建时不图省事用 DEFAULT。如果你用的是静态缓冲UpdateSubresource 是另一个选择但它在每帧更新时可能造成额外的拷贝。4.3 矩阵要不要转置一条随时能查的规则矩阵转置是 Direct3D 图形绘制里最容易让新人栽跟头的地方。原因在于 DirectXMath 的矩阵按行主序存储而 HLSL 的 float4x4 默认按列主序解释。两者相差一个转置。最常见的做法是在写入常量缓冲前先把所有矩阵用 XMMatrixTranspose 转一次就像上面的代码那样。如果你不想每次转可以在 HLSL 里把矩阵声明为 row_majorcbuffer MatrixBuffer : register(b0) { row_major float4x4 world; row_major float4x4 view; row_major float4x4 projection; };这会把 HLSL 的解释方式改成和 C 一致。两种做法都行我倾向于统一转置因为网上大多数示例和 SDK 示例都这么做踩坑时对照示例更容易找到问题。一个简单的判断方法先设 world 为单位矩阵如果三角形消失了多半是 world 相关计算有问题如果 world 正常但透视不对重点检查 view 和 proj 的转置。矩阵里的数值你要能读得懂调试时把矩阵打印出来看最后一行的 0 0 0 1 是不是还在如果转置后对角线镜像了一眼就能认出来。这一章把变换链打通就能看到三角形随摄像机移动或在三维空间里旋转变大变小。接下来是更现实的环节出问题时怎么排查。5. Direct3D 图形绘制避坑五个最容易翻车的现场与修复顺序前几章看起来顺利但真实渲染项目里十个问题有九个不是算法而是状态或生命周期问题。这里记录五个最常见的翻车现场和排查方式每一条我都实际遇到过。5.1 画面全黑但 HRESULT 全对现象程序正常运行窗口有清屏颜色但三角形始终不显示。检查了所有 Create 函数HRESULT 都是 S_OK调试层也没有报错误。这才是最让人疯掉的情况因为看起来一切正常画面却不对。原因最常见的是三种。一是顶点坐标没有落在 NDC 范围内比如你忘了做透视除法导致位置跑到了视锥外二是输入布局的 Offset 和顶点结构不一致数据被错位读取GPU 拿到了一堆垃圾坐标三是默认的背面剔除把三角形裁掉了。D3D11 默认开启背面剔除如果你的顶点是按顺时针排列的在屏幕坐标 y 向下时三角形会被认为面朝背面。解决先关掉剔除做排除。创建一个 D3D11_RASTERIZER_DESC 填充模式为 Solid、裁剪模式为 None 的光栅化状态然后 RSSetState 绑定它。如果三角形马上出现就把顶点顺序换一下而不是继续关闭剔除。还要逐项打印顶点坐标看每个分量的范围是否在 -1 到 1 之间。HRESULT 全对只表示 API 调用没有失败不代表数据语义正确。另一个我后来才意识到的点如果顶点着色器只是简单透传而 MVP 矩阵因为没初始化导致全为 0所有顶点会挤到窗口中心看起来和没画一样。矩阵是否初始化必须成为黑屏排查的固定动作。5.2 着色器编译失败只给一行 E_FAIL现象调用 D3DCompileFromFile 返回 E_FAIL程序逻辑上如果 FAILED(hr) 直接 return后面什么都看不见。你以为是文件路径问题把 shader.hlsl 放到 exe 同目录下还是一样。原因入口函数名和实际 HLSL 函数名不一致shader model 版本不支持还有可能是 HLSL 语法错误但 errBlob 没有被正确读取。很多人只检查 hr 是否失败却忘了看错误 Blob等于把 GPU 编译器给你的报错信息扔掉了。解决在 D3DCompileFromFile 后面加上这段if (FAILED(hr) errBlob) { const char* msg static_castconst char*(errBlob-GetBufferPointer()); OutputDebugStringA(msg); // 在 VS 输出窗口里看 }OutputDebugStringA 会把错误信息输出到调试器输出窗口。你会看到类似“error X3000: syntax error”这种带具体位置的报错通常一行就能定位。另外D3DCompileFromFile 的路径是相对于当前工作目录的如果用 IDE 调试工作目录不一定是 exe 所在目录最好用绝对路径或者把 shader 放进项目。5.3 窗口拉伸后画面撕裂、变形现象窗口最大化或拉大后原来正常的三角形变得拉伸变形甚至闪烁、撕裂。重启程序又正常一旦 resize 就坏。原因交换链的后台缓冲尺寸还是创建时的 800×600窗口客户区已经变成了别的尺寸。你没有处理 WM_SIZE后台缓冲和视口尺寸不匹配它就会被拉伸回屏幕大小画面比例就错了。解决在窗口过程的 WM_SIZE 分支里处理。注意顺序很重要先释放掉当前 RTV才能 ResizeBuffers因为 RTV 正引用着后台缓冲纹理case WM_SIZE: if (swapChain) { if (rtv) { rtv-Release(); rtv nullptr; } UINT w LOWORD(lParam); UINT h HIWORD(lParam); swapChain-ResizeBuffers(0, w, h, DXGI_FORMAT_UNKNOWN, 0); ID3D11Texture2D* bb nullptr; swapChain-GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)bb); device-CreateRenderTargetView(bb, nullptr, rtv); bb-Release(); D3D11_VIEWPORT vp { 0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f }; context-RSSetViewports(1, vp); } break;ResizeBuffers 第一个参数为 0 表示保持原来的缓冲数量。处理完 WM_SIZE 后每帧清屏颜色会正确铺满新窗口。撕裂问题本身则和垂直同步有关Present(1,0) 会等待垂直同步能明显减轻撕裂如果不追求刷新率保持 Present(1,0) 是更稳妥的选项。5.4 显存每帧上涨找不到泄漏点的“无头案”现象程序运行时任务管理器里 GPU 显存占用一直涨涨到一定程度程序卡死或设备丢失。你会说“我每帧都在同一个缓冲上更新哪来的泄漏”原因真实泄漏往往不在长期存在的缓冲而在“每帧创建的临时对象”。比如每帧调用了 CreateBuffer 创建临时常量缓冲用完没有 Release每帧 Map 后没有 Unmap每帧创建纹理后没释放。COM 接口都需要手动 Release没有 GC。还有更隐蔽的创建了渲染目标视图和深度模板视图放在局部变量里离开作用域后没有释放。解决把资源生命周期管理交给 ComPtr。如果项目已经用了 C用 Microsoft::WRL::ComPtr 声明所有 D3D 资源析构时自动释放。开启调试层后程序退出前调用 ReportLiveDeviceObjects#ifdef _DEBUG ID3D11Debug* d3dDebug nullptr; device-QueryInterface(__uuidof(ID3D11Debug), (void**)d3dDebug); d3dDebug-ReportLiveDeviceObjects(D3D11_RLDO_DETAIL); d3dDebug-Release(); #endif需要包含 d3d11sdklayers.h。ReportLiveDeviceObjects 会把所有没释放的 COM 对象类型和名称输出到调试窗口。看到输出里频繁出现 Buffer 或 Texture你就能顺着去查对应创建代码。还要注意Map 返回的 pData 是驱动给你的一块临时映射地址不能长期保留并跨帧使用有人把 pData 存在全局变量里下一帧接着读结果画面走样还查不出来。这条习惯能省下好几天的排查时间。5.5 Device Removed显卡驱动把设备弄丢了一次现象程序运行十几分钟或几小时后某次 Draw 调用返回 DXGI_ERROR_DEVICE_REMOVED或者 Present 直接崩溃。你以为是自己代码 bug但重启程序后又能跑好久。原因设备被移除通常不是你的代码直接报错而是显卡驱动检测到 GPU 长时间无响应主动重置了 GPU。触发因素包括着色器里有死循环导致 GPU 挂起、一次绘制规模过大引发超时、显存不足被系统回收。也有物理原因GPU 过热、供电不够。解决出现这种错误后先查原因HRESULT reason device-GetDeviceRemovedReason(); if (reason DXGI_ERROR_DEVICE_HUNG) { // 考虑 shader 或 draw 规模是否过重 } else if (reason DXGI_ERROR_DEVICE_REMOVED) { // 驱动更新或重置 }从代码上最有效的手段是“不要挑战 GPU”。避免在同一个 draw 中处理过多的顶点和像素shader 里的循环要有明确的退出条件降低后处理分辨率。程序层面要做好设备丢失恢复释放所有随设备创建的资源重新走一遍第 2 章的初始化流程。但最好的解法是防止它发生毕竟用户不会愿意看到画面黑一下又不明不白地恢复。如果把这五条按实际发生频率排序黑屏最高其次是着色器编译然后是 Resize显存和 Device Removed 问题通常是运行一段时间后才暴露。所以我的排查顺序永远是先查数据格式再查状态绑定最后才怀疑硬件。6. 用 PIX 验证 Direct3D 绘制结果抓帧、Overdraw 与我的调试习惯PIX 是微软官方的图形调试工具也是我遇到疑难问题时最后打开的“后悔药”。它能把一帧画面里所有的 DrawCall、状态、资源都拆开给你看相当于把 Direct3D 图形绘制的黑匣子直接打开。6.1 抓帧后先从哪一步看起用 PIX 捕获一帧后先不要急着看性能。找到 Event 列表里的 DrawIndexed 或 Draw点击它然后在 Pipeline Stages 视图里查看 Input Assembler 阶段的顶点数据。你能直接读到每个顶点的坐标和颜色如果顶点坐标不是预期值问题在 C 侧的数据准备或常量缓冲如果坐标正确但输出不对再点开 VS Stage 看 SV_Position 输出。这个步骤能在三分钟内把问题定位到“CPU 数据坏”还是“GPU 处理坏”。6.2 Overdraw 和 Draw Call 怎么看PIX 的统计面板会显示像素着色器执行的次数。如果 PS 执行次数远超屏幕像素数说明存在严重的 Overdraw通常是大量半透明物体叠加或遮挡剔除不合理。Draw Call 数量同样可以统计几千个批次会让 CPU 开销过大。但我要提醒你性能优化一定要在功能正确之后否则只是给一个坏结果提速。6.3 我的调试顺序先看状态绑定再谈优化我的习惯是每次画面不对先进 PIX 看三件事——IA 的顶点输入、VS 输出、PS 输出。确认这三步没问题再去检查 RTV、视口、常量缓冲和转置。前几年有一次调矩阵调了两天最后抓帧发现 VS 输出的坐标全部落在屏幕外因为我在某个分支里给投影矩阵传了一个全 0 矩阵。如果一开始就抓帧这一步半小时就能查完。后来我养成了“先抓帧、后改代码”的习惯省下来的时间足够再做一个功能。Direct3D 图形绘制难在状态多但每条状态都能通过工具验证找到属于自己的检查顺序就不会再靠玄学调参。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网