尧图精选

DX12入门避坑:从Device到贴图三角形的25集实战

🕒 发布时间:2026/9/17 14:38:11 📁 来源:尧图网络
1. 老教程一上手就卡住问题多半出在环境判断上如果你最近在搜索引擎里敲过DX12入门八成会翻到那批2016年前后写的老文章。它们有个共同特点代码框架看着完整但照着敲下来你会在D3D12CreateDevice这一步就收到一个E_FAIL调试层还一声不吭。我第一次遇到这个情况时怀疑过VS版本、怀疑过Windows SDK、甚至重装过系统最后才发现是枚举适配器时把核显当成了默认设备而那块核显根本不支持DX12的feature level。图形API实战这类内容最怕的就是代码给全了、场景没交代。DX12本身没有错它只是比DX11更诚实——它不会再帮你兜底所有该你选的、该你管的东西它一律不替你做主。所以这套从Device到贴图三角形的25集我想按真实动手的顺序重写一遍把每一步为什么这么写和卡住时怎么查都放进去而不是把官方文档翻译成中文了事。1.1 老教程的代码在新环境里为什么会崩最典型的一个差异是适配器选择。老教程里常见这么一句ComPtrIDXGIFactory4 factory; CreateDXGIFactory1(IID_PPV_ARGS(factory)); ComPtrIDXGIAdapter adapter; factory-EnumAdapters(0, adapter); // 直接取第0个 D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(device));EnumAdapters(0)取的是系统枚举的第一个适配器而在很多笔记本上第一个往往是Intel核显。核显对DX12的支持程度取决于型号和驱动部分老核显只到feature level 11_0甚至更低D3D12CreateDevice自然返回失败。正确的做法是用EnumAdapters1遍历跳过DXGI_ADAPTER_FLAG_SOFTWARE再用一次探测式调用确认它能不能创建deviceComPtrIDXGIFactory6 factory; CreateDXGIFactory2(0, IID_PPV_ARGS(factory)); ComPtrIDXGIAdapter1 adapter; for (UINT i 0; factory-EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(adapter)) ! DXGI_ERROR_NOT_FOUND; i) { DXGI_ADAPTER_DESC1 desc; adapter-GetDesc1(desc); if (desc.Flags DXGI_ADAPTER_FLAG_SOFTWARE) continue; if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), nullptr))) { break; // 找到了能用的硬件适配器 } }注意最后那个D3D12CreateDevice的第三个参数传的是__uuidof(ID3D12Device)ppDevice传nullptr。这是一种我只想问问它行不行不真的创建设备的探测写法探测成功之后再正式创建。EnumAdapterByGpuPreference是IDXGIFactory6的新接口它能按高性能优先帮你把独显排前面比手写一堆判断省事得多前提是系统的DXGI版本够新。1.2 判断你的机器到底支持哪个feature levelD3D12CreateDevice的第二个参数是D3D_FEATURE_LEVEL老教程一律写D3D_FEATURE_LEVEL_11_0。这没错DX12的最低门槛就是11_0但如果你想用12_1、12_2里的特性比如光线追踪、网格着色器就得先确认设备支不支持。稳妥的写法是先探测再决定降级策略D3D_FEATURE_LEVEL levels[] { D3D_FEATURE_LEVEL_12_2, D3D_FEATURE_LEVEL_12_1, D3D_FEATURE_LEVEL_12_0, D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; D3D_FEATURE_LEVEL selected D3D_FEATURE_LEVEL_11_0; for (auto lv : levels) { if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), lv, __uuidof(ID3D12Device), nullptr))) { selected lv; break; } }我实测在几台不同机器上跑过2018年以后的独显基本都能到12_1核显则经常停在11_0。把这个feature level打印出来或者在窗口标题里显示能省掉大量为什么某个API调用返回E_NOTIMPL的困惑。一个我踩过的坑是有些教程在创建device时用了12_1但后面又调用了需要12_2的接口运行时才崩日志还藏在调试层里。养成先探测、再选最低够用级别的习惯能避开一大半兼容性问题。1.3 这25集按什么节奏推进我给自己定的原则是每集只引入一个新的、必须理解的核心对象绝不一口气塞五个。顺序大致是Device和Adapter起步→ 调试层和验证贯穿全程→ 命令队列/分配器/列表三件套 → 围栏同步 → 交换链与第一帧清屏 → 根签名和PSO → 画三角形 → 上传堆与纹理 → 贴图三角形 → 各种调试工具实操。看起来慢但每一集都能真的跑出画面而不是攒了二十集的代码最后一次性编译。2. Device创建一行调用背后藏着三种典型故障D3D12CreateDevice长得人畜无害但它是我调试时间花得最多的一步。把它单独拎出来讲一节是因为后面所有东西都建立在Device句柄之上这里出问题后面全白搭。2.1 调试层必须一开始就开这是我最想强调的一点也是老教程最容易漏的一步。调试层Debug Layer要在创建device之前启用一旦device创建完成再想开就得重新创建#if defined(_DEBUG) ComPtrID3D12Debug debugController; if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(debugController)))) { debugController-EnableDebugLayer(); // 进一步开启GPU端验证可选性能有损耗 ComPtrID3D12Debug1 debug1; if (SUCCEEDED(debugController.As(debug1))) { debug1-SetEnableGPUBasedValidation(TRUE); } } #endifSetEnableGPUBasedValidation会带来明显的性能下降所以我在正式跑帧率测试时会关掉它但调试阶段一定开着。它会捕获资源状态转换错误、描述符越界、命令列表未关闭就执行等一堆问题这些错误在没有调试层的时候表现为随机黑屏或者设备移除极难定位。2.2 适配器选择里最容易忽略的软件适配器上一节提到的DXGI_ADAPTER_FLAG_SOFTWARE判断很多人会漏。Windows自带一个叫WARP的软件光栅化器它确实支持DX12如果你不加判断地取了枚举列表里的最后一个有可能拿到它。WARP适合跑自动化测试但帧率低得离谱拿它做实时渲染你会发现画面能出就是卡得像幻灯片。实测经验是如果程序在一个没有独显的环境里跑得特别慢先打印适配器名字看看是不是WARP。2.3 创建返回E_FAIL时的排查顺序D3D12CreateDevice返回E_FAIL我不会上来就改代码而是按这个顺序排排查项检查方法常见结论适配器是否选错打印DXGI_ADAPTER_DESC1.Description选到了核显/软件适配器feature level要求过高从11_0开始逐级往上探测设备不支持12_1及以上调试层是否可用D3D12GetDebugInterface是否成功系统未装图形工具组件SDK版本检查Windows SDK安装头文件与运行库不匹配驱动问题更新显卡驱动老驱动对DX12支持不全这张表是我自己踩出来的。有一次E_FAIL查了半天最后发现是系统没装图形工具这个可选组件调试层接口拿不到而代码里又ASSERT了调试层必须成功直接崩了。把调试层的启用加上容错判断是新手最容易忽略的自保措施。2.4 设备对象创建之后立刻做的事device拿到手我通常会做三件事查询能力CheckFeatureSupport、设置调试对象名SetName方便抓帧工具里辨认、以及获取描述符大小GetDescriptorHandleIncrementSize。第三件事在D3D12里尤其重要因为描述符堆是手动管理的RTV、CBV、SRV的大小可能不同提前存下来能避免后面反复查询UINT rtvSize device-GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); UINT cbvSrvSize device-GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);SetName虽然不影响运行但在用抓帧工具时你能在资源列表里看到BackBuffer0MainDepthBuffer这类有意义的名字而不是一堆Resource 0x000001...调试效率差好几倍。3. 命令对象三件套队列、分配器、列表的分工逻辑DX12把命令的录制和命令的执行彻底分开了这是它和DX11最大的区别。理解这个分离才能理解为什么它要这么啰嗦地给你三个对象。3.1 命令队列是GPU的工作台命令队列ID3D12CommandQueue代表一条GPU执行通道。D3D12里有三种类型DIRECT全能图形计算拷贝、COMPUTE只做计算、COPY只做数据拷贝。初学者用DIRECT一条就够。创建时注意一点队列的Priority和Flags在起步阶段保持默认即可别照抄那些用D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT的代码那玩意关掉后一旦GPU卡死系统不会再帮你恢复整台机器可能都得重启。3.2 分配器不能一边录制一边重置命令分配器ID3D12CommandAllocator管理的是命令列表的内存。它有个铁律只要命令列表还没执行完对应的分配器就不能重置。很多人第一次写出每帧allocator-Reset()然后list-Reset()再录制的循环跑起来看着正常直到某一帧GPU还没消化完上一帧的命令分配器被重置调试层立刻报错或者直接设备移除。正确做法是每帧用不同的分配器或者等围栏确认GPU执行完毕后再重置。三缓冲时准备三个分配器、三个命令列表是常见配置我在第4集的同步部分会展开。3.3 命令列表录制与关闭的配对命令列表ID3D12GraphicsCommandList的用法遵循一个固定节奏Reset→ 一系列SetXXX和DrawXXX→Close。忘了Close就调用ExecuteCommandLists调试层会报命令列表未关闭。反过来Close之后再调用任何录制命令也会报错。这个配对关系我建议直接封装成一个函数避免手滑。3.4 围栏CPU和GPU之间的信号灯围栏ID3D12Fence是同步的唯一可靠手段。它的逻辑是CPU往队列里塞一个Signal围栏值加一然后CPU可以在任意时刻查询围栏的GetCompletedValue看GPU执行到哪了。要等GPU完成就用SetEventOnCompletion加一个事件对象const UINT64 currentFenceValue m_fenceValue; m_commandQueue-Signal(m_fence.Get(), currentFenceValue); if (m_fence-GetCompletedValue() currentFenceValue) { m_fence-SetEventOnCompletion(currentFenceValue, m_fenceEvent); WaitForSingleObject(m_fenceEvent, INFINITE); }这是退出程序前等待GPU的典型写法。我在实践中见过的最隐蔽的bug是围栏值用了UINT而不是UINT64程序连续跑几个小时之后溢出同步逻辑突然错乱。DX12的围栏值就是UINT64别省这个位宽。4. 交换链与第一帧把清出来的颜色真正显示到屏幕上从零到屏幕上出现一块纯色中间要跨过交换链、RTV描述符堆、资源状态转换三道门槛。这一节讲的是很多人第一次看到自己程序输出的时刻。4.1 交换链创建参数逐个说清楚交换链描述里的字段不多但每个都有讲究参数建议值说明BufferCount2或3双缓冲/三缓冲三缓冲更平滑FormatDXGI_FORMAT_R8G8B8A8_UNORM兼容性最好SwapEffectDXGI_SWAP_EFFECT_FLIP_DISCARD现代写法必须用FLIP系列SampleDesc.Count1FLIP模式下必须为1BufferUsageDXGI_USAGE_RENDER_TARGET_OUTPUT作为渲染目标DXGI_SWAP_EFFECT_FLIP_DISCARD是重点。老教程里大量用的是DISCARD但那个模式在新系统上已经不被推荐而且FLIP系列才能配合IDXGISwapChain3的GetCurrentBackBufferIndex使用。用FLIP模式创建的交换链不能直接当渲染目标用必须先拿到实际的后台缓冲资源这一点和旧模式一致但很多人卡在为什么Present之后画面撕裂或花屏——八成因是后台缓冲索引取错了。4.2 RTV描述符堆怎么分配D3D12里渲染目标视图RTV必须放在描述符堆里堆本身是不可直接被shader访问的它只是一个描述符的存放区域。给每个后台缓冲创建一个RTVD3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc {}; rtvHeapDesc.NumDescriptors FRAME_COUNT; rtvHeapDesc.Type D3D12_DESCRIPTOR_HEAP_TYPE_RTV; rtvHeapDesc.Flags D3D12_DESCRIPTOR_HEAP_FLAG_NONE; // RTV堆不能是shader可见的 m_device-CreateDescriptorHeap(rtvHeapDesc, IID_PPV_ARGS(m_rtvHeap));FLAG_NONE是必须的RTV堆不允许shader可见写错了直接创建失败。然后遍历交换链的后台缓冲逐个创建RTVCD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap-GetCPUDescriptorHandleForHeapStart()); for (UINT i 0; i FRAME_COUNT; i) { m_swapChain-GetBuffer(i, IID_PPV_ARGS(m_renderTargets[i])); m_device-CreateRenderTargetView(m_renderTargets[i].Get(), nullptr, rtvHandle); rtvHandle.Offset(1, m_rtvDescriptorSize); }这里Offset(1, m_rtvDescriptorSize)就是前面存下来的描述符大小它让句柄指向堆里的下一个格子。忘了Offset所有RTV都会写到同一个描述符上现象是画面只有一个缓冲的颜色对切换时闪一下。4.3 清屏、转换状态、Present 的完整一帧一帧的最简流程是这样的auto* cmdList m_commandList.Get(); cmdList-Reset(m_commandAllocator.Get(), nullptr); // 把后台缓冲从PRESENT状态转到RENDER_TARGET auto barrier CD3DX12_RESOURCE_BARRIER::Transition( m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET); cmdList-ResourceBarrier(1, barrier); CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap-GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize); const float clearColor[] { 0.1f, 0.2f, 0.3f, 1.0f }; cmdList-ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr); // 转回PRESENT barrier CD3DX12_RESOURCE_BARRIER::Transition( m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT); cmdList-ResourceBarrier(1, barrier); cmdList-Close(); ID3D12CommandList* lists[] { cmdList }; m_commandQueue-ExecuteCommandLists(1, lists); m_swapChain-Present(1, 0);资源状态转换Resource Barrier是DX12的必修课也是调试层报错的重灾区。后台缓冲在Present时必须是PRESENT状态作为渲染目标时必须是RENDER_TARGET状态来回切换全靠barrier。我一开始嫌麻烦把barrier全注释掉了结果调试层疯狂刷RESOURCE_BARRIER相关的警告画面偶尔正确、偶尔花屏运气成分极大。把状态转换当成呼吸一样自然是DX12思维转变的关键一步。Present(1, 0)的第一个参数是同步间隔1表示开启垂直同步。如果帧率测试时想跑满可以传0但会看到画面撕裂。第二个参数是标志位入门阶段传0即可。5. 让三角形浮出水面根签名、着色器与PSO清屏只是热身真正画出三角形得把渲染管线固定下来。这一节的核心是三个对象根签名、着色器、管线状态对象。5.1 顶点数据与输入布局画三角形至少要三个顶点。顶点结构体要和输入布局一一对应struct Vertex { DirectX::XMFLOAT3 position; DirectX::XMFLOAT4 color; };对应的输入布局D3D12_INPUT_ELEMENT_DESC inputLayout[] { { POSITION, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }, { COLOR, 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 } };第二个元素里的offset是12因为XMFLOAT3占12字节。这个偏移量手算很容易错我建议用offsetof(Vertex, color)代替硬编码结构体一改就不用手改偏移。顶点数据要从上传堆拷到默认堆D3D12_HEAP_TYPE_UPLOAD→D3D12_HEAP_TYPE_DEFAULT或者为了省事直接放在上传堆里用因为顶点数据量小、每帧不变。第6集讲纹理上传时会把这两种堆的分工讲透。5.2 根签名的设计思路根签名Root Signature定义shader能拿到哪些资源以及它们的绑定方式。它介于灵活但要手动填描述符表和高效但参数有限之间。入门阶段一个只带顶点缓冲通过D3D12_ROOT_PARAMETER_TYPE_VERTEX_BUFFER_VIEW的根签名就够CD3DX12_ROOT_PARAMETER param[1]; param[0].InitAsConstantBufferView(0); // 常量缓冲放MVP矩阵 CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc; rootSigDesc.Init(1, param, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT这个标志不加管线创建时会报错因为默认根签名不允许输入装配阶段读取顶点数据。我第一次看到这个报错时完全不知道为什么查了半天文档才反应过来。根签名设计的原则是把最频繁变化的参数放进根参数直接存在命令里快但占空间把大量描述符放进描述符表走堆灵活但有额外开销。5.3 着色器编译与PSO失败排查着色器用HLSL写运行时用D3DCompileFromFile编译调试期或者用离线编译好的字节码发布期。PSOID3D12PipelineState把着色器、输入布局、根签名、光栅化状态、混合状态等打包成一个不可变对象。PSO创建失败时错误信息往往比较笼统常用的排查方法报错方向可能原因检查点着色器编译失败HLSL语法错看编译回调的error blob输入布局不匹配语义名、格式对不上顶点结构体与布局逐字段比对根签名不匹配参数量/类型不一致着色器里cbuffer的binding渲染目标格式不符PSO的RTV格式和交换链不一致两处格式必须相同最后一条我踩过。交换链用了R8G8B8A8_UNORMPSO里却写了B8G8R8A8_UNORM创建的PSO在绑定时就会出问题画面一片黑。把这两处的格式用一个常量统一管理能根治这类错误。5.4 画三角形时黑屏的排查链路这是很多人第一次的至暗时刻。黑屏排查我会按这个链路走先确认清屏颜色是否正常显示。如果清屏色都看不到问题在交换链或RTV。确认顶点数据是否真的被上传用调试层看资源内容或者临时把清屏色换成红色确认渲染循环在跑。检查视口RSSetViewports和裁剪矩形RSSetScissorRects有没有设置。D3D12不设视口默认是空的三角形直接被裁掉。检查根签名的标志和PSO的flag是否匹配。检查顶点着色器输出的齐次坐标有没有问题比如z值为0或者w为负三角形会在裁剪空间外。第3条是我最常忘的。D3D12不像D3D11那样有隐式视口RSSetViewports必须显式调用而且要在OMSetRenderTargets之后、DrawInstanced之前。忘了这一步画面能清屏但三角形永远不出现。6. 从纯色到贴图三角形纹理资源的完整旅程前面的三角形是顶点颜色插值出来的升级到贴图本质是把一张图的像素传到GPU并让shader按UV采样。这一步涉及上传堆、资源状态、SRV、采样器四样东西是D3D12资源管理的集中体现。6.1 上传堆与默认堆的分工这是理解D3D12资源管理的核心。GPU访问的显存分两大类默认堆D3D12_HEAP_TYPE_DEFAULT只有GPU能访问速度最快上传堆D3D12_HEAP_TYPE_UPLOADCPU能写适合放临时数据回读堆D3D12_HEAP_TYPE_READBACK让CPU能读GPU结果常用于做GPU读回。纹理的正确姿势是先在默认堆创建纹理资源再在上传堆创建一块同大小的缓冲把纹理数据memcpy进去最后用CopyTextureRegion从上传缓冲拷到默认堆纹理。这个中转过程看似多此一举但它是D3D12的设计哲学——CPU和GPU通过明确的数据流向交互而不是隐式的。6.2 纹理上传的完整步骤第一步计算上传缓冲的大小用GetRequiredIntermediateSize别自己算行对齐规则很容易错const UINT64 uploadSize GetRequiredIntermediateSize(texture.Get(), 0, 1); CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD); CD3DX12_RESOURCE_DESC bufDesc CD3DX12_RESOURCE_DESC::Buffer(uploadSize); device-CreateCommittedResource(uploadHeapProps, D3D12_HEAP_FLAG_NONE, bufDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(textureUploadHeap));第二步用UpdateSubresources把数据搬进去它会自动处理行间距和子资源偏移D3D12_SUBRESOURCE_DATA subData {}; subData.pData imageData; subData.RowPitch width * 4; // 假设RGBA8 subData.SlicePitch subData.RowPitch * height; UpdateSubresources(commandList, texture.Get(), textureUploadHeap.Get(), 0, 0, 1, subData);RowPitch这里可以是原始数据的行字节数UpdateSubresources会按目标纹理的GetCopyableFootprints规则补齐。第三步做资源状态转换。纹理在拷贝前必须是COPY_DEST状态拷完转成PIXEL_SHADER_RESOURCE才能被shader采样。这两次barrier漏一个调试层立刻报资源状态不匹配。6.3 SRV和采样器纹理要被shader读到得创建一个SRV描述符放在CBV/SRV/UAV堆里类型为D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV并且这个堆必须是shader可见的D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE。创建SRVD3D12_SHADER_RESOURCE_VIEW_DESC srvDesc {}; srvDesc.Shader4ComponentMapping D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Format DXGI_FORMAT_R8G8B8A8_UNORM; srvDesc.ViewDimension D3D12_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels 1; device-CreateShaderResourceView(texture.Get(), srvDesc, srvHandle);采样器可以在根签名里用静态采样器D3D12_STATIC_SAMPLER_DESC指定避免额外描述符堆。入门阶段用静态采样器最省事过滤模式选MIN_MAG_MIP_LINEAR寻址模式选WRAP基本能覆盖大部分贴图场景。6.4 从纯色三角形到贴图三角形的最小改动清单把前面的东西串起来改动其实不多顶点结构体加一个UV字段输入布局加一条TEXCOORD语义。根签名加一个D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE指向SRV堆并附上一个静态采样器。像素着色器里加一行Texture2D tex : register(t0); SamplerState smp : register(s0);然后tex.Sample(smp, uv)。绘制前SetGraphicsRootDescriptorTable绑定SRV并SetDescriptorHeaps把SRV堆设进去。顺序上有个坑SetDescriptorHeaps必须在SetGraphicsRootDescriptorTable之前调用否则调试层会抱怨你绑定了一张没被设进去的堆。这个顺序我见不少人写反报错信息也不直观。7. 真实调试记录教程里不会写的那些报错前面讲的是顺利路径真正耗时间的是调试。这一节放几个我实际遇到过、并且在很多教程里找不到答案的问题。7.1 设备移除device removed先看调试层的最后一条设备移除是D3D12里最让人头疼的错误它表现为GPU驱动重置所有后续命令全部失败。触发原因很多越界访问、死循环的shader、GPU超时。我的排查习惯是打开调试层和GPU验证重现时盯着调试层输出的最后几条。如果调试层没报基本可以判断是shader里的逻辑问题比如一个永远不满足条件的循环GPU跑了几秒被系统判定为挂起。D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT这个标志能暂时阻止系统把挂起的GPU重置方便抓帧定位但用完一定要去掉否则下一个问题就是整机卡死。7.2 资源状态不匹配的刷屏调试层最吵的一类报错是资源状态不匹配比如Resource is in state D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE but expected D3D12_RESOURCE_STATE_RENDER_TARGET。这类报错的根因通常是漏了一次barrier或者两次barrier之间资源被别的命令用了。解决它的思路不是哪报错改哪而是把每一帧每个资源的生命周期画出来确认状态转换是闭合的。我养成的习惯是每创建一个资源就在注释里写下它可能经历的状态序列改代码时对照检查。7.3 抓帧工具的实际用法调试层管逻辑错误抓帧工具管画面为什么不对。抓帧工具能抓一整帧的所有命令逐draw call看绑定了什么资源、shader输入输出是什么。我第一次用它定位的问题是贴图显示成纯黑抓帧发现SRV绑的是空描述符根因是SetDescriptorHeaps的调用顺序反了。没有抓帧工具光看代码是看不出来的因为代码逻辑看起来完全正确。入门阶段建议抓一帧就分析一帧别攒着。7.4 贴图异常的几种典型表现全黑SRV没绑定或者纹理没上传成功。全白采样器过滤模式配成了POINT且UV超出范围或者采样器根本没设。上下颠倒图片坐标系和纹理坐标系Y轴方向相反需要在UV上做1.0 - v或者加载时翻转图片。颜色错乱格式对不上比如数据是BGRA却按RGBA读。最后一条我调了半天。图片加载库输出的格式和SRV的Format不一致画面颜色红蓝对调肉眼很难第一眼看出拿调色板的图片一试就现原形。写代码时把数据格式和资源格式两个概念分开记录能省很多事。8. 往下走把25集练完之后的几个方向这套25集跑完你应该能从零搭出一个带贴图的三角形并且会用调试层和抓帧工具定位问题。再往下我个人的建议是先别急着上PBR或阴影而是把深度缓冲和多物体渲染这两件事做扎实。深度缓冲涉及深度资源的状态转换和DSV的创建是后面所有3D场景的基础多物体渲染则逼你面对常量缓冲的动态更新和根参数的分配策略这两个问题在单三角形阶段完全可以绕过去但一旦场景里有第二个物体就绕不开了。我自己的习惯是每学完一个阶段就把代码删掉重写一遍不带参考。第一次写命令队列同步时我照着教程抄感觉懂了第二遍不看教程卡在围栏值的递增逻辑上才发现自己根本没理解信号是先加值再等。这种删掉重写的笨办法比反复看教程管用得多。图形API这东西代码里的每一行都对应一个真实发生在GPU上的动作糊弄不过去也正因如此跑通一个三角形的成就感比想象中足。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →