Skip to content

Commit 7330d26

Browse files
committed
macOS docking and viewports
1 parent e278277 commit 7330d26

File tree

5 files changed

+710
-152
lines changed

5 files changed

+710
-152
lines changed

backends/imgui_impl_metal.mm

Lines changed: 196 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
// Implemented features:
55
// [X] Renderer: User texture binding. Use 'MTLTexture' as ImTextureID. Read the FAQ about ImTextureID!
66
// [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.
98

109
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
1110
// 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,17 +27,21 @@
2827

2928
#include "imgui.h"
3029
#include "imgui_impl_metal.h"
31-
30+
#import <time.h>
3231
#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();
3538

3639
#pragma mark - Support classes
3740

3841
// A wrapper around a MTLBuffer object that knows the last time it was reused
3942
@interface MetalBuffer : NSObject
4043
@property (nonatomic, strong) id<MTLBuffer> buffer;
41-
@property (nonatomic, assign) NSTimeInterval lastReuseTime;
44+
@property (nonatomic, assign) double lastReuseTime;
4245
- (instancetype)initWithBuffer:(id<MTLBuffer>)buffer;
4346
@end
4447

@@ -61,7 +64,7 @@ @interface MetalContext : NSObject
6164
@property (nonatomic, strong) NSMutableDictionary *renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
6265
@property (nonatomic, strong, nullable) id<MTLTexture> fontTexture;
6366
@property (nonatomic, strong) NSMutableArray<MetalBuffer *> *bufferCache;
64-
@property (nonatomic, assign) NSTimeInterval lastBufferCachePurge;
67+
@property (nonatomic, assign) double lastBufferCachePurge;
6568
- (void)makeDeviceObjectsWithDevice:(id<MTLDevice>)device;
6669
- (void)makeFontTextureWithDevice:(id<MTLDevice>)device;
6770
- (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
@@ -81,6 +84,11 @@ - (void)renderDrawData:(ImDrawData *)drawData
8184

8285
static MetalContext *g_sharedMetalContext = nil;
8386

87+
static inline CFTimeInterval GetMachAbsoluteTimeInSeconds()
88+
{
89+
return static_cast<CFTimeInterval>(static_cast<double>(clock_gettime_nsec_np(CLOCK_UPTIME_RAW)) / 1e9);
90+
}
91+
8492
#ifdef IMGUI_IMPL_METAL_CPP
8593

8694
#pragma mark - Dear ImGui Metal C++ Backend API
@@ -124,6 +132,7 @@ bool ImGui_ImplMetal_Init(id<MTLDevice> device)
124132
ImGuiIO& io = ImGui::GetIO();
125133
io.BackendRendererName = "imgui_impl_metal";
126134
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)
127136

128137
static dispatch_once_t onceToken;
129138
dispatch_once(&onceToken, ^{
@@ -132,11 +141,15 @@ bool ImGui_ImplMetal_Init(id<MTLDevice> device)
132141

133142
ImGui_ImplMetal_CreateDeviceObjects(device);
134143

144+
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
145+
ImGui_ImplMetal_InitPlatformInterface();
146+
135147
return true;
136148
}
137149

138150
void ImGui_ImplMetal_Shutdown()
139151
{
152+
ImGui_ImplMetal_ShutdownPlatformInterface();
140153
ImGui_ImplMetal_DestroyDeviceObjects();
141154
}
142155

@@ -174,6 +187,7 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
174187
{
175188
[g_sharedMetalContext makeDeviceObjectsWithDevice:device];
176189

190+
ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
177191
ImGui_ImplMetal_CreateFontsTexture(device);
178192

179193
return true;
@@ -182,9 +196,163 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
182196
void ImGui_ImplMetal_DestroyDeviceObjects()
183197
{
184198
ImGui_ImplMetal_DestroyFontsTexture();
199+
ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();
200+
185201
[g_sharedMetalContext emptyRenderPipelineStateCache];
186202
}
187203

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+
188356
#pragma mark - MetalBuffer implementation
189357

190358
@implementation MetalBuffer
@@ -193,7 +361,7 @@ - (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
193361
if ((self = [super init]))
194362
{
195363
_buffer = buffer;
196-
_lastReuseTime = [NSDate date].timeIntervalSince1970;
364+
_lastReuseTime = GetMachAbsoluteTimeInSeconds();
197365
}
198366
return self;
199367
}
@@ -255,7 +423,7 @@ - (instancetype)init {
255423
{
256424
_renderPipelineStateCache = [NSMutableDictionary dictionary];
257425
_bufferCache = [NSMutableArray array];
258-
_lastBufferCachePurge = [NSDate date].timeIntervalSince1970;
426+
_lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
259427
}
260428
return self;
261429
}
@@ -283,8 +451,14 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device
283451
height:(NSUInteger)height
284452
mipmapped:NO];
285453
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.
286456
#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+
}
288462
#else
289463
textureDescriptor.storageMode = MTLStorageModeShared;
290464
#endif
@@ -295,32 +469,32 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device
295469

296470
- (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
297471
{
298-
NSTimeInterval now = [NSDate date].timeIntervalSince1970;
472+
uint64_t now = GetMachAbsoluteTimeInSeconds();
299473

300474
// Purge old buffers that haven't been useful for a while
301-
if (now - self.lastBufferCachePurge > 1.0)
475+
if (now - _lastBufferCachePurge > 1.0)
302476
{
303477
NSMutableArray *survivors = [NSMutableArray array];
304-
for (MetalBuffer *candidate in self.bufferCache)
478+
for (MetalBuffer *candidate in _bufferCache)
305479
{
306-
if (candidate.lastReuseTime > self.lastBufferCachePurge)
480+
if (candidate.lastReuseTime > _lastBufferCachePurge)
307481
{
308482
[survivors addObject:candidate];
309483
}
310484
}
311-
self.bufferCache = [survivors mutableCopy];
312-
self.lastBufferCachePurge = now;
485+
_bufferCache = [survivors mutableCopy];
486+
_lastBufferCachePurge = now;
313487
}
314488

315489
// See if we have a buffer we can reuse
316490
MetalBuffer *bestCandidate = nil;
317-
for (MetalBuffer *candidate in self.bufferCache)
491+
for (MetalBuffer *candidate in _bufferCache)
318492
if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
319493
bestCandidate = candidate;
320494

321495
if (bestCandidate != nil)
322496
{
323-
[self.bufferCache removeObject:bestCandidate];
497+
[_bufferCache removeObject:bestCandidate];
324498
bestCandidate.lastReuseTime = now;
325499
return bestCandidate;
326500
}
@@ -332,21 +506,21 @@ - (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTL
332506

333507
- (void)enqueueReusableBuffer:(MetalBuffer *)buffer
334508
{
335-
[self.bufferCache addObject:buffer];
509+
[_bufferCache addObject:buffer];
336510
}
337511

338512
- (_Nullable id<MTLRenderPipelineState>)renderPipelineStateForFrameAndDevice:(id<MTLDevice>)device
339513
{
340514
// Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
341515
// 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];
343517

344518
if (renderPipelineState == nil)
345519
{
346520
// No luck; make a new render pipeline state
347-
renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor:self.framebufferDescriptor device:device];
521+
renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor:_framebufferDescriptor device:device];
348522
// Cache render pipeline state for later reuse
349-
self.renderPipelineStateCache[self.framebufferDescriptor] = renderPipelineState;
523+
_renderPipelineStateCache[_framebufferDescriptor] = renderPipelineState;
350524
}
351525

352526
return renderPipelineState;

backends/imgui_impl_osx.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
// [X] Platform: OSX clipboard is supported within core Dear ImGui (no specific code in this backend).
99
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
1010
// [X] Platform: IME support.
11-
// Issues:
12-
// [ ] Platform: Multi-viewport / platform windows.
11+
// [X] Platform: Multi-viewport / platform windows.
1312

1413
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
1514
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.

0 commit comments

Comments
 (0)