40 KB、845行 生 Blame 履歴
1// dear imgui: Renderer Backend for Metal
2// This needs to be used along with a Platform Backend (e.g. OSX)
3
4// Implemented features:
5// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
6// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
7// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
8// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'.
9
10// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
11// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
12// Learn about Dear ImGui:
13// - FAQ https://dearimgui.com/faq
14// - Getting Started https://dearimgui.com/getting-started
15// - Documentation https://dearimgui.com/docs (same as your local docs/ folder).
16// - Introduction, links and more at the top of imgui.cpp
17
18// CHANGELOG
19// (minor and older changes stripped away, please see git history for details)
20// 2026-XX-XX: Metal: Added support for multiple windows via the ImGuiPlatformIO interface.
21// 2026-04-28: Added support for standard draw callbacks (in platform_io): DrawCallback_SetSamplerLinear and DrawCallback_SetSamplerNearest. (#9378, #9381)
22// 2026-04-23: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState (others are not yet supported). (#9378)
23// 2026-04-14: Metal: use a dedicated bufferCacheLock to avoid crashing when bufferCache is replaced by a new object while being used for @synchronize(). (#9367)
24// 2026-04-03: Metal: avoid redundant vertex buffer bind in SetupRenderState. (#9343)
25// 2026-03-19: Fixed issue in ImGui_ImplMetal_RenderDrawData() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310)
26// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
27// 2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture().
28// 2025-02-03: Metal: Crash fix. (#8367)
29// 2025-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419).
30// 2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'.
31// 2022-07-05: Metal: Add dispatch synchronization.
32// 2022-06-30: Metal: Use __bridge for ARC based systems.
33// 2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler.
34// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts.
35// 2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code).
36// 2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file.
37// 2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464)
38// 2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
39// 2021-02-18: Metal: Change blending equation to preserve alpha in output buffer.
40// 2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst.
41// 2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
42// 2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
43// 2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display.
44// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
45// 2018-07-05: Metal: Added new Metal backend implementation.
46
47#include "imgui.h"
48#ifndef IMGUI_DISABLE
49#include "imgui_impl_metal.h"
50#import <time.h>
51#import <Metal/Metal.h>
52
53// Forward Declarations
54static void ImGui_ImplMetal_InitMultiViewportSupport();
55static void ImGui_ImplMetal_ShutdownMultiViewportSupport();
56static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
57static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();
58
59#pragma mark - Support classes
60
61// A wrapper around a MTLBuffer object that knows the last time it was reused
62@interface MetalBuffer : NSObject
63@property (nonatomic, strong) id<MTLBuffer> buffer;
64@property (nonatomic, assign) double lastReuseTime;
65- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer;
66@end
67
68// An object that encapsulates the data necessary to uniquely identify a
69// render pipeline state. These are used as cache keys.
70@interface FramebufferDescriptor : NSObject<NSCopying>
71@property (nonatomic, assign) unsigned long sampleCount;
72@property (nonatomic, assign) MTLPixelFormat colorPixelFormat;
73@property (nonatomic, assign) MTLPixelFormat depthPixelFormat;
74@property (nonatomic, assign) MTLPixelFormat stencilPixelFormat;
75- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor;
76@end
77
78@interface MetalTexture : NSObject
79@property (nonatomic, strong) id<MTLTexture> metalTexture;
80- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture;
81@end
82
83// A singleton that stores long-lived objects that are needed by the Metal
84// renderer backend. Stores the render pipeline state cache and the default
85// font texture, and manages the reusable buffer cache.
86@interface MetalContext : NSObject
87@property (nonatomic, strong) id<MTLDevice> device;
88@property (nonatomic, strong) id<MTLDepthStencilState> depthStencilState;
89@property (nonatomic, strong) id<MTLSamplerState> samplerStateLinear;
90@property (nonatomic, strong) id<MTLSamplerState> samplerStateNearest;
91@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; // framebuffer descriptor for current frame; transient
92@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
93@property (nonatomic, strong) NSMutableArray<MetalBuffer*>* bufferCache;
94@property (nonatomic, strong) NSObject* bufferCacheLock;
95@property (nonatomic, assign) double lastBufferCachePurge;
96- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
97- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device;
98@end
99
100struct ImGui_ImplMetal_Data
101{
102 MetalContext* SharedMetalContext;
103 id<MTLRenderCommandEncoder> RenderCommandEncoder;
104
105 ImGui_ImplMetal_Data() { memset((void*)this, 0, sizeof(*this)); }
106};
107
108static ImGui_ImplMetal_Data* ImGui_ImplMetal_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; }
109static void ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); }
110
111static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); }
112
113#ifdef IMGUI_IMPL_METAL_CPP
114
115#pragma mark - Dear ImGui Metal C++ Backend API
116
117bool ImGui_ImplMetal_Init(MTL::Device* device)
118{
119 return ImGui_ImplMetal_Init((__bridge id<MTLDevice>)(device));
120}
121
122void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor)
123{
124 ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor));
125}
126
127void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
128 MTL::CommandBuffer* commandBuffer,
129 MTL::RenderCommandEncoder* commandEncoder)
130{
131 ImGui_ImplMetal_RenderDrawData(draw_data,
132 (__bridge id<MTLCommandBuffer>)(commandBuffer),
133 (__bridge id<MTLRenderCommandEncoder>)(commandEncoder));
134
135}
136
137bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device)
138{
139 return ImGui_ImplMetal_CreateDeviceObjects((__bridge id<MTLDevice>)(device));
140}
141
142#endif // #ifdef IMGUI_IMPL_METAL_CPP
143
144#pragma mark - Dear ImGui Metal Backend API
145
146void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor)
147{
148 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
149 IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?");
150#ifdef IMGUI_IMPL_METAL_CPP
151 bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease];
152#else
153 bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor];
154#endif
155 if (bd->SharedMetalContext.depthStencilState == nil)
156 ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device);
157}
158
159static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer,
160 id<MTLRenderCommandEncoder> commandEncoder, id<MTLRenderPipelineState> renderPipelineState,
161 MetalBuffer* vertexBuffer, size_t vertexBufferOffset)
162{
163 IM_UNUSED(commandBuffer);
164 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
165 [commandEncoder setCullMode:MTLCullModeNone];
166 [commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState];
167
168 // Setup viewport, orthographic projection matrix
169 // Our visible imgui space lies from draw_data->DisplayPos (top left) to
170 // draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayMin is typically (0,0) for single viewport apps.
171 MTLViewport viewport =
172 {
173 .originX = 0.0,
174 .originY = 0.0,
175 .width = (double)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x),
176 .height = (double)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y),
177 .znear = 0.0,
178 .zfar = 1.0
179 };
180 [commandEncoder setViewport:viewport];
181
182 float L = draw_data->DisplayPos.x;
183 float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x;
184 float T = draw_data->DisplayPos.y;
185 float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y;
186 float N = (float)viewport.znear;
187 float F = (float)viewport.zfar;
188 const float ortho_projection[4][4] =
189 {
190 { 2.0f/(R-L), 0.0f, 0.0f, 0.0f },
191 { 0.0f, 2.0f/(T-B), 0.0f, 0.0f },
192 { 0.0f, 0.0f, 1/(F-N), 0.0f },
193 { (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f },
194 };
195 [commandEncoder setVertexBytes:&ortho_projection length:sizeof(ortho_projection) atIndex:1];
196
197 [commandEncoder setRenderPipelineState:renderPipelineState];
198 [commandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0];
199
200 [commandEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBufferOffset atIndex:0];
201}
202
203// Draw callbacks
204static void ImGui_ImplMetal_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way.
205static void ImGui_ImplMetal_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; }
206static void ImGui_ImplMetal_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateNearest atIndex:0]; }
207
208// Metal Render function.
209void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer, id<MTLRenderCommandEncoder> commandEncoder)
210{
211 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
212 MetalContext* ctx = bd->SharedMetalContext;
213
214 // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
215 int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x);
216 int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y);
217 if (fb_width <= 0 || fb_height <= 0 || draw_data->CmdLists.Size == 0)
218 return;
219
220 // Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do.
221 // (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates).
222 if (draw_data->Textures != nullptr)
223 for (ImTextureData* tex : *draw_data->Textures)
224 if (tex->Status != ImTextureStatus_OK)
225 ImGui_ImplMetal_UpdateTexture(tex);
226
227 // Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
228 // The hit rate for this cache should be very near 100%.
229 id<MTLRenderPipelineState> renderPipelineState = ctx.renderPipelineStateCache[ctx.framebufferDescriptor];
230 if (renderPipelineState == nil)
231 {
232 // No luck; make a new render pipeline state
233 renderPipelineState = [ctx renderPipelineStateForFramebufferDescriptor:ctx.framebufferDescriptor device:commandBuffer.device];
234
235 // Cache render pipeline state for later reuse
236 ctx.renderPipelineStateCache[ctx.framebufferDescriptor] = renderPipelineState;
237 }
238
239 size_t vertexBufferLength = (size_t)draw_data->TotalVtxCount * sizeof(ImDrawVert);
240 size_t indexBufferLength = (size_t)draw_data->TotalIdxCount * sizeof(ImDrawIdx);
241 MetalBuffer* vertexBuffer = [ctx dequeueReusableBufferOfLength:vertexBufferLength device:commandBuffer.device];
242 MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device];
243
244 bd->RenderCommandEncoder = commandEncoder;
245 ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
246
247 // Will project scissor/clipping rectangles into framebuffer space
248 ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
249 ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
250
251 // Render command lists
252 size_t vertexBufferOffset = 0;
253 size_t indexBufferOffset = 0;
254 for (const ImDrawList* draw_list : draw_data->CmdLists)
255 {
256 memcpy((char*)vertexBuffer.buffer.contents + vertexBufferOffset, draw_list->VtxBuffer.Data, (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert));
257 memcpy((char*)indexBuffer.buffer.contents + indexBufferOffset, draw_list->IdxBuffer.Data, (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx));
258
259 for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++)
260 {
261 const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i];
262 if (pcmd->UserCallback)
263 {
264 // User callback, registered via ImDrawList::AddCallback()
265 if (pcmd->UserCallback == ImGui_ImplMetal_DrawCallback_ResetRenderState)
266 ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset);
267 else
268 pcmd->UserCallback(draw_list, pcmd);
269 }
270 else
271 {
272 // Project scissor/clipping rectangles into framebuffer space
273 ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y);
274 ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y);
275
276 // Clamp to viewport as setScissorRect() won't accept values that are off bounds
277 if (clip_min.x < 0.0f) { clip_min.x = 0.0f; }
278 if (clip_min.y < 0.0f) { clip_min.y = 0.0f; }
279 if (clip_max.x > (float)fb_width) { clip_max.x = (float)fb_width; }
280 if (clip_max.y > (float)fb_height) { clip_max.y = (float)fb_height; }
281 if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y)
282 continue;
283 if (pcmd->ElemCount == 0) // drawIndexedPrimitives() validation doesn't accept this
284 continue;
285
286 // Apply scissor/clipping rectangle
287 MTLScissorRect scissorRect =
288 {
289 .x = NSUInteger(clip_min.x),
290 .y = NSUInteger(clip_min.y),
291 .width = NSUInteger(clip_max.x - clip_min.x),
292 .height = NSUInteger(clip_max.y - clip_min.y)
293 };
294 [commandEncoder setScissorRect:scissorRect];
295
296 // Bind texture, Draw
297 ImTextureID tex_id = pcmd->GetTexID();
298 if (tex_id != ImTextureID_Invalid)
299 [commandEncoder setFragmentTexture:(__bridge id<MTLTexture>)(void*)(intptr_t)(tex_id) atIndex:0];
300
301 [commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0];
302 [commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
303 indexCount:pcmd->ElemCount
304 indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32
305 indexBuffer:indexBuffer.buffer
306 indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)];
307 }
308 }
309
310 vertexBufferOffset += (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert);
311 indexBufferOffset += (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx);
312 }
313
314 MetalContext* sharedMetalContext = bd->SharedMetalContext;
315 [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>)
316 {
317 @synchronized(sharedMetalContext.bufferCacheLock)
318 {
319 [sharedMetalContext.bufferCache addObject:vertexBuffer];
320 [sharedMetalContext.bufferCache addObject:indexBuffer];
321 }
322 }];
323 bd->RenderCommandEncoder = nil;
324}
325
326static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex)
327{
328 if (MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData))
329 {
330 IM_ASSERT(backend_tex.metalTexture == (__bridge id<MTLTexture>)(void*)(intptr_t)tex->TexID);
331 backend_tex.metalTexture = nil;
332
333 // Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running)
334 tex->SetTexID(ImTextureID_Invalid);
335 tex->BackendUserData = nullptr;
336 }
337 tex->SetStatus(ImTextureStatus_Destroyed);
338}
339
340void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex)
341{
342 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
343 if (tex->Status == ImTextureStatus_WantCreate)
344 {
345 // Create and upload new texture to graphics system
346 //IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height);
347 IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr);
348 IM_ASSERT(tex->Format == ImTextureFormat_RGBA32);
349
350 // We are retrieving and uploading the font atlas as a 4-channels RGBA texture here.
351 // In theory we could call GetTexDataAsAlpha8() and upload a 1-channel texture to save on memory access bandwidth.
352 // However, using a shader designed for 1-channel texture would make it less obvious to use the ImTextureID facility to render users own textures.
353 // You can make that change in your implementation.
354 MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
355 width:(NSUInteger)tex->Width
356 height:(NSUInteger)tex->Height
357 mipmapped:NO];
358 textureDescriptor.usage = MTLTextureUsageShaderRead;
359 #if TARGET_OS_OSX || TARGET_OS_MACCATALYST
360 textureDescriptor.storageMode = MTLStorageModeManaged;
361 #else
362 textureDescriptor.storageMode = MTLStorageModeShared;
363 #endif
364 id <MTLTexture> texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor];
365 [texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4];
366 MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture];
367
368 // Store identifiers
369 tex->SetTexID((ImTextureID)(intptr_t)texture);
370 tex->SetStatus(ImTextureStatus_OK);
371 tex->BackendUserData = (__bridge_retained void*)(backend_tex);
372 }
373 else if (tex->Status == ImTextureStatus_WantUpdates)
374 {
375 // Update selected blocks. We only ever write to textures regions which have never been used before!
376 // This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region.
377 MetalTexture* backend_tex = (__bridge MetalTexture*)(tex->BackendUserData);
378 for (ImTextureRect& r : tex->Updates)
379 {
380 [backend_tex.metalTexture replaceRegion:MTLRegionMake2D((NSUInteger)r.x, (NSUInteger)r.y, (NSUInteger)r.w, (NSUInteger)r.h)
381 mipmapLevel:0
382 withBytes:tex->GetPixelsAt(r.x, r.y)
383 bytesPerRow:(NSUInteger)tex->Width * 4];
384 }
385 tex->SetStatus(ImTextureStatus_OK);
386 }
387 else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
388 {
389 ImGui_ImplMetal_DestroyTexture(tex);
390 }
391}
392
393bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
394{
395 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
396 MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init];
397 depthStencilDescriptor.depthWriteEnabled = NO;
398 depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways;
399 bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
400 MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init];
401 samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear;
402 samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
403 samplerDescriptor.mipFilter = MTLSamplerMipFilterLinear;
404 bd->SharedMetalContext.samplerStateLinear = [device newSamplerStateWithDescriptor:samplerDescriptor];
405 samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest;
406 samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest;
407 samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest;
408 bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor];
409
410 ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
411#ifdef IMGUI_IMPL_METAL_CPP
412 [samplerDescriptor release];
413 [depthStencilDescriptor release];
414#endif
415
416 return true;
417}
418
419void ImGui_ImplMetal_DestroyDeviceObjects()
420{
421 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
422
423 // Destroy all textures
424 for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
425 if (tex->RefCount == 1)
426 ImGui_ImplMetal_DestroyTexture(tex);
427
428 ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();
429 [bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
430 bd->SharedMetalContext.samplerStateLinear = nil;
431 bd->SharedMetalContext.samplerStateNearest = nil;
432}
433
434bool ImGui_ImplMetal_Init(id<MTLDevice> device)
435{
436 ImGuiIO& io = ImGui::GetIO();
437 IMGUI_CHECKVERSION();
438 IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
439
440 ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)();
441 io.BackendRendererUserData = (void*)bd;
442 io.BackendRendererName = "imgui_impl_metal";
443 io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
444 io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
445 io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
446
447 ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
448 platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal_DrawCallback_ResetRenderState;
449 platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal_DrawCallback_SetSamplerLinear;
450 platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal_DrawCallback_SetSamplerNearest;
451
452 bd->SharedMetalContext = [[MetalContext alloc] init];
453 bd->SharedMetalContext.device = device;
454
455 ImGui_ImplMetal_InitMultiViewportSupport();
456
457 return true;
458}
459
460void ImGui_ImplMetal_Shutdown()
461{
462 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
463 IM_UNUSED(bd);
464 IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
465 ImGuiIO& io = ImGui::GetIO();
466 ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
467
468 ImGui_ImplMetal_ShutdownMultiViewportSupport();
469 ImGui_ImplMetal_DestroyDeviceObjects();
470 ImGui_ImplMetal_DestroyBackendData();
471
472 io.BackendRendererName = nullptr;
473 io.BackendRendererUserData = nullptr;
474 io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures | ImGuiBackendFlags_RendererHasViewports);
475 platform_io.ClearRendererHandlers();
476}
477
478#pragma mark - Multi-viewport support
479
480#import <QuartzCore/CAMetalLayer.h>
481
482#if TARGET_OS_OSX
483#import <Cocoa/Cocoa.h>
484#endif
485
486//--------------------------------------------------------------------------------------------------------
487// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
488// This is an _advanced_ and _optional_ feature, allowing the back-end to create and handle multiple viewports simultaneously.
489// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first..
490//--------------------------------------------------------------------------------------------------------
491
492struct ImGuiViewportDataMetal
493{
494 CAMetalLayer* MetalLayer;
495 id<MTLCommandQueue> CommandQueue;
496 MTLRenderPassDescriptor* RenderPassDescriptor;
497 void* Handle = nullptr;
498 bool FirstFrame = true;
499};
500
501static void ImGui_ImplMetal_CreateWindow(ImGuiViewport* viewport)
502{
503 ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
504 ImGuiViewportDataMetal* data = IM_NEW(ImGuiViewportDataMetal)();
505 viewport->RendererUserData = data;
506
507 // PlatformHandleRaw should always be a NSWindow*, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*).
508 // Some back-ends will leave PlatformHandleRaw == 0, in which case we assume PlatformHandle will contain the NSWindow*.
509 void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
510 IM_ASSERT(handle != nullptr);
511
512 id<MTLDevice> device = bd->SharedMetalContext.device;
513 CAMetalLayer* layer = [CAMetalLayer layer];
514 layer.device = device;
515 layer.framebufferOnly = YES;
516 layer.pixelFormat = bd->SharedMetalContext.framebufferDescriptor.colorPixelFormat;
517#if TARGET_OS_OSX
518 NSWindow* window = (__bridge NSWindow*)handle;
519 NSView* view = window.contentView;
520 view.layer = layer;
521 view.wantsLayer = YES;
522#endif
523 data->MetalLayer = layer;
524 data->CommandQueue = [device newCommandQueue];
525 data->RenderPassDescriptor = [[MTLRenderPassDescriptor alloc] init];
526 data->Handle = handle;
527}
528
529static void ImGui_ImplMetal_DestroyWindow(ImGuiViewport* viewport)
530{
531 // The main viewport (owned by the application) will always have RendererUserData == 0 since we didn't create the data for it.
532 if (ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData)
533 IM_DELETE(data);
534 viewport->RendererUserData = nullptr;
535}
536
537inline static CGSize MakeScaledSize(CGSize size, CGFloat scale)
538{
539 return CGSizeMake(size.width * scale, size.height * scale);
540}
541
542static void ImGui_ImplMetal_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
543{
544 ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
545 data->MetalLayer.drawableSize = MakeScaledSize(CGSizeMake(size.x, size.y), viewport->DpiScale);
546}
547
548static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
549{
550 ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
551
552#if TARGET_OS_OSX
553 void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
554 NSWindow* window = (__bridge NSWindow*)handle;
555
556 // Always render the first frame, regardless of occlusionState, to avoid an initial flicker
557 if ((window.occlusionState & NSWindowOcclusionStateVisible) == 0 && !data->FirstFrame)
558 {
559 // Do not render windows which are completely occluded. Calling -[CAMetalLayer nextDrawable] will hang for
560 // approximately 1 second if the Metal layer is completely occluded.
561 return;
562 }
563 data->FirstFrame = false;
564
565 float fb_scale = (float)window.backingScaleFactor;
566 if (data->MetalLayer.contentsScale != fb_scale)
567 {
568 data->MetalLayer.contentsScale = fb_scale;
569 data->MetalLayer.drawableSize = MakeScaledSize(window.frame.size, fb_scale);
570 }
571#endif
572
573 id <CAMetalDrawable> drawable = [data->MetalLayer nextDrawable];
574 if (drawable == nil)
575 return;
576
577 MTLRenderPassDescriptor* renderPassDescriptor = data->RenderPassDescriptor;
578 renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
579 renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
580 if ((viewport->Flags & ImGuiViewportFlags_NoRendererClear) == 0)
581 renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
582
583 id <MTLCommandBuffer> commandBuffer = [data->CommandQueue commandBuffer];
584 id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
585 ImGui_ImplMetal_RenderDrawData(viewport->DrawData, commandBuffer, renderEncoder);
586 [renderEncoder endEncoding];
587
588 [commandBuffer presentDrawable:drawable];
589 [commandBuffer commit];
590}
591
592static void ImGui_ImplMetal_InitMultiViewportSupport()
593{
594 ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
595 platform_io.Renderer_CreateWindow = ImGui_ImplMetal_CreateWindow;
596 platform_io.Renderer_DestroyWindow = ImGui_ImplMetal_DestroyWindow;
597 platform_io.Renderer_SetWindowSize = ImGui_ImplMetal_SetWindowSize;
598 platform_io.Renderer_RenderWindow = ImGui_ImplMetal_RenderWindow;
599}
600
601static void ImGui_ImplMetal_ShutdownMultiViewportSupport()
602{
603 ImGui::DestroyPlatformWindows();
604}
605
606static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows()
607{
608 ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
609 for (int i = 1; i < platform_io.Viewports.Size; i++)
610 if (!platform_io.Viewports[i]->RendererUserData)
611 ImGui_ImplMetal_CreateWindow(platform_io.Viewports[i]);
612}
613
614static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows()
615{
616 ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
617 for (int i = 1; i < platform_io.Viewports.Size; i++)
618 if (platform_io.Viewports[i]->RendererUserData)
619 ImGui_ImplMetal_DestroyWindow(platform_io.Viewports[i]);
620}
621
622#pragma mark - MetalBuffer implementation
623
624@implementation MetalBuffer
625- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
626{
627 if ((self = [super init]))
628 {
629 _buffer = buffer;
630 _lastReuseTime = GetMachAbsoluteTimeInSeconds();
631 }
632 return self;
633}
634@end
635
636#pragma mark - FramebufferDescriptor implementation
637
638@implementation FramebufferDescriptor
639- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor
640{
641 if ((self = [super init]))
642 {
643 _sampleCount = renderPassDescriptor.colorAttachments[0].texture.sampleCount;
644 _colorPixelFormat = renderPassDescriptor.colorAttachments[0].texture.pixelFormat;
645 _depthPixelFormat = renderPassDescriptor.depthAttachment.texture.pixelFormat;
646 _stencilPixelFormat = renderPassDescriptor.stencilAttachment.texture.pixelFormat;
647 }
648 return self;
649}
650
651- (nonnull id)copyWithZone:(nullable NSZone*)zone
652{
653 FramebufferDescriptor* copy = [[FramebufferDescriptor allocWithZone:zone] init];
654 copy.sampleCount = self.sampleCount;
655 copy.colorPixelFormat = self.colorPixelFormat;
656 copy.depthPixelFormat = self.depthPixelFormat;
657 copy.stencilPixelFormat = self.stencilPixelFormat;
658 return copy;
659}
660
661- (NSUInteger)hash
662{
663 NSUInteger sc = _sampleCount & 0x3;
664 NSUInteger cf = _colorPixelFormat & 0x3FF;
665 NSUInteger df = _depthPixelFormat & 0x3FF;
666 NSUInteger sf = _stencilPixelFormat & 0x3FF;
667 NSUInteger hash = (sf << 22) | (df << 12) | (cf << 2) | sc;
668 return hash;
669}
670
671- (BOOL)isEqual:(id)object
672{
673 FramebufferDescriptor* other = object;
674 if (![other isKindOfClass:[FramebufferDescriptor class]])
675 return NO;
676 return other.sampleCount == self.sampleCount &&
677 other.colorPixelFormat == self.colorPixelFormat &&
678 other.depthPixelFormat == self.depthPixelFormat &&
679 other.stencilPixelFormat == self.stencilPixelFormat;
680}
681
682@end
683
684#pragma mark - MetalTexture implementation
685
686@implementation MetalTexture
687- (instancetype)initWithTexture:(id<MTLTexture>)metalTexture
688{
689 if ((self = [super init]))
690 self.metalTexture = metalTexture;
691 return self;
692}
693
694@end
695
696#pragma mark - MetalContext implementation
697
698@implementation MetalContext
699- (instancetype)init
700{
701 if ((self = [super init]))
702 {
703 self.renderPipelineStateCache = [NSMutableDictionary dictionary];
704 self.bufferCache = [NSMutableArray array];
705 self.bufferCacheLock = [[NSObject alloc] init];
706 _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
707 }
708 return self;
709}
710
711- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
712{
713 double now = GetMachAbsoluteTimeInSeconds();
714
715 @synchronized(self.bufferCacheLock)
716 {
717 // Purge old buffers that haven't been useful for a while
718 if (now - self.lastBufferCachePurge > 1.0)
719 {
720 NSMutableArray* survivors = [NSMutableArray array];
721 for (MetalBuffer* candidate in self.bufferCache)
722 if (candidate.lastReuseTime > self.lastBufferCachePurge)
723 [survivors addObject:candidate];
724 self.bufferCache = [survivors mutableCopy];
725 self.lastBufferCachePurge = now;
726 }
727
728 // See if we have a buffer we can reuse
729 MetalBuffer* bestCandidate = nil;
730 for (MetalBuffer* candidate in self.bufferCache)
731 if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
732 bestCandidate = candidate;
733
734 if (bestCandidate != nil)
735 {
736 [self.bufferCache removeObject:bestCandidate];
737 bestCandidate.lastReuseTime = now;
738 return bestCandidate;
739 }
740 }
741
742 // No luck; make a new buffer
743 id<MTLBuffer> backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared];
744 return [[MetalBuffer alloc] initWithBuffer:backing];
745}
746
747// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling.
748- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device
749{
750 NSError* error = nil;
751
752 NSString* shaderSource = @""
753 "#include <metal_stdlib>\n"
754 "using namespace metal;\n"
755 "\n"
756 "struct Uniforms {\n"
757 " float4x4 projectionMatrix;\n"
758 "};\n"
759 "\n"
760 "struct VertexIn {\n"
761 " float2 position [[attribute(0)]];\n"
762 " float2 texCoords [[attribute(1)]];\n"
763 " uchar4 color [[attribute(2)]];\n"
764 "};\n"
765 "\n"
766 "struct VertexOut {\n"
767 " float4 position [[position]];\n"
768 " float2 texCoords;\n"
769 " float4 color;\n"
770 "};\n"
771 "\n"
772 "vertex VertexOut vertex_main(VertexIn in [[stage_in]],\n"
773 " constant Uniforms &uniforms [[buffer(1)]]) {\n"
774 " VertexOut out;\n"
775 " out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n"
776 " out.texCoords = in.texCoords;\n"
777 " out.color = float4(in.color) / float4(255.0);\n"
778 " return out;\n"
779 "}\n"
780 "\n"
781 "fragment half4 fragment_main(VertexOut in [[stage_in]],\n"
782 " texture2d<half, access::sample> texture [[texture(0)]],\n"
783 " sampler textureSampler [[sampler(0)]]) {\n"
784 " half4 texColor = texture.sample(textureSampler, in.texCoords);\n"
785 " return half4(in.color) * texColor;\n"
786 "}\n";
787
788 id<MTLLibrary> library = [device newLibraryWithSource:shaderSource options:nil error:&error];
789 if (library == nil)
790 {
791 NSLog(@"Error: failed to create Metal library: %@", error);
792 return nil;
793 }
794
795 id<MTLFunction> vertexFunction = [library newFunctionWithName:@"vertex_main"];
796 id<MTLFunction> fragmentFunction = [library newFunctionWithName:@"fragment_main"];
797
798 if (vertexFunction == nil || fragmentFunction == nil)
799 {
800 NSLog(@"Error: failed to find Metal shader functions in library: %@", error);
801 return nil;
802 }
803
804 MTLVertexDescriptor* vertexDescriptor = [MTLVertexDescriptor vertexDescriptor];
805 vertexDescriptor.attributes[0].offset = offsetof(ImDrawVert, pos);
806 vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
807 vertexDescriptor.attributes[0].bufferIndex = 0;
808 vertexDescriptor.attributes[1].offset = offsetof(ImDrawVert, uv);
809 vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords
810 vertexDescriptor.attributes[1].bufferIndex = 0;
811 vertexDescriptor.attributes[2].offset = offsetof(ImDrawVert, col);
812 vertexDescriptor.attributes[2].format = MTLVertexFormatUChar4; // color
813 vertexDescriptor.attributes[2].bufferIndex = 0;
814 vertexDescriptor.layouts[0].stepRate = 1;
815 vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
816 vertexDescriptor.layouts[0].stride = sizeof(ImDrawVert);
817
818 MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
819 pipelineDescriptor.vertexFunction = vertexFunction;
820 pipelineDescriptor.fragmentFunction = fragmentFunction;
821 pipelineDescriptor.vertexDescriptor = vertexDescriptor;
822 pipelineDescriptor.rasterSampleCount = self.framebufferDescriptor.sampleCount;
823 pipelineDescriptor.colorAttachments[0].pixelFormat = self.framebufferDescriptor.colorPixelFormat;
824 pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
825 pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
826 pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
827 pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
828 pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
829 pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
830 pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
831 pipelineDescriptor.depthAttachmentPixelFormat = self.framebufferDescriptor.depthPixelFormat;
832 pipelineDescriptor.stencilAttachmentPixelFormat = self.framebufferDescriptor.stencilPixelFormat;
833
834 id<MTLRenderPipelineState> renderPipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
835 if (error != nil)
836 NSLog(@"Error: failed to create Metal pipeline state: %@", error);
837
838 return renderPipelineState;
839}
840
841@end
842
843//-----------------------------------------------------------------------------
844
845#endif // #ifndef IMGUI_DISABLE