基于ImGui+GLFW+Metal的基础程序无法渲染、内存泄漏并崩溃
我尝试使用ImGui、GLFW和Metal API创建最基础的示例程序,程序可创建可交互窗口,但窗口始终黑屏,内存泄漏严重(3秒内占用3GB),帧率在300-3000FPS之间,运行数秒后会在layer_private_present_impl(_CAMetalLayerPrivate*, CAMetalDrawable*, double, unsigned int)函数中崩溃。以下是我的代码:
// main.mm #include <iostream> #include <GLFW/glfw3.h> #define GLFW_EXPOSE_NATIVE_COCOA #include <GLFW/glfw3native.h> #include <imgui.h> #include <backends/imgui_impl_osx.h> #include <backends/imgui_impl_metal.h> #include <Metal/Metal.h> #include <QuartzCore/CAMetalLayer.h> int main() { if (!glfwInit()) { std::cout << "Failed to init GLFW\n"; return 1; } auto* W = glfwCreateWindow(1000, 800, "My Window", nullptr, nullptr); if (!W) { std::cout << "Failed to create window\n"; return 1; } int width = 0, height = 0; glfwGetWindowSize(W, &width, &height); ImGui::CreateContext(); NSWindow* NSW = glfwGetCocoaWindow(W); ImGui_ImplOSX_Init(NSW.contentView); id<MTLDevice> device = MTLCreateSystemDefaultDevice(); ImGui_ImplMetal_Init(device); CAMetalLayer* layer = [[CAMetalLayer alloc] init]; layer.device = device; layer.pixelFormat = MTLPixelFormatBGRA8Unorm; layer.drawableSize = CGSize{ 2.0 * width, 2.0 * height }; NSW.contentView.wantsLayer = true; [NSW.contentView setLayer:layer]; id commandQueue = [device newCommandQueue]; ImGui_ImplMetal_CreateFontsTexture(device); while (!glfwWindowShouldClose(W)) { ImGuiIO& io = ImGui::GetIO(); io.DisplaySize = ImVec2(width, height); ImGui_ImplOSX_NewFrame(NSW.contentView); ImGui::NewFrame(); // User code ImGui::ShowDemoWindow(); id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer]; MTLRenderPassDescriptor* renderPassDescriptor = [[MTLRenderPassDescriptor alloc] init]; MTLRenderPassColorAttachmentDescriptor* caDesc = [[MTLRenderPassColorAttachmentDescriptor alloc] init]; id<CAMetalDrawable> drawable = layer.nextDrawable; caDesc.texture = drawable.texture; caDesc.loadAction = MTLLoadActionClear; caDesc.storeAction = MTLStoreActionStore; [renderPassDescriptor.colorAttachments setObject: caDesc atIndexedSubscript: 0]; ImGui_ImplMetal_NewFrame(renderPassDescriptor); // Rendering ImGui::Render(); ImDrawData* drawData = ImGui::GetDrawData(); renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(1, 0, 1, 1); id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; [renderEncoder pushDebugGroup:@"Dear ImGui rendering"]; ImGui_ImplMetal_RenderDrawData(drawData, commandBuffer, renderEncoder); [renderEncoder popDebugGroup]; [renderEncoder endEncoding]; // Present [commandBuffer presentDrawable: drawable]; [commandBuffer commit]; [drawable present]; glfwPollEvents(); } ImGui_ImplMetal_Shutdown(); ImGui_ImplOSX_Shutdown(); }
问题原因与修复方案
1. 重复提交present动作导致崩溃与资源泄漏
同时调用[commandBuffer presentDrawable: drawable]和[drawable present]是错误操作,Metal只需要通过commandBuffer提交present动作即可。重复调用会导致drawable资源无法正确回收,引发内存泄漏和崩溃。
修复: 删除[drawable present];这一行。
2. Objective-C临时对象未及时回收导致内存泄漏
循环内重复创建的MTLRenderPassDescriptor和MTLRenderPassColorAttachmentDescriptor属于ARC管理对象,但未通过自动释放池及时回收,导致对象堆积快速消耗内存。
修复: 在循环内包裹@autoreleasepool块,让ARC及时回收临时对象。
3. 未适配高DPI与窗口尺寸变化
初始获取窗口尺寸后未在循环中更新,且未设置io.DisplayFramebufferScale,导致ImGui渲染内容与窗口实际尺寸不匹配,引发黑屏。
修复: 在循环内重新获取窗口尺寸与屏幕缩放因子,同步更新io.DisplayFramebufferScale和MetalLayer的drawableSize。
4. 无帧率限制导致资源耗尽
未开启垂直同步,循环以CPU最高速度运行,既浪费资源又加速内存泄漏。
修复: 调用glfwSwapInterval(1)开启垂直同步限制帧率。
修复后的完整代码
// main.mm #include <iostream> #include <GLFW/glfw3.h> #define GLFW_EXPOSE_NATIVE_COCOA #include <GLFW/glfw3native.h> #include <imgui.h> #include <backends/imgui_impl_osx.h> #include <backends/imgui_impl_metal.h> #include <Metal/Metal.h> #include <QuartzCore/CAMetalLayer.h> int main() { if (!glfwInit()) { std::cout << "Failed to init GLFW\n"; return 1; } auto* W = glfwCreateWindow(1000, 800, "My Window", nullptr, nullptr); if (!W) { std::cout << "Failed to create window\n"; return 1; } // 开启垂直同步限制帧率 glfwSwapInterval(1); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); NSWindow* NSW = glfwGetCocoaWindow(W); ImGui_ImplOSX_Init(NSW.contentView); id<MTLDevice> device = MTLCreateSystemDefaultDevice(); ImGui_ImplMetal_Init(device); CAMetalLayer* layer = [[CAMetalLayer alloc] init]; layer.device = device; layer.pixelFormat = MTLPixelFormatBGRA8Unorm; NSW.contentView.wantsLayer = true; [NSW.contentView setLayer:layer]; id<MTLCommandQueue> commandQueue = [device newCommandQueue]; ImGui_ImplMetal_CreateFontsTexture(device); while (!glfwWindowShouldClose(W)) { @autoreleasepool { int width = 0, height = 0; glfwGetWindowSize(W, &width, &height); // 获取屏幕缩放因子,适配高DPI float scaleFactor = [NSW.contentView backingScaleFactor]; io.DisplaySize = ImVec2(width, height); io.DisplayFramebufferScale = ImVec2(scaleFactor, scaleFactor); layer.drawableSize = CGSize{ width * scaleFactor, height * scaleFactor }; ImGui_ImplOSX_NewFrame(NSW.contentView); ImGui::NewFrame(); // User code ImGui::ShowDemoWindow(); id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer]; MTLRenderPassDescriptor* renderPassDescriptor = [[MTLRenderPassDescriptor alloc] init]; MTLRenderPassColorAttachmentDescriptor* caDesc = [[MTLRenderPassColorAttachmentDescriptor alloc] init]; id<CAMetalDrawable> drawable = layer.nextDrawable; caDesc.texture = drawable.texture; caDesc.loadAction = MTLLoadActionClear; caDesc.storeAction = MTLStoreActionStore; caDesc.clearColor = MTLClearColorMake(1, 0, 1, 1); [renderPassDescriptor.colorAttachments setObject:caDesc atIndexedSubscript:0]; ImGui_ImplMetal_NewFrame(renderPassDescriptor); ImGui::Render(); ImDrawData* drawData = ImGui::GetDrawData(); id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; [renderEncoder pushDebugGroup:@"Dear ImGui rendering"]; ImGui_ImplMetal_RenderDrawData(drawData, commandBuffer, renderEncoder); [renderEncoder popDebugGroup]; [renderEncoder endEncoding]; // 仅通过commandBuffer提交present动作 [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; glfwPollEvents(); } } ImGui_ImplMetal_Shutdown(); ImGui_ImplOSX_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(W); glfwTerminate(); return 0; }
内容的提问来源于stack exchange,提问作者chrysante

