mirror of
https://github.com/ocornut/imgui.git
synced 2026-11-08 23:45:46 +01:00
Backends + Examples: Metal4: robust CAMetalLayer multi-viewport on macOS
Rework the Metal 4 backend and example for correct, glitch-free multi-viewport rendering on macOS, and convert example_apple_metal4 off MTKView. Backend (imgui_impl_metal4.mm): - Per-window argument tables so overlapping per-window GPU work within a frame can't corrupt each other's bindings (multi-viewport flicker). - Hold per-slot vertex/index buffers until their frame completes, so a second viewport rendered later in the same frame can't overwrite a buffer still in flight (blink). - Make hand-built render-pass drawables resident, and evict stale (resized-away) drawables from the residency set so it can't grow without bound during a live resize. - Present secondary viewports in-transaction on the frame they were resized so content stays glued to the new size (no stutter). Example (example_apple_metal4/main.mm): - Replace MTKView with a custom NSView/UIView + CAMetalLayer driven by a display link. - Proper frames-in-flight: per-slot command allocators, rotating slots, and a semaphore gate released on GPU completion via an MTLSharedEvent listener. - Glitch-free main-window resize: present in-transaction during a live resize, driven by AppKit (the display link steps aside) so the transaction commits and drawables aren't starved; async present otherwise (avoids a ~1s nextDrawable stall / freeze). - Idle CPU throttling: only redraw on input/activity plus a slow heartbeat, so the main thread sleeps when nothing changes.
This commit is contained in:
parent
9d63b956f2
commit
6555153b21
@ -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()
|
||||
|
||||
@ -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,24 @@
|
||||
@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
|
||||
#endif
|
||||
@end
|
||||
|
||||
#define FRAMES_IN_FLIGHT 2
|
||||
|
||||
//-----------------------------------------------------------------------------------
|
||||
// AppViewController
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
@implementation AppViewController
|
||||
|
||||
-(instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
|
||||
@ -49,7 +210,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 +281,128 @@
|
||||
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;
|
||||
}];
|
||||
|
||||
// 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 +452,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 +483,46 @@
|
||||
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 (_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 +531,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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user