Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete macOS docking and viewport support #4821

Open
wants to merge 1 commit into
base: docking
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 196 additions & 22 deletions backends/imgui_impl_metal.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
// Implemented features:
// [X] Renderer: User texture binding. Use 'MTLTexture' as ImTextureID. Read the FAQ about ImTextureID!
// [X] Renderer: Support for large meshes (64k+ vertices) with 16-bit indices.
// Missing features:
// [ ] Renderer: Multi-viewport / platform windows.
// [X] Renderer: Multi-viewport / platform windows.

// 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.
Expand All @@ -28,17 +27,21 @@

#include "imgui.h"
#include "imgui_impl_metal.h"

#import <time.h>
#import <Metal/Metal.h>
// #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 ...)
#import <simd/simd.h>

// Forward Declarations
static void ImGui_ImplMetal_InitPlatformInterface();
static void ImGui_ImplMetal_ShutdownPlatformInterface();
static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();

#pragma mark - Support classes

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

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

static MetalContext *g_sharedMetalContext = nil;

static inline CFTimeInterval GetMachAbsoluteTimeInSeconds()
{
return static_cast<CFTimeInterval>(static_cast<double>(clock_gettime_nsec_np(CLOCK_UPTIME_RAW)) / 1e9);
}

#ifdef IMGUI_IMPL_METAL_CPP

#pragma mark - Dear ImGui Metal C++ Backend API
Expand Down Expand Up @@ -124,6 +132,7 @@ bool ImGui_ImplMetal_Init(id<MTLDevice> device)
ImGuiIO& io = ImGui::GetIO();
io.BackendRendererName = "imgui_impl_metal";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)

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

ImGui_ImplMetal_CreateDeviceObjects(device);

if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
ImGui_ImplMetal_InitPlatformInterface();

return true;
}

void ImGui_ImplMetal_Shutdown()
{
ImGui_ImplMetal_ShutdownPlatformInterface();
ImGui_ImplMetal_DestroyDeviceObjects();
}

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

ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
ImGui_ImplMetal_CreateFontsTexture(device);

return true;
Expand All @@ -182,9 +196,163 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
void ImGui_ImplMetal_DestroyDeviceObjects()
{
ImGui_ImplMetal_DestroyFontsTexture();
ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();

[g_sharedMetalContext emptyRenderPipelineStateCache];
}

#pragma mark - Multi-viewport support

#import <QuartzCore/CAMetalLayer.h>

#if TARGET_OS_OSX
#import <Cocoa/Cocoa.h>
#endif

//--------------------------------------------------------------------------------------------------------
// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
// This is an _advanced_ and _optional_ feature, allowing the back-end to create and handle multiple viewports simultaneously.
// 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..
//--------------------------------------------------------------------------------------------------------

struct ImGuiViewportDataMetal
{
CAMetalLayer* MetalLayer;
id<MTLCommandQueue> CommandQueue;
MTLRenderPassDescriptor* RenderPassDescriptor;
void* Handle = nullptr;
bool firstFrame = true;
};

static void ImGui_ImplMetal_CreateWindow(ImGuiViewport* viewport)
{
auto data = IM_NEW(ImGuiViewportDataMetal)();
viewport->RendererUserData = data;

// PlatformHandleRaw should always be a NSWindow*, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*).
// Some back-ends will leave PlatformHandleRaw NULL, in which case we assume PlatformHandle will contain the NSWindow*.
void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
IM_ASSERT(handle != nullptr);

id<MTLDevice> device = [g_sharedMetalContext.depthStencilState device];

CAMetalLayer* layer = [CAMetalLayer layer];
layer.device = device;
layer.framebufferOnly = YES;
layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
#if TARGET_OS_OSX
NSWindow* window = (__bridge NSWindow*)handle;
NSView* view = window.contentView;
view.layer = layer;
view.wantsLayer = YES;
#endif
data->MetalLayer = layer;
data->CommandQueue = [device newCommandQueue];
data->RenderPassDescriptor = [[MTLRenderPassDescriptor alloc] init];
data->Handle = handle;
}

static void ImGui_ImplMetal_DestroyWindow(ImGuiViewport* viewport)
{
// The main viewport (owned by the application) will always have RendererUserData == NULL since we didn't create the data for it.
if (auto data = (ImGuiViewportDataMetal*)viewport->RendererUserData)
{
IM_DELETE(data);
}
viewport->RendererUserData = nullptr;
}

inline static CGSize MakeScaledSize(CGSize size, CGFloat scale) {
return CGSizeMake(size.width * scale, size.height * scale);
}

static void ImGui_ImplMetal_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
{
auto data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
data->MetalLayer.drawableSize = MakeScaledSize(CGSizeMake(size.x, size.y), viewport->DpiScale);
}

static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
{
auto data = (ImGuiViewportDataMetal *)viewport->RendererUserData;

#if TARGET_OS_OSX
void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
auto window = (__bridge NSWindow *)handle;

// Always render the firstFrame, regardless of occlusionState, to avoid an initial flicker
if ((window.occlusionState & NSWindowOcclusionStateVisible) == 0 && !data->firstFrame)
{
// Do not render windows which are completely occluded.
// FIX: Calling -[CAMetalLayer nextDrawable] will hang for approximately 1 second
// if the Metal layer is completely occluded.
return;
}

data->firstFrame = false;

viewport->DpiScale = static_cast<float>(window.backingScaleFactor);
if (data->MetalLayer.contentsScale != viewport->DpiScale)
{
data->MetalLayer.contentsScale = viewport->DpiScale;
data->MetalLayer.drawableSize = MakeScaledSize(window.frame.size, viewport->DpiScale);
}
viewport->DrawData->FramebufferScale = ImVec2(viewport->DpiScale, viewport->DpiScale);
#endif

id <CAMetalDrawable> drawable = [data->MetalLayer nextDrawable];
if (drawable == nil)
{
return;
}

MTLRenderPassDescriptor* renderPassDescriptor = data->RenderPassDescriptor;
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
if ((viewport->Flags & ImGuiViewportFlags_NoRendererClear) == 0)
{
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
}

id <MTLCommandBuffer> commandBuffer = [data->CommandQueue commandBuffer];
id <MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
ImGui_ImplMetal_RenderDrawData(viewport->DrawData, commandBuffer, renderEncoder);
[renderEncoder endEncoding];

[commandBuffer presentDrawable:drawable];
[commandBuffer commit];
}

static void ImGui_ImplMetal_InitPlatformInterface()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_CreateWindow = ImGui_ImplMetal_CreateWindow;
platform_io.Renderer_DestroyWindow = ImGui_ImplMetal_DestroyWindow;
platform_io.Renderer_SetWindowSize = ImGui_ImplMetal_SetWindowSize;
platform_io.Renderer_RenderWindow = ImGui_ImplMetal_RenderWindow;
}

static void ImGui_ImplMetal_ShutdownPlatformInterface()
{
ImGui::DestroyPlatformWindows();
}

static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
for (int i = 1; i < platform_io.Viewports.Size; i++)
if (!platform_io.Viewports[i]->RendererUserData)
ImGui_ImplMetal_CreateWindow(platform_io.Viewports[i]);
}

static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
for (int i = 1; i < platform_io.Viewports.Size; i++)
if (platform_io.Viewports[i]->RendererUserData)
ImGui_ImplMetal_DestroyWindow(platform_io.Viewports[i]);
}

#pragma mark - MetalBuffer implementation

@implementation MetalBuffer
Expand All @@ -193,7 +361,7 @@ - (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
if ((self = [super init]))
{
_buffer = buffer;
_lastReuseTime = [NSDate date].timeIntervalSince1970;
_lastReuseTime = GetMachAbsoluteTimeInSeconds();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does change to timing function fix anything, or is it just a "good practice" change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just good practice, as [NSDate data].timeIntervalSince1970 is a bit slower (showed up in profiling).

}
return self;
}
Expand Down Expand Up @@ -255,7 +423,7 @@ - (instancetype)init {
{
_renderPipelineStateCache = [NSMutableDictionary dictionary];
_bufferCache = [NSMutableArray array];
_lastBufferCachePurge = [NSDate date].timeIntervalSince1970;
_lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
}
return self;
}
Expand Down Expand Up @@ -283,8 +451,14 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device
height:(NSUInteger)height
mipmapped:NO];
textureDescriptor.usage = MTLTextureUsageShaderRead;
// Only override the storageMode for macOS with GPUs supporting unified memory,
// as the default value, per Apple docs, is already set correctly.
#if TARGET_OS_OSX || TARGET_OS_MACCATALYST
textureDescriptor.storageMode = MTLStorageModeManaged;
if (@available(macOS 10.15, macCatalyst 13.0, *)) {
if (device.hasUnifiedMemory) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not build on 10.13 (Xcode 10.1). macCatalyst and hasUnifiedMemory symbols are not available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add preprocessor checks for this.

textureDescriptor.storageMode = MTLStorageModeShared;
}
}
#else
textureDescriptor.storageMode = MTLStorageModeShared;
#endif
Expand All @@ -295,32 +469,32 @@ - (void)makeFontTextureWithDevice:(id<MTLDevice>)device

- (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
{
NSTimeInterval now = [NSDate date].timeIntervalSince1970;
uint64_t now = GetMachAbsoluteTimeInSeconds();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does timing function change here change any behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, as the two values are time instants, so the mathematics is the same.


// Purge old buffers that haven't been useful for a while
if (now - self.lastBufferCachePurge > 1.0)
if (now - _lastBufferCachePurge > 1.0)
{
NSMutableArray *survivors = [NSMutableArray array];
for (MetalBuffer *candidate in self.bufferCache)
for (MetalBuffer *candidate in _bufferCache)
{
if (candidate.lastReuseTime > self.lastBufferCachePurge)
if (candidate.lastReuseTime > _lastBufferCachePurge)
{
[survivors addObject:candidate];
}
}
self.bufferCache = [survivors mutableCopy];
self.lastBufferCachePurge = now;
_bufferCache = [survivors mutableCopy];
_lastBufferCachePurge = now;
}

// See if we have a buffer we can reuse
MetalBuffer *bestCandidate = nil;
for (MetalBuffer *candidate in self.bufferCache)
for (MetalBuffer *candidate in _bufferCache)
if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
bestCandidate = candidate;

if (bestCandidate != nil)
{
[self.bufferCache removeObject:bestCandidate];
[_bufferCache removeObject:bestCandidate];
bestCandidate.lastReuseTime = now;
return bestCandidate;
}
Expand All @@ -332,21 +506,21 @@ - (MetalBuffer *)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTL

- (void)enqueueReusableBuffer:(MetalBuffer *)buffer
{
[self.bufferCache addObject:buffer];
[_bufferCache addObject:buffer];
}

- (_Nullable id<MTLRenderPipelineState>)renderPipelineStateForFrameAndDevice:(id<MTLDevice>)device
{
// 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<MTLRenderPipelineState> renderPipelineState = self.renderPipelineStateCache[self.framebufferDescriptor];
id<MTLRenderPipelineState> renderPipelineState = _renderPipelineStateCache[_framebufferDescriptor];

if (renderPipelineState == nil)
{
// No luck; make a new render pipeline state
renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor:self.framebufferDescriptor device:device];
renderPipelineState = [self _renderPipelineStateForFramebufferDescriptor:_framebufferDescriptor device:device];
// Cache render pipeline state for later reuse
self.renderPipelineStateCache[self.framebufferDescriptor] = renderPipelineState;
_renderPipelineStateCache[_framebufferDescriptor] = renderPipelineState;
}

return renderPipelineState;
Expand Down
3 changes: 1 addition & 2 deletions backends/imgui_impl_osx.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
// [X] Platform: OSX clipboard is supported within core Dear ImGui (no specific code in this backend).
// [X] Platform: Gamepad support. Enabled with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'.
// [X] Platform: IME support.
// Issues:
// [ ] Platform: Multi-viewport / platform windows.
// [X] Platform: Multi-viewport / platform windows.

// 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.
Expand Down
Loading