Backends: Metal4: added multi-viewport support (#9465, #9451)

This commit is contained in:
Jonathan Hoffstadt
2026-07-08 10:04:54 -05:00
committed by ocornut
parent a23e9fb1b5
commit df71e28084
3 changed files with 229 additions and 34 deletions

View File

@@ -6,9 +6,9 @@
// [X] Renderer: User texture binding. Use 'MTLTexture.gpuResourceID' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Multi-viewport support (multiple windows).
// Missing features or Issues:
// [ ] Texture view pool support? Reevaluate which type to use for ImtextureID.
// [ ] Renderer: Multi-viewport support (multiple windows).
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.

View File

@@ -6,9 +6,10 @@
// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset).
// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures).
// [X] Renderer: Multi-viewport support (multiple windows).
// Missing features or Issues:
// [ ] Texture view pool support? Reevaluate which type to use for ImtextureID.
// [ ] Renderer: Multi-viewport support (multiple windows).
// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this.
// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need.
@@ -33,11 +34,21 @@
#include <Metal/Metal.hpp>
#endif
// Forward Declarations
static void ImGui_ImplMetal_InitMultiViewportSupport();
static void ImGui_ImplMetal_ShutdownMultiViewportSupport();
static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();
#pragma mark - Support classes and structs
#define METAL_IMGUI_VIEWPORTS_PER_CHUNK 64
struct ImGui_Metal4_ConstantData
{
float ModelViewProjectionMatrix[4][4];
// 1 projection matrix is encoded as 4x4 float.
// Store up to 64 of them in same constant buffer, so multiple viewports can use same constant buffer.
float ModelViewProjectionMatrix[METAL_IMGUI_VIEWPORTS_PER_CHUNK][4][4];
};
@interface MetalBuffer : NSObject
@@ -76,13 +87,15 @@ struct ImGui_Metal4_ConstantData
@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache;
@property (nonatomic, assign) NSUInteger framesInFlight;
@property (nonatomic, assign) NSUInteger currentFrameSlot;
@property (nonatomic, strong) NSArray<id<MTLBuffer>>* constantBuffers;
@property (nonatomic, assign) ImGui_Metal4_ConstantData** constantBufferContentsArray;
@property (nonatomic, strong) NSMutableArray<NSMutableArray<MetalBuffer*>*>* bufferCaches;
@property (nonatomic, strong) NSObject* bufferCacheLock;
@property (nonatomic, assign) double lastBufferCachePurge;
- (id<MTLBuffer>)currentConstantBuffer;
- (ImGui_Metal4_ConstantData*)currentConstantBufferContents;
@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;
@property (nonatomic) uint64_t currentConstantBufferIndex;
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
- (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device;
@end
@@ -145,6 +158,10 @@ void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, i
bd->SharedMetalContext.currentFrameSlot = (NSUInteger)frameInFlightIndex;
if (bd->SharedMetalContext.depthStencilState == nil)
ImGui_ImplMetal4_CreateDeviceObjects(bd->SharedMetalContext.device);
bd->SharedMetalContext.currentConstantBufferIndex = 0;
[bd->SharedMetalContext.events[frameInFlightIndex] waitUntilSignaledValue:bd->SharedMetalContext.eventValue timeoutMS:UINT64_MAX];
[bd->SharedMetalContext.commandAllocators[frameInFlightIndex] reset];
}
static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4CommandBuffer> commandBuffer,
@@ -183,11 +200,18 @@ static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4Comm
{ 0.0f, 0.0f, 1/(F-N), 0.0f },
{ (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f },
};
ImGui_Metal4_ConstantData* constantBufferContents = [bd->SharedMetalContext currentConstantBufferContents];
memcpy(constantBufferContents->ModelViewProjectionMatrix, ortho_projection, sizeof(ortho_projection));
int currentChunk = (int)((float)bd->SharedMetalContext.currentConstantBufferIndex / (float)METAL_IMGUI_VIEWPORTS_PER_CHUNK);
int currentIndex = bd->SharedMetalContext.currentConstantBufferIndex % METAL_IMGUI_VIEWPORTS_PER_CHUNK;
int currentFrameIndex = (int)bd->SharedMetalContext.currentFrameSlot;
id<MTLBuffer> constantBuffer = bd->SharedMetalContext.constantBuffers[currentFrameIndex][currentChunk];
ImGui_Metal4_ConstantData* constantBufferContents = (ImGui_Metal4_ConstantData*)constantBuffer.contents;
memcpy(&constantBufferContents->ModelViewProjectionMatrix[currentIndex], ortho_projection, sizeof(ortho_projection));
id<MTL4ArgumentTable> argumentTable = bd->SharedMetalContext.argumentTable;
[argumentTable setAddress:[bd->SharedMetalContext currentConstantBuffer].gpuAddress atIndex:1];
[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];
[commandEncoder setArgumentTable:argumentTable atStages:MTLRenderStageVertex | MTLRenderStageFragment];
@@ -234,8 +258,23 @@ void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id<MTL4CommandBuffer
MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device];
bd->RenderCommandEncoder = commandEncoder;
// check if more chunks are required
const int chunksRequired = 1 + (int)((float)bd->SharedMetalContext.currentConstantBufferIndex / (float)METAL_IMGUI_VIEWPORTS_PER_CHUNK);
if (chunksRequired > bd->SharedMetalContext.constantBufferChunkCount)
{
for (NSUInteger i = 0; i < bd->SharedMetalContext.framesInFlight; i++)
{
id<MTLBuffer> buffer = [bd->SharedMetalContext.device newBufferWithLength:sizeof(ImGui_Metal4_ConstantData) options:MTLResourceStorageModeShared];
[bd->SharedMetalContext.constantBuffers[i] addObject:buffer];
[bd->SharedMetalContext.residencySet addAllocation:buffer];
}
bd->SharedMetalContext.constantBufferChunkCount++;
}
ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
bd->SharedMetalContext.currentConstantBufferIndex++;
// Will project scissor/clipping rectangles into framebuffer space
ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports
ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2)
@@ -415,27 +454,33 @@ bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest;
bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor];
NSMutableArray<id<MTLBuffer>>* constantBuffers = [NSMutableArray array];
ImGui_Metal4_ConstantData** constantBufferContentsArray = (ImGui_Metal4_ConstantData**)malloc(sizeof(ImGui_Metal4_ConstantData*) * bd->SharedMetalContext.framesInFlight);
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++)
{
id<MTLBuffer> constantBuffer = [device newBufferWithLength:sizeof(ImGui_Metal4_ConstantData) options:MTLResourceStorageModeShared];
[constantBuffers addObject:constantBuffer];
constantBufferContentsArray[i] = (ImGui_Metal4_ConstantData*)constantBuffer.contents;
[bd->SharedMetalContext.residencySet addAllocation:constantBuffer];
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];
[bd->SharedMetalContext.constantBuffers[i] addObject:buffer];
[bd->SharedMetalContext.residencySet addAllocation:buffer];
}
bd->SharedMetalContext.constantBuffers = constantBuffers;
bd->SharedMetalContext.constantBufferContentsArray = constantBufferContentsArray;
bd->SharedMetalContext.constantBufferChunkCount = 1;
bd->SharedMetalContext.events = events;
MTL4ArgumentTableDescriptor* argumentTableDescriptor = [[MTL4ArgumentTableDescriptor alloc] init];
argumentTableDescriptor.maxBufferBindCount = 8;
argumentTableDescriptor.maxTextureBindCount = 8;
argumentTableDescriptor.maxSamplerStateBindCount = 8;
argumentTableDescriptor.maxBufferBindCount = 2; // vertex buffer + constant buffer
argumentTableDescriptor.maxTextureBindCount = 1; // font atlas or user texture
argumentTableDescriptor.maxSamplerStateBindCount = 2;
argumentTableDescriptor.supportAttributeStrides = YES; // required: vertex buffer is bound via setAddress:stride:atIndex: for stage_in fetch
bd->SharedMetalContext.commandAllocators = commandAllocators;
bd->SharedMetalContext.argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error];
IM_ASSERT(bd->SharedMetalContext.argumentTable != nil && error == nil);
ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows();
return true;
}
@@ -451,6 +496,8 @@ void ImGui_ImplMetal4_DestroyDeviceObjects()
[bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
bd->SharedMetalContext.samplerStateLinear = nil;
bd->SharedMetalContext.samplerStateNearest = nil;
ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows();
[bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
}
bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQueue, int framesInFlight)
@@ -465,6 +512,7 @@ bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQue
io.BackendRendererName = "imgui_impl_metal4";
io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render.
io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional)
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal4_DrawCallback_ResetRenderState;
@@ -479,6 +527,8 @@ bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQue
for (NSUInteger i = 0; i < framesInFlight; i++)
[bufferCaches addObject:[NSMutableArray array]];
bd->SharedMetalContext.bufferCaches = bufferCaches;
ImGui_ImplMetal_InitMultiViewportSupport();
return true;
}
@@ -490,6 +540,7 @@ void ImGui_ImplMetal4_Shutdown()
ImGuiIO& io = ImGui::GetIO();
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
ImGui_ImplMetal_ShutdownMultiViewportSupport();
ImGui_ImplMetal4_DestroyDeviceObjects();
ImGui_ImplMetal4_DestroyBackendData();
@@ -587,17 +638,6 @@ void ImGui_ImplMetal4_Shutdown()
- (void)dealloc
{
free(_constantBufferContentsArray);
}
- (id<MTLBuffer>)currentConstantBuffer
{
return self.constantBuffers[self.currentFrameSlot];
}
- (ImGui_Metal4_ConstantData*)currentConstantBufferContents
{
return self.constantBufferContentsArray[self.currentFrameSlot];
}
- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device
@@ -739,6 +779,159 @@ fragment half4 fragment_main(VertexOut in [[stage_in]],
@end
#pragma mark - Multi-viewport support
#import <QuartzCore/CAMetalLayer.h>
#if TARGET_OS_OSX
#import <Cocoa/Cocoa.h>
#endif
//--------------------------------------------------------------------------------------------------------
// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT
// This is an _advanced_ and _optional_ feature, allowing the back-end to create and handle multiple viewports simultaneously.
// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first..
//--------------------------------------------------------------------------------------------------------
struct ImGuiViewportDataMetal
{
CAMetalLayer* MetalLayer;
id<MTLCommandQueue> CommandQueue;
MTL4RenderPassDescriptor* RenderPassDescriptor;
void* Handle = nullptr;
bool FirstFrame = true;
};
static void ImGui_ImplMetal_CreateWindow(ImGuiViewport* viewport)
{
ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
ImGuiViewportDataMetal* data = IM_NEW(ImGuiViewportDataMetal)();
viewport->RendererUserData = data;
// PlatformHandleRaw should always be a NSWindow*, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*).
// Some back-ends will leave PlatformHandleRaw == 0, in which case we assume PlatformHandle will contain the NSWindow*.
void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
IM_ASSERT(handle != nullptr);
id<MTLDevice> device = bd->SharedMetalContext.device;
CAMetalLayer* layer = [CAMetalLayer layer];
layer.device = device;
layer.framebufferOnly = YES;
layer.pixelFormat = bd->SharedMetalContext.framebufferDescriptor.colorPixelFormat;
#if TARGET_OS_OSX
NSWindow* window = (__bridge NSWindow*)handle;
NSView* view = window.contentView;
view.layer = layer;
view.wantsLayer = YES;
#endif
data->MetalLayer = layer;
data->CommandQueue = [device newCommandQueue];
data->RenderPassDescriptor = [[MTL4RenderPassDescriptor alloc] init];
data->Handle = handle;
}
static void ImGui_ImplMetal_DestroyWindow(ImGuiViewport* viewport)
{
// The main viewport (owned by the application) will always have RendererUserData == 0 since we didn't create the data for it.
if (ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData)
IM_DELETE(data);
viewport->RendererUserData = nullptr;
}
inline static CGSize MakeScaledSize(CGSize size, CGFloat scale)
{
return CGSizeMake(size.width * scale, size.height * scale);
}
static void ImGui_ImplMetal_SetWindowSize(ImGuiViewport* viewport, ImVec2 size)
{
ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
data->MetalLayer.drawableSize = MakeScaledSize(CGSizeMake(size.x, size.y), viewport->DpiScale);
}
static void ImGui_ImplMetal_RenderWindow(ImGuiViewport* viewport, void*)
{
ImGuiViewportDataMetal* data = (ImGuiViewportDataMetal*)viewport->RendererUserData;
#if TARGET_OS_OSX
void* handle = viewport->PlatformHandleRaw ? viewport->PlatformHandleRaw : viewport->PlatformHandle;
NSWindow* window = (__bridge NSWindow*)handle;
// Always render the first frame, regardless of occlusionState, to avoid an initial flicker
if ((window.occlusionState & NSWindowOcclusionStateVisible) == 0 && !data->FirstFrame)
{
// Do not render windows which are completely occluded. Calling -[CAMetalLayer nextDrawable] will hang for
// approximately 1 second if the Metal layer is completely occluded.
return;
}
data->FirstFrame = false;
float fb_scale = (float)window.backingScaleFactor;
if (data->MetalLayer.contentsScale != fb_scale)
{
data->MetalLayer.contentsScale = fb_scale;
data->MetalLayer.drawableSize = MakeScaledSize(window.frame.size, fb_scale);
}
#endif
id <CAMetalDrawable> drawable = [data->MetalLayer nextDrawable];
if (drawable == nil)
return;
MTL4RenderPassDescriptor* renderPassDescriptor = data->RenderPassDescriptor;
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
if ((viewport->Flags & ImGuiViewportFlags_NoRendererClear) == 0)
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
id <MTL4CommandBuffer> commandBuffer = [bd->SharedMetalContext.device newCommandBuffer];
[commandBuffer beginCommandBufferWithAllocator:bd->SharedMetalContext.commandAllocators[bd->SharedMetalContext.currentFrameSlot]];
id <MTL4RenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
ImGui_ImplMetal4_RenderDrawData(viewport->DrawData, commandBuffer, renderEncoder);
[renderEncoder endEncoding];
[commandBuffer endCommandBuffer];
[bd->SharedMetalContext.commandQueue waitForDrawable:drawable];
[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()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
platform_io.Renderer_CreateWindow = ImGui_ImplMetal_CreateWindow;
platform_io.Renderer_DestroyWindow = ImGui_ImplMetal_DestroyWindow;
platform_io.Renderer_SetWindowSize = ImGui_ImplMetal_SetWindowSize;
platform_io.Renderer_RenderWindow = ImGui_ImplMetal_RenderWindow;
}
static void ImGui_ImplMetal_ShutdownMultiViewportSupport()
{
ImGui::DestroyPlatformWindows();
}
static void ImGui_ImplMetal_CreateDeviceObjectsForPlatformWindows()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
for (int i = 1; i < platform_io.Viewports.Size; i++)
if (!platform_io.Viewports[i]->RendererUserData)
ImGui_ImplMetal_CreateWindow(platform_io.Viewports[i]);
}
static void ImGui_ImplMetal_InvalidateDeviceObjectsForPlatformWindows()
{
ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
for (int i = 1; i < platform_io.Viewports.Size; i++)
if (platform_io.Viewports[i]->RendererUserData)
ImGui_ImplMetal_DestroyWindow(platform_io.Viewports[i]);
}
//-----------------------------------------------------------------------------
#endif // #ifndef IMGUI_DISABLE

View File

@@ -177,6 +177,8 @@ Docking+Viewports Branch:
- Fixed behavior change/regression in 1.92.8 where a window using the
ImGuiWindowFlags_AlwaysAutoResize flag would have ItemWidth default
change when docked. (#9355, #9443) [@reybits]
- Backends:
- Metal4: added multi-viewport support. (#9465, #9451) [@hoffstadt]
-----------------------------------------------------------------------