// dear imgui: Renderer Backend for Metal 4 // This needs to be used along with a Platform Backend (e.g. OSX) // Implemented features: // [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! // [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). // [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). // Missing features or Issues: // [ ] Metal-cpp support. // [ ] Texture view pool support? Reevaluate which type to use for ImtextureID. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. // Learn about Dear ImGui: // - FAQ https://dearimgui.com/faq // - Getting Started https://dearimgui.com/getting-started // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) // 2026-07-02: Metal 4: Added new Metal 4 backend implementation. (#9458) #include "imgui.h" #ifndef IMGUI_DISABLE #include "imgui_impl_metal4.h" #import #import #pragma mark - Support classes and structs struct ImGui_Metal4_ConstantData { float ModelViewProjectionMatrix[4][4]; }; @interface MetalBuffer : NSObject @property (nonatomic, strong) id buffer; @property (nonatomic, assign) double lastReuseTime; - (instancetype)initWithBuffer:(id)buffer; @end // An object that encapsulates the data necessary to uniquely identify a // render pipeline state. These are used as cache keys. @interface FramebufferDescriptor : NSObject @property (nonatomic, assign) unsigned long sampleCount; @property (nonatomic, assign) MTLPixelFormat colorPixelFormat; @property (nonatomic, assign) MTLPixelFormat depthPixelFormat; @property (nonatomic, assign) MTLPixelFormat stencilPixelFormat; - (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor; @end @interface MetalTexture : NSObject @property (nonatomic, strong) id metalTexture; - (instancetype)initWithTexture:(id)metalTexture; @end // A singleton that stores long-lived objects that are needed by the Metal // renderer backend. Stores the render pipeline state cache and the default // font texture, and manages the reusable buffer cache. @interface MetalContext : NSObject @property (nonatomic, strong) id device; @property (nonatomic, strong) id commandQueue; @property (nonatomic, strong) id depthStencilState; @property (nonatomic, strong) id argumentTable; @property (nonatomic, strong) id samplerStateLinear; @property (nonatomic, strong) id samplerStateNearest; @property (nonatomic, strong) id residencySet; @property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; @property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; @property (nonatomic, assign) NSUInteger framesInFlight; @property (nonatomic, assign) NSUInteger currentFrameSlot; @property (nonatomic, strong) NSArray>* constantBuffers; @property (nonatomic, assign) ImGui_Metal4_ConstantData** constantBufferContentsArray; @property (nonatomic, strong) NSMutableArray*>* bufferCaches; @property (nonatomic, strong) NSObject* bufferCacheLock; @property (nonatomic, assign) double lastBufferCachePurge; - (id)currentConstantBuffer; - (ImGui_Metal4_ConstantData*)currentConstantBufferContents; - (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device; - (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device; @end struct ImGui_ImplMetal4_Data { MetalContext* SharedMetalContext; id RenderCommandEncoder; ImGui_ImplMetal4_Data() { memset((void*)this, 0, sizeof(*this)); } }; static ImGui_ImplMetal4_Data* ImGui_ImplMetal4_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal4_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } static void ImGui_ImplMetal4_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal4_GetBackendData()); } static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); } #pragma mark - Dear ImGui Metal Backend API void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal4_Init()?"); IM_ASSERT(frameInFlightIndex < bd->SharedMetalContext.framesInFlight && "frameInFlightIndex out of range! See framesInFlight passed to ImGui_ImplMetal4_Init()."); #ifdef IMGUI_IMPL_METAL_CPP bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease]; #else bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]; #endif bd->SharedMetalContext.currentFrameSlot = (NSUInteger)frameInFlightIndex; if (bd->SharedMetalContext.depthStencilState == nil) ImGui_ImplMetal4_CreateDeviceObjects(bd->SharedMetalContext.device); } static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id commandBuffer, id commandEncoder, id renderPipelineState, MetalBuffer* vertexBuffer, size_t vertexBufferOffset) { IM_UNUSED(commandBuffer); ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [commandEncoder setCullMode:MTLCullModeNone]; [commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState]; // Setup viewport, orthographic projection matrix // Our visible imgui space lies from draw_data->DisplayPos (top left) to // draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayMin is typically (0,0) for single viewport apps. MTLViewport viewport = { .originX = 0.0, .originY = 0.0, .width = (double)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x), .height = (double)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y), .znear = 0.0, .zfar = 1.0 }; [commandEncoder setViewport:viewport]; float L = draw_data->DisplayPos.x; float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; float T = draw_data->DisplayPos.y; float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; float N = (float)viewport.znear; float F = (float)viewport.zfar; const float ortho_projection[4][4] = { { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, { 0.0f, 2.0f/(T-B), 0.0f, 0.0f }, { 0.0f, 0.0f, 1/(F-N), 0.0f }, { (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f }, }; ImGui_Metal4_ConstantData* constantBufferContents = [bd->SharedMetalContext currentConstantBufferContents]; memcpy(constantBufferContents->ModelViewProjectionMatrix, ortho_projection, sizeof(ortho_projection)); id argumentTable = bd->SharedMetalContext.argumentTable; [argumentTable setAddress:[bd->SharedMetalContext currentConstantBuffer].gpuAddress atIndex:1]; [argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset) attributeStride:sizeof(ImDrawVert) atIndex:0]; [argumentTable setSamplerState:bd->SharedMetalContext.samplerStateLinear.gpuResourceID atIndex:0]; [commandEncoder setArgumentTable:argumentTable atStages:MTLRenderStageVertex | MTLRenderStageFragment]; [commandEncoder setRenderPipelineState:renderPipelineState]; } static void ImGui_ImplMetal4_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. static void ImGui_ImplMetal4_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [bd->SharedMetalContext.argumentTable setSamplerState:bd->SharedMetalContext.samplerStateLinear.gpuResourceID atIndex:0]; } static void ImGui_ImplMetal4_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [bd->SharedMetalContext.argumentTable setSamplerState:bd->SharedMetalContext.samplerStateNearest.gpuResourceID atIndex:0]; } void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id commandBuffer, id commandEncoder) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); MetalContext* ctx = bd->SharedMetalContext; // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x); int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y); if (fb_width <= 0 || fb_height <= 0 || draw_data->CmdLists.Size == 0) return; // Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do. // (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates). if (draw_data->Textures != nullptr) for (ImTextureData* tex : *draw_data->Textures) if (tex->Status != ImTextureStatus_OK) ImGui_ImplMetal4_UpdateTexture(tex); // Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame // The hit rate for this cache should be very near 100%. id renderPipelineState = ctx.renderPipelineStateCache[ctx.framebufferDescriptor]; if (renderPipelineState == nil) { // No luck; make a new render pipeline state renderPipelineState = [ctx renderPipelineStateForFramebufferDescriptor:ctx.framebufferDescriptor device:commandBuffer.device]; // Cache render pipeline state for later reuse ctx.renderPipelineStateCache[ctx.framebufferDescriptor] = renderPipelineState; } size_t vertexBufferLength = (size_t)draw_data->TotalVtxCount * sizeof(ImDrawVert); size_t indexBufferLength = (size_t)draw_data->TotalIdxCount * sizeof(ImDrawIdx); MetalBuffer* vertexBuffer = [ctx dequeueReusableBufferOfLength:vertexBufferLength device:commandBuffer.device]; MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device]; bd->RenderCommandEncoder = commandEncoder; ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0); // Will project scissor/clipping rectangles into framebuffer space ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) // Before rendering command lists, commit residency set [bd->SharedMetalContext.residencySet commit]; // Render command lists size_t vertexBufferOffset = 0; size_t indexBufferOffset = 0; for (const ImDrawList* draw_list : draw_data->CmdLists) { memcpy((char*)vertexBuffer.buffer.contents + vertexBufferOffset, draw_list->VtxBuffer.Data, (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert)); memcpy((char*)indexBuffer.buffer.contents + indexBufferOffset, draw_list->IdxBuffer.Data, (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx)); for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) { const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; if (pcmd->UserCallback) { // User callback, registered via ImDrawList::AddCallback() if (pcmd->UserCallback == ImGui_ImplMetal4_DrawCallback_ResetRenderState) ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset); else pcmd->UserCallback(draw_list, pcmd); } else { // Project scissor/clipping rectangles into framebuffer space ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); // Clamp to viewport as setScissorRect() won't accept values that are off bounds if (clip_min.x < 0.0f) { clip_min.x = 0.0f; } if (clip_min.y < 0.0f) { clip_min.y = 0.0f; } if (clip_max.x > (float)fb_width) { clip_max.x = (float)fb_width; } if (clip_max.y > (float)fb_height) { clip_max.y = (float)fb_height; } if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) continue; if (pcmd->ElemCount == 0) // drawIndexedPrimitives() validation doesn't accept this continue; // Apply scissor/clipping rectangle MTLScissorRect scissorRect = { .x = NSUInteger(clip_min.x), .y = NSUInteger(clip_min.y), .width = NSUInteger(clip_max.x - clip_min.x), .height = NSUInteger(clip_max.y - clip_min.y) }; [commandEncoder setScissorRect:scissorRect]; // Bind texture, Draw ImTextureID tex_id = pcmd->GetTexID(); if (tex_id != ImTextureID_Invalid) { id texture = (__bridge id)(void*)(intptr_t)tex_id; [bd->SharedMetalContext.argumentTable setTexture:texture.gpuResourceID atIndex:0]; } [bd->SharedMetalContext.argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset + (pcmd->VtxOffset * sizeof(ImDrawVert))) attributeStride:sizeof(ImDrawVert) atIndex:0]; size_t indexBufferCmdOffset = indexBufferOffset + (pcmd->IdxOffset * sizeof(ImDrawIdx)); [commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexCount:pcmd->ElemCount indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 indexBuffer:indexBuffer.buffer.gpuAddress + indexBufferCmdOffset indexBufferLength:indexBuffer.buffer.length - indexBufferCmdOffset]; } } vertexBufferOffset += (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert); indexBufferOffset += (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx); } MetalContext* sharedMetalContext = bd->SharedMetalContext; @synchronized(sharedMetalContext.bufferCacheLock) { NSMutableArray* slotCache = sharedMetalContext.bufferCaches[sharedMetalContext.currentFrameSlot]; [slotCache addObject:vertexBuffer]; [slotCache addObject:indexBuffer]; } bd->RenderCommandEncoder = nil; } static void ImGui_ImplMetal4_DestroyTexture(ImTextureData* tex) { if (MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData)) { IM_ASSERT(backend_tex.metalTexture == (__bridge id)(void*)(intptr_t)tex->TexID); backend_tex.metalTexture = nil; // Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running) tex->SetTexID(ImTextureID_Invalid); tex->BackendUserData = nullptr; } tex->SetStatus(ImTextureStatus_Destroyed); } void ImGui_ImplMetal4_UpdateTexture(ImTextureData* tex) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); if (tex->Status == ImTextureStatus_WantCreate) { // Create and upload new texture to graphics system //IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height); IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr); IM_ASSERT(tex->Format == ImTextureFormat_RGBA32); // We are retrieving and uploading the font atlas as a 4-channels RGBA texture here. // In theory we could call GetTexDataAsAlpha8() and upload a 1-channel texture to save on memory access bandwidth. // However, using a shader designed for 1-channel texture would make it less obvious to use the ImTextureID facility to render users own textures. // You can make that change in your implementation. MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:(NSUInteger)tex->Width height:(NSUInteger)tex->Height mipmapped:NO]; textureDescriptor.usage = MTLTextureUsageShaderRead; textureDescriptor.storageMode = MTLStorageModeShared; id texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor]; [bd->SharedMetalContext.residencySet addAllocation:texture]; [texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4]; MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture]; // Store identifiers tex->SetTexID((ImTextureID)(intptr_t)texture); tex->SetStatus(ImTextureStatus_OK); tex->BackendUserData = (__bridge_retained void*)(backend_tex); } else if (tex->Status == ImTextureStatus_WantUpdates) { // Update selected blocks. We only ever write to textures regions which have never been used before! // This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region. MetalTexture* backend_tex = (__bridge MetalTexture*)(tex->BackendUserData); for (ImTextureRect& r : tex->Updates) { [backend_tex.metalTexture replaceRegion:MTLRegionMake2D((NSUInteger)r.x, (NSUInteger)r.y, (NSUInteger)r.w, (NSUInteger)r.h) mipmapLevel:0 withBytes:tex->GetPixelsAt(r.x, r.y) bytesPerRow:(NSUInteger)tex->Width * 4]; } tex->SetStatus(ImTextureStatus_OK); } else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0) { ImGui_ImplMetal4_DestroyTexture(tex); } } bool ImGui_ImplMetal4_CreateDeviceObjects(id device) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); MTLResidencySetDescriptor* residencySetDescriptor = [[MTLResidencySetDescriptor alloc] init]; residencySetDescriptor.initialCapacity = 1000; NSError* error = nil; bd->SharedMetalContext.residencySet = [device newResidencySetWithDescriptor:residencySetDescriptor error:&error]; IM_ASSERT(bd->SharedMetalContext.residencySet != nil && error == nil); [bd->SharedMetalContext.commandQueue addResidencySet:bd->SharedMetalContext.residencySet]; MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init]; depthStencilDescriptor.depthWriteEnabled = NO; depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways; bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor]; MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; samplerDescriptor.supportArgumentBuffers = YES; samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.mipFilter = MTLSamplerMipFilterLinear; bd->SharedMetalContext.samplerStateLinear = [device newSamplerStateWithDescriptor:samplerDescriptor]; samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest; samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest; samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest; bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor]; NSMutableArray>* constantBuffers = [NSMutableArray array]; ImGui_Metal4_ConstantData** constantBufferContentsArray = (ImGui_Metal4_ConstantData**)malloc(sizeof(ImGui_Metal4_ConstantData*) * bd->SharedMetalContext.framesInFlight); for (NSUInteger i = 0; i < bd->SharedMetalContext.framesInFlight; i++) { id constantBuffer = [device newBufferWithLength:sizeof(ImGui_Metal4_ConstantData) options:MTLResourceStorageModeShared]; [constantBuffers addObject:constantBuffer]; constantBufferContentsArray[i] = (ImGui_Metal4_ConstantData*)constantBuffer.contents; [bd->SharedMetalContext.residencySet addAllocation:constantBuffer]; } bd->SharedMetalContext.constantBuffers = constantBuffers; bd->SharedMetalContext.constantBufferContentsArray = constantBufferContentsArray; MTL4ArgumentTableDescriptor* argumentTableDescriptor = [[MTL4ArgumentTableDescriptor alloc] init]; argumentTableDescriptor.maxBufferBindCount = 8; argumentTableDescriptor.maxTextureBindCount = 8; argumentTableDescriptor.maxSamplerStateBindCount = 8; argumentTableDescriptor.supportAttributeStrides = YES; // required: vertex buffer is bound via setAddress:stride:atIndex: for stage_in fetch bd->SharedMetalContext.argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error]; IM_ASSERT(bd->SharedMetalContext.argumentTable != nil && error == nil); return true; } void ImGui_ImplMetal4_DestroyDeviceObjects() { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); // Destroy all textures for (ImTextureData* tex : ImGui::GetPlatformIO().Textures) if (tex->RefCount == 1) ImGui_ImplMetal4_DestroyTexture(tex); [bd->SharedMetalContext.renderPipelineStateCache removeAllObjects]; bd->SharedMetalContext.samplerStateLinear = nil; bd->SharedMetalContext.samplerStateNearest = nil; } bool ImGui_ImplMetal4_Init(id device, id commandQueue, int framesInFlight) { ImGuiIO& io = ImGui::GetIO(); IMGUI_CHECKVERSION(); IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); IM_ASSERT(framesInFlight > 0 && "framesInFlight must be at least 1!"); ImGui_ImplMetal4_Data* bd = IM_NEW(ImGui_ImplMetal4_Data)(); io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "imgui_impl_metal4"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render. ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal4_DrawCallback_ResetRenderState; platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal4_DrawCallback_SetSamplerLinear; platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal4_DrawCallback_SetSamplerNearest; bd->SharedMetalContext = [[MetalContext alloc] init]; bd->SharedMetalContext.device = device; bd->SharedMetalContext.commandQueue = commandQueue; bd->SharedMetalContext.framesInFlight = (NSUInteger)framesInFlight; NSMutableArray*>* bufferCaches = [NSMutableArray array]; for (NSUInteger i = 0; i < framesInFlight; i++) [bufferCaches addObject:[NSMutableArray array]]; bd->SharedMetalContext.bufferCaches = bufferCaches; return true; } void ImGui_ImplMetal4_Shutdown() { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); IM_UNUSED(bd); IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); ImGui_ImplMetal4_DestroyDeviceObjects(); ImGui_ImplMetal4_DestroyBackendData(); io.BackendRendererName = nullptr; io.BackendRendererUserData = nullptr; io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures); platform_io.ClearRendererHandlers(); } @implementation MetalBuffer - (instancetype)initWithBuffer:(id)buffer { if ((self = [super init])) { _buffer = buffer; _lastReuseTime = GetMachAbsoluteTimeInSeconds(); } return self; } @end #pragma mark - FramebufferDescriptor implementation @implementation FramebufferDescriptor - (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor { if ((self = [super init])) { _sampleCount = renderPassDescriptor.colorAttachments[0].texture.sampleCount; _colorPixelFormat = renderPassDescriptor.colorAttachments[0].texture.pixelFormat; _depthPixelFormat = renderPassDescriptor.depthAttachment.texture.pixelFormat; _stencilPixelFormat = renderPassDescriptor.stencilAttachment.texture.pixelFormat; } return self; } - (nonnull id)copyWithZone:(nullable NSZone*)zone { FramebufferDescriptor* copy = [[FramebufferDescriptor allocWithZone:zone] init]; copy.sampleCount = self.sampleCount; copy.colorPixelFormat = self.colorPixelFormat; copy.depthPixelFormat = self.depthPixelFormat; copy.stencilPixelFormat = self.stencilPixelFormat; return copy; } - (NSUInteger)hash { NSUInteger sc = _sampleCount & 0x3; NSUInteger cf = _colorPixelFormat & 0x3FF; NSUInteger df = _depthPixelFormat & 0x3FF; NSUInteger sf = _stencilPixelFormat & 0x3FF; NSUInteger hash = (sf << 22) | (df << 12) | (cf << 2) | sc; return hash; } - (BOOL)isEqual:(id)object { FramebufferDescriptor* other = object; if (![other isKindOfClass:[FramebufferDescriptor class]]) return NO; return other.sampleCount == self.sampleCount && other.colorPixelFormat == self.colorPixelFormat && other.depthPixelFormat == self.depthPixelFormat && other.stencilPixelFormat == self.stencilPixelFormat; } @end #pragma mark - MetalTexture implementation @implementation MetalTexture - (instancetype)initWithTexture:(id)metalTexture { if ((self = [super init])) self.metalTexture = metalTexture; return self; } @end #pragma mark - MetalContext implementation @implementation MetalContext - (instancetype)init { if ((self = [super init])) { self.renderPipelineStateCache = [NSMutableDictionary dictionary]; self.bufferCacheLock = [[NSObject alloc] init]; _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds(); } return self; } - (void)dealloc { free(_constantBufferContentsArray); } - (id)currentConstantBuffer { return self.constantBuffers[self.currentFrameSlot]; } - (ImGui_Metal4_ConstantData*)currentConstantBufferContents { return self.constantBufferContentsArray[self.currentFrameSlot]; } - (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device { double now = GetMachAbsoluteTimeInSeconds(); NSMutableArray* slotCache = self.bufferCaches[self.currentFrameSlot]; @synchronized(self.bufferCacheLock) { // Purge old buffers that haven't been useful for a while if (now - self.lastBufferCachePurge > 1.0) { for (NSMutableArray* cache in self.bufferCaches) { NSMutableArray* survivors = [NSMutableArray array]; for (MetalBuffer* candidate in cache) if (candidate.lastReuseTime > self.lastBufferCachePurge) [survivors addObject:candidate]; [cache setArray:survivors]; } self.lastBufferCachePurge = now; } // See if we have a buffer we can reuse, from this frame-in-flight slot's own cache MetalBuffer* bestCandidate = nil; for (MetalBuffer* candidate in slotCache) if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime)) bestCandidate = candidate; if (bestCandidate != nil) { [slotCache removeObject:bestCandidate]; bestCandidate.lastReuseTime = now; return bestCandidate; } } // No luck; make a new buffer id backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared]; [self.residencySet addAllocation:backing]; return [[MetalBuffer alloc] initWithBuffer:backing]; } const char* shaderCode = R"( #include using namespace metal; struct Uniforms { float4x4 projectionMatrix; }; struct VertexIn { float2 position [[attribute(0)]]; float2 texCoords [[attribute(1)]]; uchar4 color [[attribute(2)]]; }; struct VertexOut { float4 position [[position]]; float2 texCoords; float4 color; }; vertex VertexOut vertex_main(VertexIn in [[stage_in]], constant Uniforms &uniforms [[buffer(1)]]) { VertexOut out; out.position = uniforms.projectionMatrix * float4(in.position, 0, 1); out.texCoords = in.texCoords; out.color = float4(in.color) / float4(255.0); return out; } fragment half4 fragment_main(VertexOut in [[stage_in]], texture2d texture [[texture(0)]], sampler textureSampler [[sampler(0)]]) { half4 texColor = texture.sample(textureSampler, in.texCoords); return half4(in.color) * texColor; } )"; - (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device { NSError* error = nil; id library = [device newLibraryWithSource:[NSString stringWithUTF8String:shaderCode] options:nil error:&error]; if (library == nil) { NSLog(@"Error: failed to create Metal library: %@", error); return nil; } id vertexFunction = [library newFunctionWithName:@"vertex_main"]; id fragmentFunction = [library newFunctionWithName:@"fragment_main"]; if (vertexFunction == nil || fragmentFunction == nil) { NSLog(@"Error: failed to find Metal shader functions in library: %@", error); return nil; } MTLVertexDescriptor* vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; vertexDescriptor.attributes[0].offset = offsetof(ImDrawVert, pos); vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position vertexDescriptor.attributes[0].bufferIndex = 0; vertexDescriptor.attributes[1].offset = offsetof(ImDrawVert, uv); vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords vertexDescriptor.attributes[1].bufferIndex = 0; vertexDescriptor.attributes[2].offset = offsetof(ImDrawVert, col); vertexDescriptor.attributes[2].format = MTLVertexFormatUChar4; // color vertexDescriptor.attributes[2].bufferIndex = 0; vertexDescriptor.layouts[0].stepRate = 1; vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; vertexDescriptor.layouts[0].stride = sizeof(ImDrawVert); MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; pipelineDescriptor.vertexFunction = vertexFunction; pipelineDescriptor.fragmentFunction = fragmentFunction; pipelineDescriptor.vertexDescriptor = vertexDescriptor; pipelineDescriptor.rasterSampleCount = self.framebufferDescriptor.sampleCount; pipelineDescriptor.colorAttachments[0].pixelFormat = self.framebufferDescriptor.colorPixelFormat; pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; pipelineDescriptor.depthAttachmentPixelFormat = self.framebufferDescriptor.depthPixelFormat; pipelineDescriptor.stencilAttachmentPixelFormat = self.framebufferDescriptor.stencilPixelFormat; id renderPipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; if (error != nil) NSLog(@"Error: failed to create Metal pipeline state: %@", error); return renderPipelineState; } @end //----------------------------------------------------------------------------- #endif // #ifndef IMGUI_DISABLE