mirror of
https://github.com/ocornut/imgui.git
synced 2026-09-27 00:13:29 +03:00
Merge a21461b2e5 into 3bae66c735
This commit is contained in:
commit
8b363bd3d1
3 changed files with 575 additions and 77 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue