This commit is contained in:
Ahmidou Lyazidi 2026-09-25 21:33:48 +00:00 • committed by GitHub
commit 8b363bd3d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 575 additions and 77 deletions

View file

@ -81,19 +81,26 @@ struct ImGui_Metal4_ConstantData
@property (nonatomic, strong) id<MTLDevice> device;
@property (nonatomic, strong) id<MTL4CommandQueue> commandQueue;
@property (nonatomic, strong) id<MTLDepthStencilState> depthStencilState;
@property (nonatomic, strong) id<MTL4ArgumentTable> argumentTable;
// One argument table per frame-in-flight slot, shared by every window (main + secondary viewports) drawn
// in that frame. Metal snapshots an argument table's bindings when each draw is *encoded* (see
// -[MTL4RenderCommandEncoder setArgumentTable:atStages:]), so rebinding it for the next window doesn't
// disturb draws already encoded for the previous one. Per-slot (rather than one global table) keeps reuse
// safe across frames-in-flight.
@property (nonatomic, strong) NSMutableArray<id<MTL4ArgumentTable>>* argumentTables; // indexed [frameSlot]
@property (nonatomic, strong) id<MTL4ArgumentTable> currentArgumentTable; // table for the RenderDrawData call in progress
@property (nonatomic, strong) id<MTLSamplerState> samplerStateLinear;
@property (nonatomic, strong) id<MTLSamplerState> samplerStateNearest;
@property (nonatomic, strong) id<MTLResidencySet> residencySet;
@property (nonatomic, strong) NSMapTable<id<MTLTexture>, NSNumber*>* residentDrawables; // drawable render target -> frame index it was last used; lets us evict stale (resized-away) drawables from residencySet instead of leaking them
@property (nonatomic, assign) uint64_t residencyFrameCounter;
@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor;
@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache;
@property (nonatomic, assign) NSUInteger framesInFlight;
@property (nonatomic, assign) NSUInteger currentFrameSlot;
@property (nonatomic, strong) NSMutableArray<NSMutableArray<MetalBuffer*>*>* bufferCaches;
@property (nonatomic, strong) NSMutableArray<NSMutableArray<MetalBuffer*>*>* buffersInUse; // per-slot: buffers handed out this frame, held until the slot's GPU work completes (next NewFrame for that slot) so a second viewport in the same frame can't reuse (and overwrite) an in-flight buffer
@property (nonatomic, strong) NSObject* bufferCacheLock;
@property (nonatomic, assign) double lastBufferCachePurge;
@property (nonatomic, strong) NSArray<id<MTLSharedEvent>>* events; // for tracking when a commands are complete to reset allocator
@property (nonatomic) uint64_t eventValue;
@property (nonatomic, strong) NSArray<id<MTL4CommandAllocator>>* commandAllocators;
@property (nonatomic, strong) NSMutableArray<NSMutableArray<id<MTLBuffer>>*>* constantBuffers;
@property (nonatomic) uint64_t constantBufferChunkCount;
@ -147,6 +154,19 @@ bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device)
#pragma mark - Dear ImGui Metal Backend API
// Add a per-frame drawable render target to the residency set (Metal 4 requires it, since the render
// pass is now built by the app rather than MTKView), tracking the frame it was last used so stale
// drawables — e.g. the fresh-sized textures a CAMetalLayer produces every frame during a live resize —
// can be evicted instead of accumulating in the set forever.
static void ImGui_ImplMetal4_TrackResidentDrawable(MetalContext* ctx, id<MTLTexture> texture)
{
if (texture == nil)
return;
if ([ctx.residentDrawables objectForKey:texture] == nil)
[ctx.residencySet addAllocation:texture];
[ctx.residentDrawables setObject:@(ctx.residencyFrameCounter) forKey:texture];
}
void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex)
{
ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
@ -160,10 +180,41 @@ void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, i
bd->SharedMetalContext.currentFrameSlot = (NSUInteger)frameInFlightIndex;
if (bd->SharedMetalContext.depthStencilState == nil)
ImGui_ImplMetal4_CreateDeviceObjects(bd->SharedMetalContext.device);
// The render pass is now built by the application (no MTKView), so the render-target texture is not
// automatically resident and must be added to the residency set. Before adding this frame's drawable,
// evict any tracked drawable not used for >= framesInFlight+2 frames: the frames-in-flight gate
// guarantees its last frame has completed on the GPU and it won't be handed out again (during a live
// resize the layer creates a new texture every frame), so this stops the residency set from growing
// without bound. Pooled same-size drawables keep getting reused, so they are never evicted.
MetalContext* ctx = bd->SharedMetalContext;
ctx.residencyFrameCounter++;
NSMutableArray<id<MTLTexture>>* staleDrawables = nil;
for (id<MTLTexture> texture in ctx.residentDrawables)
if (ctx.residencyFrameCounter - [[ctx.residentDrawables objectForKey:texture] unsignedLongLongValue] >= ctx.framesInFlight + 2)
{
if (staleDrawables == nil)
staleDrawables = [NSMutableArray array];
[staleDrawables addObject:texture];
}
for (id<MTLTexture> texture in staleDrawables)
{
[ctx.residencySet removeAllocation:texture];
[ctx.residentDrawables removeObjectForKey:texture];
}
ImGui_ImplMetal4_TrackResidentDrawable(ctx, renderPassDescriptor.colorAttachments[0].texture);
bd->SharedMetalContext.currentConstantBufferIndex = 0;
[bd->SharedMetalContext.events[frameInFlightIndex] waitUntilSignaledValue:bd->SharedMetalContext.eventValue timeoutMS:UINT64_MAX];
[bd->SharedMetalContext.commandAllocators[frameInFlightIndex] reset];
// This slot's previous frame has completed on the GPU (the app gates on frames-in-flight before
// reusing a slot), so its held buffers are now free to reuse: release them back to the pool.
@synchronized(bd->SharedMetalContext.bufferCacheLock)
{
NSMutableArray<MetalBuffer*>* slotInUse = bd->SharedMetalContext.buffersInUse[frameInFlightIndex];
[bd->SharedMetalContext.bufferCaches[frameInFlightIndex] addObjectsFromArray:slotInUse];
[slotInUse removeAllObjects];
}
}
static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4CommandBuffer> commandBuffer,
@ -212,7 +263,12 @@ static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4Comm
memcpy(&constantBufferContents->ModelViewProjectionMatrix[currentIndex], ortho_projection, sizeof(ortho_projection));
id<MTL4ArgumentTable> argumentTable = bd->SharedMetalContext.argumentTable;
// One argument table per frame-in-flight slot, shared by every window in the frame. Metal snapshots
// an argument table's bindings when each draw is *encoded* (see -[MTL4RenderCommandEncoder
// setArgumentTable:atStages:]), so rebinding it for the next window doesn't disturb draws already
// encoded for the previous one. Per-slot (not a single global table) keeps it safe across frames-in-flight.
id<MTL4ArgumentTable> argumentTable = bd->SharedMetalContext.argumentTables[currentFrameIndex];
bd->SharedMetalContext.currentArgumentTable = argumentTable;
[argumentTable setAddress:constantBuffer.gpuAddress+(uint64_t)currentIndex * sizeof(constantBufferContents->ModelViewProjectionMatrix[0]) atIndex:1];
[argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset) attributeStride:sizeof(ImDrawVert) atIndex:0];
[argumentTable setSamplerState:bd->SharedMetalContext.samplerStateLinear.gpuResourceID atIndex:0];
@ -221,8 +277,8 @@ static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4Comm
}
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]; }
static void ImGui_ImplMetal4_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [bd->SharedMetalContext.currentArgumentTable 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.currentArgumentTable setSamplerState:bd->SharedMetalContext.samplerStateNearest.gpuResourceID atIndex:0]; }
void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id<MTL4CommandBuffer> commandBuffer, id<MTL4RenderCommandEncoder> commandEncoder)
{
@ -332,10 +388,10 @@ void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id<MTL4CommandBuffer
{
id<MTLTexture> texture = (__bridge id<MTLTexture>)(void*)(intptr_t)tex_id;
[bd->SharedMetalContext.residencySet addAllocation:texture];
[bd->SharedMetalContext.argumentTable setTexture:texture.gpuResourceID atIndex:0];
[bd->SharedMetalContext.currentArgumentTable setTexture:texture.gpuResourceID atIndex:0];
}
[bd->SharedMetalContext.argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset + (pcmd->VtxOffset * sizeof(ImDrawVert))) attributeStride:sizeof(ImDrawVert) atIndex:0];
[bd->SharedMetalContext.currentArgumentTable 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
@ -353,9 +409,12 @@ void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id<MTL4CommandBuffer
MetalContext* sharedMetalContext = bd->SharedMetalContext;
@synchronized(sharedMetalContext.bufferCacheLock)
{
NSMutableArray<MetalBuffer*>* slotCache = sharedMetalContext.bufferCaches[sharedMetalContext.currentFrameSlot];
[slotCache addObject:vertexBuffer];
[slotCache addObject:indexBuffer];
// Hold these until this slot's GPU work completes (released back to the available pool at the
// next NewFrame for this slot). Returning them to the available pool now would let a second
// viewport rendered later in THIS frame dequeue and overwrite them while still in flight.
NSMutableArray<MetalBuffer*>* slotInUse = sharedMetalContext.buffersInUse[sharedMetalContext.currentFrameSlot];
[slotInUse addObject:vertexBuffer];
[slotInUse addObject:indexBuffer];
}
// Commit residency set
@ -440,6 +499,8 @@ bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
IM_ASSERT(bd->SharedMetalContext.residencySet != nil && error == nil);
[bd->SharedMetalContext.commandQueue addResidencySet:bd->SharedMetalContext.residencySet];
bd->SharedMetalContext.residentDrawables = [NSMapTable strongToStrongObjectsMapTable];
bd->SharedMetalContext.residencyFrameCounter = 0;
MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init];
depthStencilDescriptor.depthWriteEnabled = NO;
@ -457,11 +518,9 @@ bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor];
NSMutableArray<id<MTL4CommandAllocator>>* commandAllocators = [NSMutableArray array];
NSMutableArray<id<MTLSharedEvent>>* events = [NSMutableArray array];
bd->SharedMetalContext.constantBuffers = [NSMutableArray array];
for (NSUInteger i = 0; i < bd->SharedMetalContext.framesInFlight; i++)
{
events[i] = [device newSharedEvent];
commandAllocators[i] = [device newCommandAllocator];
bd->SharedMetalContext.constantBuffers[i] = [NSMutableArray array];
id<MTLBuffer> buffer = [device newBufferWithLength:sizeof(ImGui_Metal4_ConstantData) options:MTLResourceStorageModeShared];
@ -469,7 +528,6 @@ bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
[bd->SharedMetalContext.residencySet addAllocation:buffer];
}
bd->SharedMetalContext.constantBufferChunkCount = 1;
bd->SharedMetalContext.events = events;
MTL4ArgumentTableDescriptor* argumentTableDescriptor = [[MTL4ArgumentTableDescriptor alloc] init];
argumentTableDescriptor.maxBufferBindCount = 2; // vertex buffer + constant buffer
@ -479,8 +537,16 @@ bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
bd->SharedMetalContext.commandAllocators = commandAllocators;
bd->SharedMetalContext.argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error];
IM_ASSERT(bd->SharedMetalContext.argumentTable != nil && error == nil);
// One argument table per frame-in-flight slot, reused by every window in that slot's frame (bindings
// are snapshotted per draw at encode time). Pre-create them so no argument table is created mid-frame.
NSMutableArray<id<MTL4ArgumentTable>>* argumentTables = [NSMutableArray array];
for (NSUInteger i = 0; i < bd->SharedMetalContext.framesInFlight; i++)
{
id<MTL4ArgumentTable> argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error];
IM_ASSERT(argumentTable != nil && error == nil);
[argumentTables addObject:argumentTable];
}
bd->SharedMetalContext.argumentTables = argumentTables;
ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
return true;
@ -526,9 +592,14 @@ bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQue
bd->SharedMetalContext.commandQueue = commandQueue;
bd->SharedMetalContext.framesInFlight = (NSUInteger)framesInFlight;
NSMutableArray<NSMutableArray<MetalBuffer*>*>* bufferCaches = [NSMutableArray array];
NSMutableArray<NSMutableArray<MetalBuffer*>*>* buffersInUse = [NSMutableArray array];
for (NSUInteger i = 0; i < framesInFlight; i++)
{
[bufferCaches addObject:[NSMutableArray array]];
[buffersInUse addObject:[NSMutableArray array]];
}
bd->SharedMetalContext.bufferCaches = bufferCaches;
bd->SharedMetalContext.buffersInUse = buffersInUse;
ImGui_ImplMetal_InitMultiViewportSupport();
return true;
@ -803,6 +874,7 @@ struct ImGuiViewportDataMetal
MTL4RenderPassDescriptor* RenderPassDescriptor;
void* Handle = nullptr;
bool FirstFrame = true;
bool PendingResizePresent = false;
};
static void ImGui_ImplMetal_CreateWindow(ImGuiViewport* viewport)
@ -850,6 +922,9 @@ static void ImGui_ImplMetal_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
{
ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
data->MetalLayer.drawableSize = MakeScaledSize(CGSizeMake(size.x, size.y), data->MetalLayer.contentsScale);
// The window frame was just resized; ask the next RenderWindow to present in-transaction so the
// drawable lands together with the new frame (no stale/stretched content while dragging).
data->PendingResizePresent = true;
}
static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
@ -875,6 +950,13 @@ static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
CGSize want_size = MakeScaledSize(window.contentView.bounds.size, fb_scale);
if (!CGSizeEqualToSize(data->MetalLayer.drawableSize, want_size))
data->MetalLayer.drawableSize = want_size;
// On frames where the window was just resized (flagged by ImGui_ImplMetal_SetWindowSize), present
// in-transaction so content stays glued to the new size; otherwise present asynchronously to avoid a
// per-frame stall. Unlike the main window this is safe here because secondary viewports are rendered
// inside the main frame's transaction, which commits (releasing the drawable) each frame.
bool resizing = data->PendingResizePresent;
data->PendingResizePresent = false;
data->MetalLayer.presentsWithTransaction = resizing;
#endif
id <CAMetalDrawable> drawable = [data->MetalLayer nextDrawable];
@ -888,6 +970,9 @@ static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
// The secondary-viewport render pass is hand-built here too, so make its render-target texture
// resident (tracked so it can be evicted once stale) before RenderDrawData commits the residency set.
ImGui_ImplMetal4_TrackResidentDrawable(bd->SharedMetalContext, drawable.texture);
id <MTL4CommandBuffer> commandBuffer = [bd->SharedMetalContext.device newCommandBuffer];
[commandBuffer beginCommandBufferWithAllocator:bd->SharedMetalContext.commandAllocators[bd->SharedMetalContext.currentFrameSlot]];
@ -900,7 +985,6 @@ static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
[bd->SharedMetalContext.commandQueue commit:&commandBuffer count:1];
[bd->SharedMetalContext.commandQueue signalDrawable:drawable];
[drawable present];
[bd->SharedMetalContext.commandQueue signalEvent:bd->SharedMetalContext.events[bd->SharedMetalContext.currentFrameSlot] value:++(bd->SharedMetalContext.eventValue)];
}
static void ImGui_ImplMetal_InitMultiViewportSupport()

View file

@ -267,8 +267,11 @@ static bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view);
@end
static void ImGui_ImplOSX_RestackSecondaryWindows(bool active); // keep torn-off windows above the main window (see definition)
@interface ImGuiObserver : NSObject
- (void)onApplicationWillBecomeActive:(NSNotification*)aNotification;
- (void)onApplicationBecomeActive:(NSNotification*)aNotification;
- (void)onApplicationBecomeInactive:(NSNotification*)aNotification;
- (void)displaysDidChange:(NSNotification*)aNotification;
@ -277,16 +280,37 @@ static bool ImGui_ImplOSX_HandleEvent(NSEvent* event, NSView* view);
@implementation ImGuiObserver
- (void)onApplicationWillBecomeActive:(NSNotification*)aNotification
{
// Restack on WILL (before the app is actually activated) as well as DID: clicking the main window to
// reactivate brings it to the front of the normal level, and doing the restack only on DID leaves the
// main window on top of the floating windows for one frame before they are raised. Doing it here,
// before activation completes, avoids that flash. (DID still runs below to cover paths that don't post
// a WILL notification and to re-assert once the app is truly frontmost.)
if (ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData())
[bd->Window orderFront:nil];
ImGui_ImplOSX_RestackSecondaryWindows(true);
}
- (void)onApplicationBecomeActive:(NSNotification*)aNotification
{
ImGuiIO& io = ImGui::GetIO();
io.AddFocusEvent(true);
// App is frontmost again. Clicking a floating window activates the app but doesn't itself bring the
// main window forward, so bring it front here (it stays below the floating windows, which are raised
// back above it just below), so the whole app comes forward as a unit above the previously-front app.
if (ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData())
[bd->Window orderFront:nil];
ImGui_ImplOSX_RestackSecondaryWindows(true);
}
- (void)onApplicationBecomeInactive:(NSNotification*)aNotification
{
ImGuiIO& io = ImGui::GetIO();
io.AddFocusEvent(false);
// Another app took focus: drop floating windows to the normal level so its windows occlude them
// (instead of the floating windows staying on top of every other app).
ImGui_ImplOSX_RestackSecondaryWindows(false);
}
- (void)displaysDidChange:(NSNotification*)aNotification
@ -500,6 +524,10 @@ bool ImGui_ImplOSX_Init(NSView* view)
return s_clipboard.Data;
};
[[NSNotificationCenter defaultCenter] addObserver:bd->Observer
selector:@selector(onApplicationWillBecomeActive:)
name:NSApplicationWillBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:bd->Observer
selector:@selector(onApplicationBecomeActive:)
name:NSApplicationDidBecomeActiveNotification
@ -903,6 +931,48 @@ struct ImGui_ImplOSX_ViewportData
@end
// Re-stack all torn-off (secondary) viewport windows above the main window, keeping their relative order.
// While the app is ACTIVE they are independent windows at NSFloatingWindowLevel: above the main window,
// but NOT children — so dragging the main window does not drag them along. While the app is INACTIVE they
// become children of the main window at the normal level: children are occluded together with the app by
// other apps, and — crucially — the window server keeps a child above its parent at ALL times, so a click
// on the main window to reactivate can't flash it over them for a frame (which a same-level sibling would).
// Only visible windows are touched (ordering an ordered-out/destroyed window would resurrect it as an empty
// floater). The main window is excluded (it is not an ImGui_ImplOSX_Window).
static void ImGui_ImplOSX_RestackSecondaryWindows(bool active)
{
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
if (bd == nullptr)
return;
NSMutableArray<NSWindow*>* windows = [NSMutableArray array];
for (NSWindow* window in NSApp.orderedWindows) // front-to-back
if (window != bd->Window && window.isVisible && [window isKindOfClass:[ImGui_ImplOSX_Window class]])
[windows addObject:window];
if (active)
{
for (NSWindow* window in windows)
{
if (window.parentWindow != nil)
[window.parentWindow removeChildWindow:window];
window.level = NSFloatingWindowLevel;
}
// Front-to-back: each ordered directly above the main window, so the front-most stays on top.
for (NSWindow* window in windows)
[window orderWindow:NSWindowAbove relativeTo:bd->Window.windowNumber];
}
else
{
for (NSWindow* window in windows)
window.level = NSNormalWindowLevel;
// Back-to-front: each child added above the previous, so the front-most ends up the topmost child.
for (NSWindow* window in windows.reverseObjectEnumerator)
if (window.parentWindow != bd->Window)
[bd->Window addChildWindow:window ordered:NSWindowAbove];
}
}
static void ConvertNSRect(NSRect* r)
{
NSRect firstScreenFrame = NSScreen.screens[0].frame;
@ -929,14 +999,24 @@ static void ImGui_ImplOSX_CreateWindow(ImGuiViewport* viewport)
NSWindow* window = [[ImGui_ImplOSX_Window alloc] initWithContentRect:rect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:YES
defer:NO
screen:screen];
if (viewport->Flags & ImGuiViewportFlags_TopMost)
[window setLevel:NSFloatingWindowLevel];
window.title = @"Untitled";
window.opaque = YES;
// Disable the macOS fade/zoom "appear" animation (a pop on every tear-off). New windows are torn off
// while the app is active, so start them at NSFloatingWindowLevel (above the main window, and winning
// the z-order race during the tear-off drag). ImGui_ImplOSX_RestackSecondaryWindows then manages
// level/parenting as the app gains/loses focus.
[window setAnimationBehavior:NSWindowAnimationBehaviorNone];
window.level = NSFloatingWindowLevel;
// Parent the new window to the main window for the tear-off: the window server then composites it in the
// SAME pass as its parent so it appears immediately. A fresh top-level window otherwise reports
// occlusionState=NotVisible for one compositor pass and the torn-off window blinks out for a frame.
// ImGui_ImplOSX_UpdateWindow detaches it into an independent floating window once the drag is released.
//if (bd->Window != nil)
[bd->Window addChildWindow:window ordered:NSWindowAbove];
KeyEventResponder* view = [[KeyEventResponder alloc] initWithFrame:rect];
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_6 && ceil(NSAppKitVersionNumber) < NSAppKitVersionNumber10_15)
{
@ -962,6 +1042,8 @@ static void ImGui_ImplOSX_DestroyWindow(ImGuiViewport* viewport)
NSWindow* window = vd->Window;
if (window != nil && vd->WindowOwned)
{
if (window.parentWindow != nil)
[window.parentWindow removeChildWindow:window]; // detach if parented (inactive state) before hiding
window.contentView = nil;
window.contentViewController = nil;
[window orderOut:nil];
@ -985,6 +1067,29 @@ static void ImGui_ImplOSX_ShowWindow(ImGuiViewport* viewport)
[vd->Window setIsVisible:YES];
}
static void ImGui_ImplOSX_UpdateWindow(ImGuiViewport* viewport)
{
ImGui_ImplOSX_Data* bd = ImGui_ImplOSX_GetBackendData();
ImGui_ImplOSX_ViewportData* vd = (ImGui_ImplOSX_ViewportData*)viewport->PlatformUserData;
if (vd == nullptr || vd->Window == nil || bd->Window == nil) {
return;
}
// The new window is created as a child of the main window so it composites without a one-frame blink (see
// ImGui_ImplOSX_CreateWindow). Detach it into an independent floating window once (a) it is actually
// on-screen (occlusionState visible) and (b) the tear-off drag is released (no mouse button held), while
// the app is active. The occlusion check matters because Platform_UpdateWindow runs every frame INCLUDING
// the creation frame and BEFORE Platform_ShowWindow, so detaching earlier would undo the parenting before
// the window is ever composited and bring the blink back. While inactive it stays a child (see
// ImGui_ImplOSX_RestackSecondaryWindows).
if (vd->Window.parentWindow == bd->Window && NSApp.active && [NSEvent pressedMouseButtons] == 0
&& (vd->Window.occlusionState & NSWindowOcclusionStateVisible) != 0) {
[bd->Window removeChildWindow:vd->Window];
vd->Window.level = NSFloatingWindowLevel;
[vd->Window orderWindow:NSWindowAbove relativeTo:bd->Window.windowNumber];
}
}
static ImVec2 ImGui_ImplOSX_GetWindowPos(ImGuiViewport* viewport)
{
ImGui_ImplOSX_ViewportData* vd = (ImGui_ImplOSX_ViewportData*)viewport->PlatformUserData;
@ -1121,6 +1226,7 @@ static void ImGui_ImplOSX_InitMultiViewportSupport()
platform_io.Platform_CreateWindow = ImGui_ImplOSX_CreateWindow;
platform_io.Platform_DestroyWindow = ImGui_ImplOSX_DestroyWindow;
platform_io.Platform_ShowWindow = ImGui_ImplOSX_ShowWindow;
platform_io.Platform_UpdateWindow = ImGui_ImplOSX_UpdateWindow;
platform_io.Platform_SetWindowPos = ImGui_ImplOSX_SetWindowPos;
platform_io.Platform_GetWindowPos = ImGui_ImplOSX_GetWindowPos;
platform_io.Platform_SetWindowSize = ImGui_ImplOSX_SetWindowSize;

View file

@ -1,4 +1,6 @@
// Dear ImGui: standalone example application for OSX + Metal.
// Dear ImGui: standalone example application for OSX + Metal 4.
// Uses CAMetalLayer directly (instead of MTKView) for glitch-free resizing.
// Based on Tristan Hume's MetalLayerView pattern with presentsWithTransaction.
// Learn about Dear ImGui:
// - FAQ https://dearimgui.com/faq
@ -10,17 +12,171 @@
#if TARGET_OS_OSX
#import <Cocoa/Cocoa.h>
#import <CoreVideo/CoreVideo.h>
#else
#import <UIKit/UIKit.h>
#endif
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <QuartzCore/CAMetalLayer.h>
#include "imgui.h"
#include "imgui_impl_metal4.h"
#if TARGET_OS_OSX
#include "imgui_impl_osx.h"
#endif
//-----------------------------------------------------------------------------------
// MetalLayerView — custom view backed by CAMetalLayer
//-----------------------------------------------------------------------------------
// Render callback type: the view calls this to draw a frame
typedef void (^RenderCallback)(CAMetalLayer* layer, CGSize viewSize, CGFloat scaleFactor);
// Idle CPU throttling: after the last user input (or while a widget is active) keep rendering at the full
// display rate for this many frames, then fall back to a slow heartbeat so the main thread can idle.
static const NSInteger kActiveRenderFrames = 30;
#if TARGET_OS_OSX
@interface MetalLayerView : NSView <CALayerDelegate>
@property (nonatomic, strong) CAMetalLayer* metalLayer;
@property (nonatomic, copy) RenderCallback renderCallback;
@property (atomic, assign) NSInteger activeFrames; // >0 => keep driving redraws; read on the display-link thread
@end
@implementation MetalLayerView
-(instancetype)initWithFrame:(NSRect)frame device:(id<MTLDevice>)device
{
self = [super initWithFrame:frame];
if (self)
{
self.wantsLayer = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
self.layerContentsPlacement = NSViewLayerContentsPlacementScaleAxesIndependently;
_metalLayer = (CAMetalLayer*)self.layer;
_metalLayer.device = device;
_metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
_metalLayer.delegate = self;
_metalLayer.allowsNextDrawableTimeout = NO;
_metalLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
_metalLayer.needsDisplayOnBoundsChange = YES;
// Default to async presentation. presentsWithTransaction is turned on only during a live resize
// (in renderWithLayer:) so content stays glued to the window frame; see the note there and in
// displayLinkCallback for why it must stay off otherwise.
_metalLayer.presentsWithTransaction = NO;
_activeFrames = kActiveRenderFrames; // render at startup until things settle
}
return self;
}
-(CALayer*)makeBackingLayer
{
CAMetalLayer* layer = [CAMetalLayer layer];
return layer;
}
-(BOOL)wantsUpdateLayer { return YES; }
-(void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
_metalLayer.drawableSize = [self convertSizeToBacking:newSize];
}
-(void)viewDidChangeBackingProperties
{
[super viewDidChangeBackingProperties];
if (self.window)
{
_metalLayer.contentsScale = self.window.backingScaleFactor;
// Moving to a display with a different backing scale changes the pixel size without changing the
// point size, so setFrameSize: is not called — update the drawable resolution here too.
_metalLayer.drawableSize = [self convertSizeToBacking:self.bounds.size];
}
}
-(void)displayLayer:(CALayer*)layer
{
if (_renderCallback)
{
CGFloat scale = self.window.backingScaleFactor ?: NSScreen.mainScreen.backingScaleFactor;
_renderCallback(_metalLayer, self.bounds.size, scale);
}
}
@end
#else // iOS
@interface MetalLayerView : UIView
@property (nonatomic, strong) CAMetalLayer* metalLayer;
@property (nonatomic, copy) RenderCallback renderCallback;
@property (nonatomic, strong) CADisplayLink* displayLink;
@end
@implementation MetalLayerView
+(Class)layerClass
{
return [CAMetalLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame device:(id<MTLDevice>)device
{
self = [super initWithFrame:frame];
if (self)
{
_metalLayer = (CAMetalLayer*)self.layer;
_metalLayer.device = device;
_metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
_metalLayer.framebufferOnly = YES;
self.contentScaleFactor = UIScreen.mainScreen.scale;
_metalLayer.drawableSize = CGSizeMake(frame.size.width * self.contentScaleFactor,
frame.size.height * self.contentScaleFactor);
}
return self;
}
-(void)layoutSubviews
{
[super layoutSubviews];
_metalLayer.drawableSize = CGSizeMake(self.bounds.size.width * self.contentScaleFactor,
self.bounds.size.height * self.contentScaleFactor);
}
-(void)startDisplayLink
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFired:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
-(void)stopDisplayLink
{
[_displayLink invalidate];
_displayLink = nil;
}
-(void)displayLinkFired:(CADisplayLink*)link
{
if (_renderCallback)
{
CGFloat scale = self.contentScaleFactor;
_renderCallback(_metalLayer, self.bounds.size, scale);
}
}
@end
#endif
//-----------------------------------------------------------------------------------
// AppViewController
//-----------------------------------------------------------------------------------
#if TARGET_OS_OSX
@interface AppViewController : NSViewController<NSWindowDelegate>
@end
#else
@ -28,19 +184,25 @@
@end
#endif
@interface AppViewController () <MTKViewDelegate>
@property (nonatomic, readonly) MTKView *mtkView;
@property (nonatomic, strong) id <MTLDevice> device;
@property (nonatomic, strong) id <MTL4CommandQueue> commandQueue;
@property (nonatomic, strong) id <MTL4CommandAllocator> commandAllocator;
@interface AppViewController ()
@property (nonatomic, strong) MetalLayerView* metalView;
@property (nonatomic, strong) id<MTLDevice> device;
@property (nonatomic, strong) id<MTL4CommandQueue> commandQueue;
@property (nonatomic, strong) NSArray<id<MTL4CommandAllocator>>* commandAllocators;
@property (nonatomic, strong) id<MTLSharedEvent> frameEvent;
@property (nonatomic, assign) uint64_t frameValue;
@property (nonatomic, strong) dispatch_semaphore_t inFlightSemaphore;
@property (nonatomic, strong) MTLSharedEventListener* frameListener;
@property (nonatomic, assign) uint64_t frameCount;
#if TARGET_OS_OSX
@property (nonatomic, assign) CVDisplayLinkRef displayLink;
@property (nonatomic, strong) id eventMonitor; // local NSEvent monitor that keeps rendering active on input
@property (nonatomic, strong) id becomeActiveObserver; // resumes rendering when the app is reactivated (Cmd-Tab)
#endif
@end
#define FRAMES_IN_FLIGHT 2
//-----------------------------------------------------------------------------------
// AppViewController
//-----------------------------------------------------------------------------------
@implementation AppViewController
-(instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
@ -49,7 +211,24 @@
_device = MTLCreateSystemDefaultDevice();
_commandQueue = [_device newMTL4CommandQueue];
_commandAllocator = [_device newCommandAllocator];
// One command allocator per frame-in-flight slot so a slot's allocator is only reset/reused once
// its previous frame's GPU work has completed (gated by the semaphore below).
NSMutableArray<id<MTL4CommandAllocator>>* commandAllocators = [NSMutableArray array];
for (NSUInteger i = 0; i < FRAMES_IN_FLIGHT; i++)
[commandAllocators addObject:[_device newCommandAllocator]];
_commandAllocators = commandAllocators;
_frameEvent = [_device newSharedEvent];
_frameValue = 0;
_frameCount = 0;
// Frames-in-flight gate: never let the CPU get more than FRAMES_IN_FLIGHT frames ahead of the GPU,
// so per-slot resources (command allocators, constant buffers, argument tables) are safely double
// buffered. The listener signals the semaphore from a serial queue when a frame's GPU work finishes.
dispatch_queue_t listenerQueue = dispatch_queue_create("imgui.metal4.frameListener", DISPATCH_QUEUE_SERIAL);
_frameListener = [[MTLSharedEventListener alloc] initWithDispatchQueue:listenerQueue];
_inFlightSemaphore = dispatch_semaphore_create(FRAMES_IN_FLIGHT);
if (!self.device)
{
@ -103,62 +282,137 @@
return self;
}
-(MTKView *)mtkView
{
return (MTKView *)self.view;
}
-(void)loadView
{
self.view = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, 1200, 800)];
#if TARGET_OS_OSX
self.metalView = [[MetalLayerView alloc] initWithFrame:CGRectMake(0, 0, 1200, 800) device:self.device];
#else
self.metalView = [[MetalLayerView alloc] initWithFrame:UIScreen.mainScreen.bounds device:self.device];
#endif
self.view = self.metalView;
}
-(void)viewDidLoad
{
[super viewDidLoad];
self.mtkView.device = self.device;
self.mtkView.delegate = self;
#if TARGET_OS_OSX
ImGui_ImplOSX_Init(self.view);
[NSApp activateIgnoringOtherApps:YES];
#endif
}
-(void)drawInMTKView:(MTKView*)view
{
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize.x = view.bounds.size.width;
io.DisplaySize.y = view.bounds.size.height;
// Setup render callback — captures self weakly to avoid retain cycle
__weak AppViewController* weakSelf = self;
self.metalView.renderCallback = ^(CAMetalLayer* layer, CGSize viewSize, CGFloat scaleFactor) {
[weakSelf renderWithLayer:layer viewSize:viewSize scaleFactor:scaleFactor];
};
#if TARGET_OS_OSX
CGFloat framebufferScale = view.window.screen.backingScaleFactor ?: NSScreen.mainScreen.backingScaleFactor;
// Keep rendering at the full rate for a short grace period after any user input, so hover/typing/drag
// stay responsive; the display-link callback throttles once this decays. (See kActiveRenderFrames.)
NSEventMask inputMask = NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | NSEventMaskRightMouseDown |
NSEventMaskRightMouseUp | NSEventMaskOtherMouseDown | NSEventMaskOtherMouseUp |
NSEventMaskMouseMoved | NSEventMaskLeftMouseDragged | NSEventMaskRightMouseDragged |
NSEventMaskOtherMouseDragged | NSEventMaskScrollWheel | NSEventMaskKeyDown |
NSEventMaskKeyUp | NSEventMaskFlagsChanged;
__weak MetalLayerView* weakView = self.metalView;
self.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:inputMask handler:^NSEvent* (NSEvent* event) {
weakView.activeFrames = kActiveRenderFrames;
return event;
}];
// Reactivating the app (Cmd-Tab, or clicking a window) doesn't produce a local input event, so bump the
// render counter on activation too — otherwise the throttle leaves the window on the slow heartbeat for
// up to ~1s after switching back, which looks like a lag.
self.becomeActiveObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSApplicationDidBecomeActiveNotification
object:nil
queue:NSOperationQueue.mainQueue
usingBlock:^(NSNotification*) {
weakView.activeFrames = kActiveRenderFrames;
}];
// Start CVDisplayLink for continuous rendering
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
CVDisplayLinkSetOutputCallback(_displayLink, &displayLinkCallback, (__bridge void*)self.metalView);
CVDisplayLinkStart(_displayLink);
#else
CGFloat framebufferScale = view.window.screen.scale ?: UIScreen.mainScreen.scale;
[self.metalView startDisplayLink];
#endif
io.DisplayFramebufferScale = ImVec2(framebufferScale, framebufferScale);
[self.commandAllocator reset];
}
id<MTL4CommandBuffer> commandBuffer = [self.device newCommandBuffer];
[commandBuffer beginCommandBufferWithAllocator:self.commandAllocator];
#if TARGET_OS_OSX
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink,
const CVTimeStamp* now,
const CVTimeStamp* outputTime,
CVOptionFlags flagsIn,
CVOptionFlags* flagsOut,
void* context)
{
MetalLayerView* view = (__bridge MetalLayerView*)context;
// Idle throttle: only drive a redraw when the UI was recently active (view.activeFrames, bumped by
// input and by active widgets), or on a slow heartbeat so time-based animations still advance and we
// periodically re-check for activity. When idle we don't even wake the main thread. (activeFrames is
// an atomic property; this runs on the display-link thread.)
static uint64_t tick = 0;
tick++;
if (view.activeFrames <= 0 && (tick % 60) != 0)
return kCVReturnSuccess;
MTL4RenderPassDescriptor* renderPassDescriptor = view.currentMTL4RenderPassDescriptor;
if (renderPassDescriptor == nil)
dispatch_async(dispatch_get_main_queue(), ^{
// While the OS is live-resizing the window, AppKit already redraws the layer each step inside its
// resize CATransaction (which commits, releasing the in-transaction drawable). Driving an extra
// render from the display link here would present in-transaction WITHOUT a commit and starve the
// drawable pool, so skip it during resize and let AppKit be the sole driver.
if (!view.inLiveResize)
[view setNeedsDisplay:YES];
});
return kCVReturnSuccess;
}
#endif
-(void)renderWithLayer:(CAMetalLayer*)layer viewSize:(CGSize)viewSize scaleFactor:(CGFloat)scaleFactor
{
// Frames-in-flight gate: block until a slot is free (its previous frame's GPU work has completed).
// Once past this, the slot's per-frame resources are guaranteed no longer read by the GPU.
dispatch_semaphore_wait(self.inFlightSemaphore, DISPATCH_TIME_FOREVER);
NSUInteger slot = self.frameCount % FRAMES_IN_FLIGHT;
self.frameCount++;
ImGuiIO& io = ImGui::GetIO();
io.DisplaySize.x = viewSize.width;
io.DisplaySize.y = viewSize.height;
io.DisplayFramebufferScale = ImVec2(scaleFactor, scaleFactor);
#if TARGET_OS_OSX
// While the OS is live-resizing the window, present in-transaction so the content stays glued to the
// window frame (glitch-free resize). This is safe because during a live resize AppKit drives the
// redraw inside its resize transaction — which commits and releases the drawable — while the display
// link stands down (see displayLinkCallback). Async otherwise, to avoid a per-frame stall.
layer.presentsWithTransaction = self.metalView.inLiveResize;
#endif
id<CAMetalDrawable> drawable = [layer nextDrawable];
if (!drawable)
{
[commandBuffer endCommandBuffer];
[self.commandQueue commit:&commandBuffer count:1];
// Release the gate we just took, otherwise the semaphore count leaks and we deadlock.
dispatch_semaphore_signal(self.inFlightSemaphore);
return;
}
[self.commandAllocators[slot] reset];
id<MTL4CommandBuffer> commandBuffer = [self.device newCommandBuffer];
[commandBuffer beginCommandBufferWithAllocator:self.commandAllocators[slot]];
// Build the Metal 4 render pass descriptor by hand (MTKView's currentMTL4RenderPassDescriptor is gone).
MTL4RenderPassDescriptor* renderPassDescriptor = [[MTL4RenderPassDescriptor alloc] init];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Start the Dear ImGui frame
static int frameIndex = 0;
ImGui_ImplMetal4_NewFrame(renderPassDescriptor, frameIndex);
frameIndex++;
frameIndex = (frameIndex + 1) % FRAMES_IN_FLIGHT;
ImGui_ImplMetal4_NewFrame(renderPassDescriptor, (int)slot);
#if TARGET_OS_OSX
ImGui_ImplOSX_NewFrame(view);
ImGui_ImplOSX_NewFrame(self.view);
#endif
ImGui::NewFrame();
@ -208,19 +462,30 @@
ImGui::Render();
ImDrawData* draw_data = ImGui::GetDrawData();
#if TARGET_OS_OSX
// Idle throttle bookkeeping: keep rendering while the UI is doing something (text-cursor blink, an
// active/dragged widget, a held mouse button, or a live resize); otherwise let the counter decay so
// the display-link callback can drop to the idle heartbeat. Mouse movement is handled by the input
// monitor in viewDidLoad.
if (io.WantTextInput || ImGui::IsAnyItemActive() || [NSEvent pressedMouseButtons] != 0 || self.metalView.inLiveResize)
self.metalView.activeFrames = kActiveRenderFrames;
else if (self.metalView.activeFrames > 0)
self.metalView.activeFrames = self.metalView.activeFrames - 1;
#endif
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w);
id <MTL4RenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
[renderEncoder pushDebugGroup:@"Dear ImGui rendering"];
ImGui_ImplMetal4_RenderDrawData(draw_data, commandBuffer, renderEncoder);
[renderEncoder popDebugGroup];
[renderEncoder endEncoding];
// Present
[commandBuffer endCommandBuffer];
[self.commandQueue waitForDrawable:view.currentDrawable];
// Present (canonical Metal 4 sequence, as the original MTKView example used).
[self.commandQueue waitForDrawable:drawable];
[self.commandQueue commit:&commandBuffer count:1];
[self.commandQueue signalDrawable:view.currentDrawable];
[view.currentDrawable present];
[self.commandQueue signalDrawable:drawable];
[drawable present];
// Update and Render additional Platform Windows
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
@ -228,10 +493,51 @@
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
// Release the frames-in-flight gate once all of this frame's GPU work (main + secondary viewports)
// completes: the listener signals the semaphore when the shared event reaches this frame's value.
// Capture the semaphore itself — not self — so the completion block does not create a retain cycle.
uint64_t v = ++self.frameValue;
[self.commandQueue signalEvent:self.frameEvent value:v];
dispatch_semaphore_t sema = self.inFlightSemaphore;
[self.frameEvent notifyListener:self.frameListener atValue:v block:^(id<MTLSharedEvent>, uint64_t){
dispatch_semaphore_signal(sema);
}];
}
-(void)mtkView:(MTKView*)view drawableSizeWillChange:(CGSize)size
-(void)shutdown
{
#if TARGET_OS_OSX
if (_eventMonitor)
{
[NSEvent removeMonitor:_eventMonitor];
_eventMonitor = nil;
}
if (_becomeActiveObserver)
{
[NSNotificationCenter.defaultCenter removeObserver:_becomeActiveObserver];
_becomeActiveObserver = nil;
}
if (_displayLink)
{
CVDisplayLinkStop(_displayLink);
CVDisplayLinkRelease(_displayLink);
_displayLink = NULL;
}
#else
[self.metalView stopDisplayLink];
#endif
ImGui_ImplMetal4_Shutdown();
#if TARGET_OS_OSX
ImGui_ImplOSX_Shutdown();
#endif
ImGui::DestroyContext();
}
-(void)dealloc
{
[self shutdown];
}
//-----------------------------------------------------------------------------------
@ -240,17 +546,19 @@
#if TARGET_OS_OSX
- (void)viewWillAppear
-(void)viewWillAppear
{
[super viewWillAppear];
self.view.window.delegate = self;
// Deliver mouse-moved events so the input monitor can keep rendering active while hovering.
self.view.window.acceptsMouseMovedEvents = YES;
}
- (void)windowWillClose:(NSNotification *)notification
-(void)windowWillClose:(NSNotification *)notification
{
ImGui_ImplMetal4_Shutdown();
ImGui_ImplOSX_Shutdown();
ImGui::DestroyContext();
// Closing the main window terminates the app (matching the GLFW example), so any torn-off
// viewport windows are torn down with it instead of being left behind as orphan windows.
[NSApp terminate:nil];
}
#else