4
4
// Implemented features:
5
5
// [X] Renderer: User texture binding. Use 'MTLTexture' as ImTextureID. Read the FAQ about ImTextureID!
6
6
// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bit indices.
7
- // Missing features:
8
- // [ ] Renderer: Multi-viewport / platform windows.
7
+ // [X] Renderer: Multi-viewport / platform windows.
9
8
10
9
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
11
10
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
28
27
29
28
#include " imgui.h"
30
29
#include " imgui_impl_metal.h"
31
-
30
+ # import < time.h >
32
31
#import < Metal/Metal.h>
33
- // #import <QuartzCore/CAMetalLayer.h> // Not supported in XCode 9.2. Maybe a macro to detect the SDK version can be used (something like #if MACOS_SDK >= 10.13 ...)
34
- #import < simd/simd.h>
32
+
33
+ // Forward Declarations
34
+ static void ImGui_ImplMetal_InitPlatformInterface ();
35
+ static void ImGui_ImplMetal_ShutdownPlatformInterface ();
36
+ static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows ();
37
+ static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows ();
35
38
36
39
#pragma mark - Support classes
37
40
38
41
// A wrapper around a MTLBuffer object that knows the last time it was reused
39
42
@interface MetalBuffer : NSObject
40
43
@property (nonatomic , strong ) id <MTLBuffer > buffer;
41
- @property (nonatomic , assign ) NSTimeInterval lastReuseTime;
44
+ @property (nonatomic , assign ) double lastReuseTime;
42
45
- (instancetype )initWithBuffer : (id <MTLBuffer >)buffer ;
43
46
@end
44
47
@@ -61,7 +64,7 @@ @interface MetalContext : NSObject
61
64
@property (nonatomic , strong ) NSMutableDictionary *renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
62
65
@property (nonatomic , strong , nullable ) id <MTLTexture > fontTexture;
63
66
@property (nonatomic , strong ) NSMutableArray <MetalBuffer *> *bufferCache;
64
- @property (nonatomic , assign ) NSTimeInterval lastBufferCachePurge;
67
+ @property (nonatomic , assign ) double lastBufferCachePurge;
65
68
- (void )makeDeviceObjectsWithDevice : (id <MTLDevice >)device ;
66
69
- (void )makeFontTextureWithDevice : (id <MTLDevice >)device ;
67
70
- (MetalBuffer *)dequeueReusableBufferOfLength : (NSUInteger )length device : (id <MTLDevice >)device ;
@@ -81,6 +84,11 @@ - (void)renderDrawData:(ImDrawData *)drawData
81
84
82
85
static MetalContext *g_sharedMetalContext = nil ;
83
86
87
+ static inline CFTimeInterval GetMachAbsoluteTimeInSeconds ()
88
+ {
89
+ return static_cast <CFTimeInterval>(static_cast <double >(clock_gettime_nsec_np (CLOCK_UPTIME_RAW)) / 1e9 );
90
+ }
91
+
84
92
#ifdef IMGUI_IMPL_METAL_CPP
85
93
86
94
#pragma mark - Dear ImGui Metal C++ Backend API
@@ -124,6 +132,7 @@ bool ImGui_ImplMetal_Init(id<MTLDevice> device)
124
132
ImGuiIO& io = ImGui::GetIO ();
125
133
io.BackendRendererName = " imgui_impl_metal" ;
126
134
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
135
+ io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
127
136
128
137
static dispatch_once_t onceToken;
129
138
dispatch_once (&onceToken, ^{
@@ -132,11 +141,15 @@ bool ImGui_ImplMetal_Init(id<MTLDevice> device)
132
141
133
142
ImGui_ImplMetal_CreateDeviceObjects (device);
134
143
144
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
145
+ ImGui_ImplMetal_InitPlatformInterface ();
146
+
135
147
return true ;
136
148
}
137
149
138
150
void ImGui_ImplMetal_Shutdown ()
139
151
{
152
+ ImGui_ImplMetal_ShutdownPlatformInterface ();
140
153
ImGui_ImplMetal_DestroyDeviceObjects ();
141
154
}
142
155
@@ -174,6 +187,7 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
174
187
{
175
188
[g_sharedMetalContext makeDeviceObjectsWithDevice: device];
176
189
190
+ ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows ();
177
191
ImGui_ImplMetal_CreateFontsTexture (device);
178
192
179
193
return true ;
@@ -182,9 +196,163 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
182
196
void ImGui_ImplMetal_DestroyDeviceObjects ()
183
197
{
184
198
ImGui_ImplMetal_DestroyFontsTexture ();
199
+ ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows ();
200
+
185
201
[g_sharedMetalContext emptyRenderPipelineStateCache ];
186
202
}
187
203
204
+ #pragma mark - Multi-viewport support
205
+
206
+ #import < QuartzCore/CAMetalLayer.h>
207
+
208
+ #if TARGET_OS_OSX
209
+ #import < Cocoa/Cocoa.h>
210
+ #endif
211
+
212
+ // --------------------------------------------------------------------------------------------------------
213
+ // MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
214
+ // This is an _advanced_ and _optional_ feature, allowing the back-end to create and handle multiple viewports simultaneously.
215
+ // 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..
216
+ // --------------------------------------------------------------------------------------------------------
217
+
218
+ struct ImGuiViewportDataMetal
219
+ {
220
+ CAMetalLayer * MetalLayer;
221
+ id <MTLCommandQueue > CommandQueue;
222
+ MTLRenderPassDescriptor * RenderPassDescriptor;
223
+ void * Handle = nullptr ;
224
+ bool firstFrame = true ;
225
+ };
226
+
227
+ static void ImGui_ImplMetal_CreateWindow (ImGuiViewport* viewport)
228
+ {
229
+ auto data = IM_NEW (ImGuiViewportDataMetal)();
230
+ viewport->RendererUserData = data;
231
+
232
+ // PlatformHandleRaw should always be a NSWindow*, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*).
233
+ // Some back-ends will leave PlatformHandleRaw NULL, in which case we assume PlatformHandle will contain the NSWindow*.
234
+ void * handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle ;
235
+ IM_ASSERT (handle != nullptr );
236
+
237
+ id <MTLDevice > device = [g_sharedMetalContext.depthStencilState device ];
238
+
239
+ CAMetalLayer * layer = [CAMetalLayer layer ];
240
+ layer.device = device;
241
+ layer.framebufferOnly = YES ;
242
+ layer.pixelFormat = MTLPixelFormatBGRA8Unorm ;
243
+ #if TARGET_OS_OSX
244
+ NSWindow * window = (__bridge NSWindow *)handle;
245
+ NSView * view = window.contentView ;
246
+ view.layer = layer;
247
+ view.wantsLayer = YES ;
248
+ #endif
249
+ data->MetalLayer = layer;
250
+ data->CommandQueue = [device newCommandQueue ];
251
+ data->RenderPassDescriptor = [[MTLRenderPassDescriptor alloc ] init ];
252
+ data->Handle = handle;
253
+ }
254
+
255
+ static void ImGui_ImplMetal_DestroyWindow (ImGuiViewport* viewport)
256
+ {
257
+ // The main viewport (owned by the application) will always have RendererUserData == NULL since we didn't create the data for it.
258
+ if (auto data = (ImGuiViewportDataMetal*)viewport->RendererUserData )
259
+ {
260
+ IM_DELETE (data);
261
+ }
262
+ viewport->RendererUserData = nullptr ;
263
+ }
264
+
265
+ inline static CGSize MakeScaledSize (CGSize size, CGFloat scale) {
266
+ return CGSizeMake (size.width * scale, size.height * scale);
267
+ }
268
+
269
+ static void ImGui_ImplMetal_SetWindowSize (ImGuiViewport* viewport, ImVec2 size)
270
+ {
271
+ auto data = (ImGuiViewportDataMetal*)viewport->RendererUserData ;
272
+ data->MetalLayer .drawableSize = MakeScaledSize (CGSizeMake (size.x , size.y ), viewport->DpiScale );
273
+ }
274
+
275
+ static void ImGui_ImplMetal_RenderWindow (ImGuiViewport* viewport, void *)
276
+ {
277
+ auto data = (ImGuiViewportDataMetal *)viewport->RendererUserData ;
278
+
279
+ #if TARGET_OS_OSX
280
+ void * handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle ;
281
+ auto window = (__bridge NSWindow *)handle;
282
+
283
+ // Always render the firstFrame, regardless of occlusionState, to avoid an initial flicker
284
+ if ((window.occlusionState & NSWindowOcclusionStateVisible) == 0 && !data->firstFrame )
285
+ {
286
+ // Do not render windows which are completely occluded.
287
+ // FIX: Calling -[CAMetalLayer nextDrawable] will hang for approximately 1 second
288
+ // if the Metal layer is completely occluded.
289
+ return ;
290
+ }
291
+
292
+ data->firstFrame = false ;
293
+
294
+ viewport->DpiScale = static_cast <float >(window.backingScaleFactor );
295
+ if (data->MetalLayer .contentsScale != viewport->DpiScale )
296
+ {
297
+ data->MetalLayer .contentsScale = viewport->DpiScale ;
298
+ data->MetalLayer .drawableSize = MakeScaledSize (window.frame .size , viewport->DpiScale );
299
+ }
300
+ viewport->DrawData ->FramebufferScale = ImVec2 (viewport->DpiScale , viewport->DpiScale );
301
+ #endif
302
+
303
+ id <CAMetalDrawable > drawable = [data->MetalLayer nextDrawable ];
304
+ if (drawable == nil )
305
+ {
306
+ return ;
307
+ }
308
+
309
+ MTLRenderPassDescriptor * renderPassDescriptor = data->RenderPassDescriptor ;
310
+ renderPassDescriptor.colorAttachments [0 ].texture = drawable.texture ;
311
+ renderPassDescriptor.colorAttachments [0 ].clearColor = MTLClearColorMake (0 , 0 , 0 , 0 );
312
+ if ((viewport->Flags & ImGuiViewportFlags_NoRendererClear) == 0 )
313
+ {
314
+ renderPassDescriptor.colorAttachments [0 ].loadAction = MTLLoadActionClear ;
315
+ }
316
+
317
+ id <MTLCommandBuffer > commandBuffer = [data->CommandQueue commandBuffer ];
318
+ id <MTLRenderCommandEncoder > renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor: renderPassDescriptor];
319
+ ImGui_ImplMetal_RenderDrawData (viewport->DrawData , commandBuffer, renderEncoder);
320
+ [renderEncoder endEncoding ];
321
+
322
+ [commandBuffer presentDrawable: drawable];
323
+ [commandBuffer commit ];
324
+ }
325
+
326
+ static void ImGui_ImplMetal_InitPlatformInterface ()
327
+ {
328
+ ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO ();
329
+ platform_io.Renderer_CreateWindow = ImGui_ImplMetal_CreateWindow;
330
+ platform_io.Renderer_DestroyWindow = ImGui_ImplMetal_DestroyWindow;
331
+ platform_io.Renderer_SetWindowSize = ImGui_ImplMetal_SetWindowSize;
332
+ platform_io.Renderer_RenderWindow = ImGui_ImplMetal_RenderWindow;
333
+ }
334
+
335
+ static void ImGui_ImplMetal_ShutdownPlatformInterface ()
336
+ {
337
+ ImGui::DestroyPlatformWindows ();
338
+ }
339
+
340
+ static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows ()
341
+ {
342
+ ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO ();
343
+ for (int i = 1 ; i < platform_io.Viewports .Size ; i++)
344
+ if (!platform_io.Viewports [i]->RendererUserData )
345
+ ImGui_ImplMetal_CreateWindow (platform_io.Viewports [i]);
346
+ }
347
+
348
+ static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows ()
349
+ {
350
+ ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO ();
351
+ for (int i = 1 ; i < platform_io.Viewports .Size ; i++)
352
+ if (platform_io.Viewports [i]->RendererUserData )
353
+ ImGui_ImplMetal_DestroyWindow (platform_io.Viewports [i]);
354
+ }
355
+
188
356
#pragma mark - MetalBuffer implementation
189
357
190
358
@implementation MetalBuffer
@@ -193,7 +361,7 @@ - (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
193
361
if ((self = [super init ]))
194
362
{
195
363
_buffer = buffer;
196
- _lastReuseTime = [ NSDate date ]. timeIntervalSince1970 ;
364
+ _lastReuseTime = GetMachAbsoluteTimeInSeconds () ;
197
365
}
198
366
return self;
199
367
}
@@ -255,7 +423,7 @@ - (instancetype)init {
255
423
{
256
424
_renderPipelineStateCache = [NSMutableDictionary dictionary ];
257
425
_bufferCache = [NSMutableArray array ];
258
- _lastBufferCachePurge = [ NSDate date ]. timeIntervalSince1970 ;
426
+ _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds () ;
259
427
}
260
428
return self;
261
429
}
@@ -283,8 +451,14 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device
283
451
height: (NSUInteger )height
284
452
mipmapped: NO ];
285
453
textureDescriptor.usage = MTLTextureUsageShaderRead ;
454
+ // Only override the storageMode for macOS with GPUs supporting unified memory,
455
+ // as the default value, per Apple docs, is already set correctly.
286
456
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
287
- textureDescriptor.storageMode = MTLStorageModeManaged ;
457
+ if (@available (macOS 10.15 , macCatalyst 13.0 , *)) {
458
+ if (device.hasUnifiedMemory ) {
459
+ textureDescriptor.storageMode = MTLStorageModeShared ;
460
+ }
461
+ }
288
462
#else
289
463
textureDescriptor.storageMode = MTLStorageModeShared ;
290
464
#endif
@@ -295,32 +469,32 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device
295
469
296
470
- (MetalBuffer *)dequeueReusableBufferOfLength : (NSUInteger )length device : (id <MTLDevice >)device
297
471
{
298
- NSTimeInterval now = [ NSDate date ]. timeIntervalSince1970 ;
472
+ uint64_t now = GetMachAbsoluteTimeInSeconds () ;
299
473
300
474
// Purge old buffers that haven't been useful for a while
301
- if (now - self. lastBufferCachePurge > 1.0 )
475
+ if (now - _lastBufferCachePurge > 1.0 )
302
476
{
303
477
NSMutableArray *survivors = [NSMutableArray array ];
304
- for (MetalBuffer *candidate in self. bufferCache )
478
+ for (MetalBuffer *candidate in _bufferCache )
305
479
{
306
- if (candidate.lastReuseTime > self. lastBufferCachePurge )
480
+ if (candidate.lastReuseTime > _lastBufferCachePurge )
307
481
{
308
482
[survivors addObject: candidate];
309
483
}
310
484
}
311
- self. bufferCache = [survivors mutableCopy ];
312
- self. lastBufferCachePurge = now;
485
+ _bufferCache = [survivors mutableCopy ];
486
+ _lastBufferCachePurge = now;
313
487
}
314
488
315
489
// See if we have a buffer we can reuse
316
490
MetalBuffer *bestCandidate = nil ;
317
- for (MetalBuffer *candidate in self. bufferCache )
491
+ for (MetalBuffer *candidate in _bufferCache )
318
492
if (candidate.buffer .length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime ))
319
493
bestCandidate = candidate;
320
494
321
495
if (bestCandidate != nil )
322
496
{
323
- [self .bufferCache removeObject: bestCandidate];
497
+ [_bufferCache removeObject: bestCandidate];
324
498
bestCandidate.lastReuseTime = now;
325
499
return bestCandidate;
326
500
}
@@ -332,21 +506,21 @@ - (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTL
332
506
333
507
- (void )enqueueReusableBuffer : (MetalBuffer *)buffer
334
508
{
335
- [self .bufferCache addObject: buffer];
509
+ [_bufferCache addObject: buffer];
336
510
}
337
511
338
512
- (_Nullable id <MTLRenderPipelineState >)renderPipelineStateForFrameAndDevice : (id <MTLDevice >)device
339
513
{
340
514
// Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
341
515
// The hit rate for this cache should be very near 100%.
342
- id <MTLRenderPipelineState > renderPipelineState = self. renderPipelineStateCache [ self .framebufferDescriptor ];
516
+ id <MTLRenderPipelineState > renderPipelineState = _renderPipelineStateCache[_framebufferDescriptor ];
343
517
344
518
if (renderPipelineState == nil )
345
519
{
346
520
// No luck; make a new render pipeline state
347
- renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor: self .framebufferDescriptor device: device];
521
+ renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor: _framebufferDescriptor device: device];
348
522
// Cache render pipeline state for later reuse
349
- self. renderPipelineStateCache [ self .framebufferDescriptor ] = renderPipelineState;
523
+ _renderPipelineStateCache[_framebufferDescriptor ] = renderPipelineState;
350
524
}
351
525
352
526
return renderPipelineState;
0 commit comments