使用电脑端体验更佳

定位一个{{s:Device Lost}}

发布于:1790183058575 | 修改于:1790183441344 | 分类: 技术 | 浏览: 14

显卡驱动的质量方差大的离谱。但一般来说,无论多糟糕的事情,你所能得到也就是某个物体画不出来,或者画出来很奇怪,或者算出来很奇怪 但偶尔的偶尔,这受苦受累惯了的老黄牛也会有忍受不了的问题。这时候驱动就会决定自行了断,撒手人寰,留下坐在屏幕前的你对着卡死的电脑目瞪口呆 这就是{{s:DeviceLost}},而我在做的渲染器每次都能稳定复现一个 我花了差不多{{s:一个月}}的时间才定位到问题。 这篇文章讲一下我是如何手工排查的 ## Bug表现 在我往场景中加了500盏灯光后,飞到场景最高处,俯视所有灯光,然后摇晃相机,就能触发 在VkQueueSubmit后的第一个有返回值的API调用会返回`VK_ERROR_DEVICE_LOST`。 同时,电脑会卡死几秒,然后画面卡住不动。所有图形api调用都会失效 ![](https://pic.feiqi3.cn/blogPic/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202026-09-23%20000359.png) ## 结论 目前我所知道的会导致DeviceLost的原因只有一个:{{s:GPU上踩内存}} 但结论不重要,我的渲染器目前代码量超过20k行,光是VulkanRHI层目前就有接近7k行代码,更别提架在上面的其他应用部分。我完全没有头绪。 ## 渲染流程 ![](https://pic.feiqi3.cn/blogPic/RenderPipeline_2026923.png) 目前难以确定是什么时候引入的这个Bug,我姑且猜测是在实现完成CLusterLights后发生的。 计算ClusterLights比较复杂,要两个Compute。 第一个负责用HiZ剔除灯光,第二个负责把灯光放进TileBox里。 在画物体的时候,为了知道灯光数量和灯光信息,需要进行好几次GPU上索引SSBO。 这是疑点最大的地方。 ## GPU挂在哪里了? 我没办法靠猜来知道这一切啊...我需要一个{{s:提示}} Vulkan有个Device上的拓展[VkPhysicalDeviceFaultFeaturesEXT](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFaultFeaturesEXT.html),能够在DeviceLost发生后得到一些信息。 于是: ```cpp void GPUPostMortem(rs_context_vk* ctx) { if (!DeviceFault) { return; } VkDeviceFaultCountsEXT deviceFaultCnts = {VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT}; vkGetDeviceFaultInfoEXT(ctx->device, &deviceFaultCnts, nullptr); VkDeviceFaultInfoEXT deviceFaultInfo = { VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT }; std::vector<VkDeviceFaultAddressInfoEXT> deviceFaultAddrInfo{}; deviceFaultAddrInfo.resize(deviceFaultCnts.addressInfoCount); deviceFaultInfo.pAddressInfos = deviceFaultAddrInfo.data(); std::vector<VkDeviceFaultVendorInfoEXT> deviceFaultVendorInfo{}; deviceFaultVendorInfo.resize(deviceFaultCnts.vendorInfoCount); deviceFaultInfo.pVendorInfos = deviceFaultVendorInfo.data(); char* vendorData = new char[deviceFaultCnts.vendorBinarySize + 1]; deviceFaultInfo.pVendorBinaryData = vendorData; vkGetDeviceFaultInfoEXT(ctx->device, &deviceFaultCnts, &deviceFaultInfo); std::stringstream ss; ss << "=================== GPU PostMortem Begin ===================\n" << "Backend: Vulkan\n" << "Device: " << ctx->choosenPhysicalDeviceInfo << "\n" << (BindlessAvailable ? std::string("Bindless Mode") : "No Bindless") << "\n" << (BufferDeviceAddressEnable ? "BDA Enabled" : "No BDA") << "\n" << "Description: " << deviceFaultInfo.description << "\n\n"; ss << "================== Address fault Begin ==================\n"; for (int i = 0; i < deviceFaultCnts.addressInfoCount;++i) { auto addressInfo = deviceFaultInfo.pAddressInfos[i]; ss << "Fault - " << i + 1 << "/" << deviceFaultCnts.addressInfoCount << "\n"; ss << "Address Type: " << (int)(addressInfo.addressType) << "\n"; ss << "Buffer Address: " << std::hex << addressInfo.reportedAddress << std::dec << "\n"; ss << "Buffer Address Precision: " << std::hex << addressInfo.addressPrecision << std::dec << "\n\n"; } ss << "================== Address fault End ==================\n\n"; ss << "================== Vendor info Begin ==================\n"; for (int i = 0; i < deviceFaultCnts.vendorInfoCount;++i) { auto vendorInfo = deviceFaultInfo.pVendorInfos[i]; ss << "Vendor Fault - " << i + 1 << "/" << deviceFaultCnts.vendorInfoCount << "\n"; ss << "Description: " << (int)(vendorInfo.description) << "\n"; ss << "Fault Code: " << std::hex << vendorInfo.vendorFaultCode << std::dec << "\n"; ss << "Fault Data: " << std::hex << vendorInfo.vendorFaultData << std::dec << "\n\n"; } ss << "================== Vendor info End ==================\n\n"; ss << "================== GPU PostMortem End ==================\n"; //Print all log info Log::error( ss.str() ); delete[] vendorData; } ``` ## 崩溃后的信息 ``` [ERROR] =================== GPU PostMortem Begin =================== Backend: Vulkan Device: NVIDIA GeForce RTX 4060 Laptop GPU (Driver: 616.368.0) Bindless Mode BDA Enabled Description: ================== Address fault Begin ================== Fault - 1/2 Address Type: 6 Buffer Address: 20002ba90 Buffer Address Precision: 10 Fault - 2/2 Address Type: 1 Buffer Address: 3cafc000 Buffer Address Precision: 1000 ================== Address fault End ================== ================== Vendor info Begin ================== ================== Vendor info End ================== ================== GPU PostMortem End ================== ``` 第一个Fault提示: Address Type: 6 ---> 这是指令指针的地址,也就是指向了shader二进制中某一行...但是我不知道是哪个shader,更没有shader的二进制.... 第二个fault: Address Type: 1 ---> 这是个读错误。意味着和我猜想的一模一样,GPU上的踩内存。 没有任何实质进展 ## 找到所有Buffer的地址 我把所有的创建的Buffer的GPU地址打印了出来,扔给AI去看。 如果是内存越界问题,那么这个地址应该是和现有的Buffer有关,至少和现有创建的Buffer在地址上相差不大 我把Log扔给了AI去看,但是并没有发现任何似乎和`3cafc000`有关的地址...问题再次陷入了僵局 ## Nvidia Nsight Aftermath Vendor的工具还是又大又好OvO 对于N卡[Aftermath](https://docs.nvidia.com/nsight-aftermath/index.html)可以捕捉所有运行时的GPUCrash,生成gpudmp,定位到shader中的崩溃地点 > 记得前文中的VkPhysicalDeviceFaultFeaturesEXT么? > 它其实也会生成Dump的二进制code > 我没测试过,但应该能被afterMath读取 > 不过应该不如aftermath生成的好 按照这样设置 ![](https://pic.feiqi3.cn/blogPic/_after_math_setup.png) 运行程序..然后 我抓到了! ![](https://pic.feiqi3.cn/blogPic/_aftermath_caught_a_crash.png) 打开crash,然后... 欸怎么找不到Shader源文件啊..... ![](https://pic.feiqi3.cn/blogPic/_aftermath_dump_out.png) 不过不用担心,关键信息已经出来了 fault address是0x0000000010 也就是说在Shader上解引用了一个空指针???? 鉴于现在这么高的崩溃概率,应该是主PBRShader引起的... 崩溃地点的PC不是很高,说明还没到光照计算 看来之前的猜测错了啊,并不是ClusterLight引入的 那会是什么呢? 祭出接下来的法宝 出来吧! ## {{w:Shader Debug Print}} 什么,竟然能在Shader里{{s:打印信息}}? 这是一个由ValidationLayer引入的拓展,能够在Shader内使用printf()语法,然后再控制台上输出信息 > 这个拓展的实现有些神奇,最早只在Cuda上见过Device端的DebugPrint > GPU自然没有往控制台IO的能力,那是怎么实现的呢? > 其实是ValidationLayer创建了一个HostVisible的Buffer > Shader运行时并不会真的拼接字符串,而是记录那些动态的信息,并且存入这个Buffer > 在submit执行完毕后会回读内部数据 > 然后把数据在CPU内拼入字符串内 > Bingo! 现在我希望对于MainPBRShader,每个画的出来的DrawCall需要打印一次消息(有且仅有打印一次!) > 因为Print是一个很重很重的操作! 做到这点,只需要用一个UAV,然后每次Shader执行的时候往里面的特定地点写入一个Flag,第一个拿到Flag的线程负责写入 ```glsl bool debugPrintOnceSlot(uint slot) { uint64_t addr = GET_DEBUG_FLAG_BUFFER_ADDRESS(); if (addr == uint64_t(0)) { return false; } DebugFlagBuffer flags = DebugFlagBuffer(addr); slot = slot % uint(DEBUG_PRINT_FLAG_COUNT); if (flags.printedFlag[slot] != 0u) { return false; } return atomicCompSwap(flags.printedFlag[slot], 0u, 1u) == 0u; } //Once per shader (pipeline) per frame. bool debugPrintOnce() { return debugPrintOnceSlot(GET_PIPELINE_INDEX()); } //Once per draw call per frame. bool debugPrintOnceDraw() { return debugPrintOnceSlot(GET_DRAW_CALL_INDEX()); } #define DEBUG_PRINT_ONCE(args) do { if (debugPrintOnce()) { DEBUG_PRINT(args); } } while(false) #define DEBUG_PRINT_ONCE_DRAW(args) do { if (debugPrintOnceDraw()) { DEBUG_PRINT(args); } } while(false) ``` 目标的Shader在这里[StandardPBR.ps](https://github.com/feiqi3/Renderer/blob/342dbb0e16426ac2e493624ef103879068d874ac/shader/StandardPBR.ps) 资源声明部分如下 ```cpp RESOURCE_DECL_BEG(4) SLOT_TEXTURE(4, texture2D, u_baseColorTex) SLOT_SAMPLER(4, sampler, u_baseColorSampler) SLOT_TEXTURE(4, texture2D, u_normalTex) SLOT_SAMPLER(4, sampler, u_normalSampler) SLOT_TEXTURE(4, texture2D, u_metallicRoughnessTex) SLOT_SAMPLER(4, sampler, u_metallicRoughnessSampler) SLOT_TEXTURE(4, texture2D, u_AOTex) SLOT_SAMPLER(4, sampler, u_AOSampler) SLOT_CONST_BUFFER(4, PBRDataAlias, CBUFFER_pbrData) RESOURCE_DECL_END ``` 我主要关注声明的资源`SLOT_CONST_BUFFER(4, PBRDataAlias, CBUFFER_pbrData)` 这个CBuffer存了大部分PBR-related的参数,是最早会被访问的Buffer,问题大概率会出在这 我在排查bug的时候不止打印了这个Buffer的地址。 由于是BindlessMode,所有的资源绑定其实都靠一个UBO里存的uint32和uint64 我把这个UBO全部打印出来了 崩溃出现了,然后...我测,怎么打印了一个ViewMatrix出来... ## 真相大白 这个项目里的UBO背后是Vulkan的Dynamic UniformBuffer 使用普通UniformBuffer的流程是: 把UBO绑定到DescriptorSet上,把DescriptorSet绑定到当前状态上,绘制 使用Dynamic UniformBuffer时的流程是: 把UBO绑定到DescriptorSet上,把DescriptorSet绑定到当前状态的同时 再传入一个Offset,绘制 随后,实际传入shader的地址实际上是Base+Offset Bindless的资源UBO和各种Matrix都是这么传入的。那么问题多半就出在这里 随后翻看Git记录,发现了很久之前为了优化DescriptorUpdate写的代码: ```cpp //Avoid multi bind after set was bind to cmd. bool needUpadte = false; if (descriptorPack.setUpdatedFif == curFif) { needUpadte = false; } else { needUpadte = true; descriptorPack.setUpdatedFif = curFif; } ...... if(needUpadte){ //VkUpdateDescriptorSets(....) } ``` 问题出在我用了Fif而不是逻辑帧数来判断是否需要update 在MaxFrameInFlight=2的时候,一个物体可能在第二帧进行了Update,此时descriptorPack.setUpdatedFif=0 然后第三帧的时候物体因为不可见被剔除,因此没有发生Update 到了第六帧,物体又可见了,此时curFif=2,于是不更新,descriptorSet里的绑定信息还是第二帧的 由于UBO是一个RingBuffer,因此曾经存放这个物体信息的内存早就被其他数据覆盖了 于是崩溃 ## 结尾 !{{s:好累}} 最近把渲染器的SkeletonAnimation部分做了验证,离MMD渲染器又接近了一步... 接下来会有一篇博客讲角色动画和骨骼蒙皮 {{r:关于怎么让一只狐狸动起来}}=v= <audio controls> <source src="https://pic.feiqi3.cn/media/Weezer_You_Might_Think.mp3" type="audio/mpeg"> 这里原本应该是一首叫 You might think的歌OwO但是你没加载出来哦 </audio>


评论