Backends: Metal4: Added Metal 4 backend. Added SDL3+Metal4 example. (#9458, #9451)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a4d3ed4..cc84ae2 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -572,6 +572,9 @@
     - name: Build macOS example_sdl2_metal
       run: make -C examples/example_sdl2_metal
 
+    - name: Build macOS example_sdl3_metal4
+      run: make -C examples/example_sdl3_metal4
+
     - name: Build macOS example_sdl2_opengl2
       run: make -C examples/example_sdl2_opengl2
       if: github.event_name == 'workflow_run'
diff --git a/.gitignore b/.gitignore
index a8b4e38..83c0ab4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,6 +72,7 @@
 examples/example_sdl2_sdlrenderer2/example_sdl2_sdlrenderer2
 examples/example_sdl2_vulkan/example_sdl2_vulkan
 examples/example_sdl3_metal/example_sdl3_metal
+examples/example_sdl3_metal4/example_sdl3_metal4
 examples/example_sdl3_opengl3/example_sdl3_opengl3
 examples/example_sdl3_sdlgpu3/example_sdl3_sdlgpu3
 examples/example_sdl3_sdlrenderer3/example_sdl3_sdlrenderer3
diff --git a/backends/imgui_impl_metal4.h b/backends/imgui_impl_metal4.h
index 8af2c8b..7f9ec2d 100644
--- a/backends/imgui_impl_metal4.h
+++ b/backends/imgui_impl_metal4.h
@@ -1,8 +1,8 @@
-// dear imgui: Renderer Backend for Metal
+// dear imgui: Renderer Backend for Metal 4
 // This needs to be used along with a Platform Backend (e.g. OSX)
 
 // Implemented features:
-//  [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef!
+//  [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).
 
@@ -24,55 +24,31 @@
 
 #ifdef __OBJC__
 
-@class MTLRenderPassDescriptor;
-@protocol MTLDevice, MTLCommandBuffer, MTLRenderCommandEncoder;
+@class MTL4RenderPassDescriptor;
+@protocol MTLDevice, MTL4CommandBuffer, MTL4RenderCommandEncoder, MTL4CommandQueue;
 
+// framesInFlight must match the number of frames your application keeps in flight (e.g. the size of your own
+// command buffer/allocator ring). The backend uses it to size its own per-frame-in-flight resources (constant
+// buffer, vertex/index buffer cache) so the CPU never overwrites a slot the GPU may still be reading.
 // Follow "Getting Started" link and check examples/ folder to learn about using backends!
-IMGUI_IMPL_API bool ImGui_ImplMetal_Init(id<MTLDevice> device);
-IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
-IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor);
-IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* drawData,
-                                                   id<MTLCommandBuffer> commandBuffer,
-                                                   id<MTLRenderCommandEncoder> commandEncoder);
+IMGUI_IMPL_API bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQueue, int framesInFlight);
+IMGUI_IMPL_API void ImGui_ImplMetal4_Shutdown();
+// frameInFlightIndex must match the slot you use to index your own per-frame-in-flight resources
+// (e.g. the same index used to pick your command buffer/allocator), and must be < framesInFlight passed to Init().
+IMGUI_IMPL_API void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex);
+IMGUI_IMPL_API void ImGui_ImplMetal4_RenderDrawData(ImDrawData* drawData,
+                                                   id<MTL4CommandBuffer> commandBuffer,
+                                                   id<MTL4RenderCommandEncoder> commandEncoder);
 
 // Called by Init/NewFrame/Shutdown
-IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device);
-IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
+IMGUI_IMPL_API bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device);
+IMGUI_IMPL_API void ImGui_ImplMetal4_DestroyDeviceObjects();
 
 // (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually.
-IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
+IMGUI_IMPL_API void ImGui_ImplMetal4_UpdateTexture(ImTextureData* tex);
 
 #endif
 
 //-----------------------------------------------------------------------------
-// C++ API
-//-----------------------------------------------------------------------------
-
-// Enable Metal C++ binding support with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file
-// More info about using Metal from C++: https://developer.apple.com/metal/cpp/
-
-#ifdef IMGUI_IMPL_METAL_CPP
-#include <Metal/Metal.hpp>
-#ifndef __OBJC__
-
-// Follow "Getting Started" link and check examples/ folder to learn about using backends!
-IMGUI_IMPL_API bool ImGui_ImplMetal_Init(MTL::Device* device);
-IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown();
-IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor);
-IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
-                                                   MTL::CommandBuffer* commandBuffer,
-                                                   MTL::RenderCommandEncoder* commandEncoder);
-
-// Called by Init/NewFrame/Shutdown
-IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device);
-IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects();
-
-// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually.
-IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex);
-
-#endif
-#endif
-
-//-----------------------------------------------------------------------------
 
 #endif // #ifndef IMGUI_DISABLE
diff --git a/backends/imgui_impl_metal4.mm b/backends/imgui_impl_metal4.mm
index 652668c..1114b47 100644
--- a/backends/imgui_impl_metal4.mm
+++ b/backends/imgui_impl_metal4.mm
@@ -1,4 +1,4 @@
-// dear imgui: Renderer Backend for Metal
+// dear imgui: Renderer Backend for Metal 4
 // This needs to be used along with a Platform Backend (e.g. OSX)
 
 // Implemented features:
@@ -14,43 +14,26 @@
 // - Documentation        https://dearimgui.com/docs (same as your local docs/ folder).
 // - Introduction, links and more at the top of imgui.cpp
 
+// FIXME: Metal-cpp support
+// FIXME?: Texture view pool support
+
 // CHANGELOG
 // (minor and older changes stripped away, please see git history for details)
-//  2026-04-28: Added support for standard draw callbacks (in platform_io): DrawCallback_SetSamplerLinear and DrawCallback_SetSamplerNearest. (#9378, #9381)
-//  2026-04-23: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState (others are not yet supported). (#9378)
-//  2026-04-14: Metal: use a dedicated bufferCacheLock to avoid crashing when bufferCache is replaced by a new object while being used for @synchronize(). (#9367)
-//  2026-04-03: Metal: avoid redundant vertex buffer bind in SetupRenderState. (#9343)
-//  2026-03-19: Fixed issue in ImGui_ImplMetal_RenderDrawData() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310)
-//  2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown.
-//  2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture().
-//  2025-02-03: Metal: Crash fix. (#8367)
-//  2025-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419).
-//  2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'.
-//  2022-07-05: Metal: Add dispatch synchronization.
-//  2022-06-30: Metal: Use __bridge for ARC based systems.
-//  2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler.
-//  2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts.
-//  2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code).
-//  2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file.
-//  2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464)
-//  2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement)
-//  2021-02-18: Metal: Change blending equation to preserve alpha in output buffer.
-//  2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst.
-//  2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag.
-//  2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state.
-//  2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display.
-//  2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window.
-//  2018-07-05: Metal: Added new Metal backend implementation.
+//  2026-29-06: Metal 4: Added new Metal 4 backend implementation.
 
 #include "imgui.h"
 #ifndef IMGUI_DISABLE
-#include "imgui_impl_metal.h"
+#include "imgui_impl_metal4.h"
 #import <time.h>
 #import <Metal/Metal.h>
 
-#pragma mark - Support classes
+#pragma mark - Support classes and structs
 
-// A wrapper around a MTLBuffer object that knows the last time it was reused
+struct ImGui_Metal4_ConstantData
+{
+    float ModelViewProjectionMatrix[4][4];
+};
+
 @interface MetalBuffer : NSObject
 @property (nonatomic, strong) id<MTLBuffer> buffer;
 @property (nonatomic, assign) double        lastReuseTime;
@@ -64,7 +47,7 @@
 @property (nonatomic, assign) MTLPixelFormat colorPixelFormat;
 @property (nonatomic, assign) MTLPixelFormat depthPixelFormat;
 @property (nonatomic, assign) MTLPixelFormat stencilPixelFormat;
-- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor;
+- (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor;
 @end
 
 @interface MetalTexture : NSObject
@@ -77,83 +60,63 @@
 // font texture, and manages the reusable buffer cache.
 @interface MetalContext : NSObject
 @property (nonatomic, strong) id<MTLDevice>                 device;
+@property (nonatomic, strong) id<MTL4CommandQueue>          commandQueue;
 @property (nonatomic, strong) id<MTLDepthStencilState>      depthStencilState;
+@property (nonatomic, strong) id<MTL4ArgumentTable>         argumentTable;
 @property (nonatomic, strong) id<MTLSamplerState>           samplerStateLinear;
 @property (nonatomic, strong) id<MTLSamplerState>           samplerStateNearest;
-@property (nonatomic, strong) FramebufferDescriptor*        framebufferDescriptor; // framebuffer descriptor for current frame; transient
-@property (nonatomic, strong) NSMutableDictionary*          renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors
-@property (nonatomic, strong) NSMutableArray<MetalBuffer*>* bufferCache;
+@property (nonatomic, strong) id<MTLResidencySet>           residencySet;
+@property (nonatomic, strong) FramebufferDescriptor*        framebufferDescriptor;
+@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;
 - (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id<MTLDevice>)device;
 - (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device;
 @end
 
-struct ImGui_ImplMetal_Data
+struct ImGui_ImplMetal4_Data
 {
-    MetalContext*               SharedMetalContext;
-    id<MTLRenderCommandEncoder> RenderCommandEncoder;
+    MetalContext*                SharedMetalContext;
+    id<MTL4RenderCommandEncoder> RenderCommandEncoder;
 
-    ImGui_ImplMetal_Data()      { memset((void*)this, 0, sizeof(*this)); }
+    ImGui_ImplMetal4_Data()       { memset((void*)this, 0, sizeof(*this)); }
 };
 
-static ImGui_ImplMetal_Data*    ImGui_ImplMetal_GetBackendData()    { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; }
-static void                     ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); }
+static ImGui_ImplMetal4_Data*    ImGui_ImplMetal4_GetBackendData()    { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal4_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; }
+static void                      ImGui_ImplMetal4_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal4_GetBackendData()); }
 
 static inline CFTimeInterval    GetMachAbsoluteTimeInSeconds()      { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); }
 
-#ifdef IMGUI_IMPL_METAL_CPP
-
-#pragma mark - Dear ImGui Metal C++ Backend API
-
-bool ImGui_ImplMetal_Init(MTL::Device* device)
-{
-    return ImGui_ImplMetal_Init((__bridge id<MTLDevice>)(device));
-}
-
-void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor)
-{
-    ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor));
-}
-
-void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data,
-                                    MTL::CommandBuffer* commandBuffer,
-                                    MTL::RenderCommandEncoder* commandEncoder)
-{
-    ImGui_ImplMetal_RenderDrawData(draw_data,
-                                   (__bridge id<MTLCommandBuffer>)(commandBuffer),
-                                   (__bridge id<MTLRenderCommandEncoder>)(commandEncoder));
-
-}
-
-bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device)
-{
-    return ImGui_ImplMetal_CreateDeviceObjects((__bridge id<MTLDevice>)(device));
-}
-
-#endif // #ifdef IMGUI_IMPL_METAL_CPP
-
 #pragma mark - Dear ImGui Metal Backend API
 
-void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor)
+void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex)
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
-    IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?");
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
+    IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal4_Init()?");
+    IM_ASSERT(frameInFlightIndex < bd->SharedMetalContext.framesInFlight && "frameInFlightIndex out of range! See framesInFlight passed to ImGui_ImplMetal4_Init().");
 #ifdef IMGUI_IMPL_METAL_CPP
     bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease];
 #else
     bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor];
 #endif
+    bd->SharedMetalContext.currentFrameSlot = (NSUInteger)frameInFlightIndex;
     if (bd->SharedMetalContext.depthStencilState == nil)
-        ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device);
+        ImGui_ImplMetal4_CreateDeviceObjects(bd->SharedMetalContext.device);
 }
 
-static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer,
-    id<MTLRenderCommandEncoder> commandEncoder, id<MTLRenderPipelineState> renderPipelineState,
+static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id<MTL4CommandBuffer> commandBuffer,
+    id<MTL4RenderCommandEncoder> commandEncoder, id<MTLRenderPipelineState> renderPipelineState,
     MetalBuffer* vertexBuffer, size_t vertexBufferOffset)
 {
     IM_UNUSED(commandBuffer);
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
     [commandEncoder setCullMode:MTLCullModeNone];
     [commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState];
 
@@ -184,23 +147,24 @@
         { 0.0f,         0.0f,        1/(F-N),   0.0f },
         { (R+L)/(L-R),  (T+B)/(B-T), N/(F-N),   1.0f },
     };
-    [commandEncoder setVertexBytes:&ortho_projection length:sizeof(ortho_projection) atIndex:1];
+    ImGui_Metal4_ConstantData* constantBufferContents = [bd->SharedMetalContext currentConstantBufferContents];
+    memcpy(constantBufferContents->ModelViewProjectionMatrix, ortho_projection, sizeof(ortho_projection));
 
+    id<MTL4ArgumentTable> argumentTable = bd->SharedMetalContext.argumentTable;
+    [argumentTable setAddress:[bd->SharedMetalContext currentConstantBuffer].gpuAddress 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];
     [commandEncoder setRenderPipelineState:renderPipelineState];
-    [commandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0];
-
-    [commandEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBufferOffset atIndex:0];
 }
 
-// Draw callbacks
-static void ImGui_ImplMetal_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_ImplMetal_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*)  { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; }
-static void ImGui_ImplMetal_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateNearest atIndex:0]; }
+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]; }
 
-// Metal Render function.
-void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id<MTLCommandBuffer> commandBuffer, id<MTLRenderCommandEncoder> commandEncoder)
+void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id<MTL4CommandBuffer> commandBuffer, id<MTL4RenderCommandEncoder> commandEncoder)
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
     MetalContext* ctx = bd->SharedMetalContext;
 
     // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
@@ -214,7 +178,7 @@
     if (draw_data->Textures != nullptr)
         for (ImTextureData* tex : *draw_data->Textures)
             if (tex->Status != ImTextureStatus_OK)
-                ImGui_ImplMetal_UpdateTexture(tex);
+                ImGui_ImplMetal4_UpdateTexture(tex);
 
     // Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame
     // The hit rate for this cache should be very near 100%.
@@ -234,12 +198,15 @@
     MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device];
 
     bd->RenderCommandEncoder = commandEncoder;
-    ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
+    ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0);
 
     // 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)
 
+    // Before rendering command lists, commit residency set
+    [bd->SharedMetalContext.residencySet commit];
+
     // Render command lists
     size_t vertexBufferOffset = 0;
     size_t indexBufferOffset = 0;
@@ -254,8 +221,8 @@
             if (pcmd->UserCallback)
             {
                 // User callback, registered via ImDrawList::AddCallback()
-                if (pcmd->UserCallback == ImGui_ImplMetal_DrawCallback_ResetRenderState)
-                    ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset);
+                if (pcmd->UserCallback == ImGui_ImplMetal4_DrawCallback_ResetRenderState)
+                    ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset);
                 else
                     pcmd->UserCallback(draw_list, pcmd);
             }
@@ -288,14 +255,19 @@
                 // Bind texture, Draw
                 ImTextureID tex_id = pcmd->GetTexID();
                 if (tex_id != ImTextureID_Invalid)
-                    [commandEncoder setFragmentTexture:(__bridge id<MTLTexture>)(void*)(intptr_t)(tex_id) atIndex:0];
+                {
+                    id<MTLTexture> texture = (__bridge id<MTLTexture>)(void*)(intptr_t)tex_id;
+                    [bd->SharedMetalContext.argumentTable setTexture:texture.gpuResourceID atIndex:0];
+                }
 
-                [commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0];
+                [bd->SharedMetalContext.argumentTable 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
-                                           indexCount:pcmd->ElemCount
-                                            indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32
-                                          indexBuffer:indexBuffer.buffer
-                                    indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)];
+                                   indexCount:pcmd->ElemCount
+                                    indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32
+                                  indexBuffer:indexBuffer.buffer.gpuAddress + indexBufferCmdOffset
+                            indexBufferLength:indexBuffer.buffer.length - indexBufferCmdOffset];
             }
         }
 
@@ -304,18 +276,16 @@
     }
 
     MetalContext* sharedMetalContext = bd->SharedMetalContext;
-    [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>)
+    @synchronized(sharedMetalContext.bufferCacheLock)
     {
-        @synchronized(sharedMetalContext.bufferCacheLock)
-        {
-            [sharedMetalContext.bufferCache addObject:vertexBuffer];
-            [sharedMetalContext.bufferCache addObject:indexBuffer];
-        }
-    }];
+        NSMutableArray<MetalBuffer*>* slotCache = sharedMetalContext.bufferCaches[sharedMetalContext.currentFrameSlot];
+        [slotCache addObject:vertexBuffer];
+        [slotCache addObject:indexBuffer];
+    }
     bd->RenderCommandEncoder = nil;
 }
 
-static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex)
+static void ImGui_ImplMetal4_DestroyTexture(ImTextureData* tex)
 {
     if (MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData))
     {
@@ -329,9 +299,9 @@
     tex->SetStatus(ImTextureStatus_Destroyed);
 }
 
-void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex)
+void ImGui_ImplMetal4_UpdateTexture(ImTextureData* tex)
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
     if (tex->Status == ImTextureStatus_WantCreate)
     {
         // Create and upload new texture to graphics system
@@ -348,12 +318,10 @@
                                                                                                     height:(NSUInteger)tex->Height
                                                                                                  mipmapped:NO];
         textureDescriptor.usage = MTLTextureUsageShaderRead;
-    #if TARGET_OS_OSX || TARGET_OS_MACCATALYST
-        textureDescriptor.storageMode = MTLStorageModeManaged;
-    #else
         textureDescriptor.storageMode = MTLStorageModeShared;
-    #endif
+
         id <MTLTexture> texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor];
+        [bd->SharedMetalContext.residencySet addAllocation:texture];
         [texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4];
         MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture];
 
@@ -378,18 +346,29 @@
     }
     else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0)
     {
-        ImGui_ImplMetal_DestroyTexture(tex);
+        ImGui_ImplMetal4_DestroyTexture(tex);
     }
 }
 
-bool ImGui_ImplMetal_CreateDeviceObjects(id<MTLDevice> device)
+bool ImGui_ImplMetal4_CreateDeviceObjects(id<MTLDevice> device)
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
+
+    MTLResidencySetDescriptor* residencySetDescriptor = [[MTLResidencySetDescriptor alloc] init];
+    residencySetDescriptor.initialCapacity = 1000;
+
+    NSError* error = nil;
+    bd->SharedMetalContext.residencySet = [device newResidencySetWithDescriptor:residencySetDescriptor error:&error];
+    IM_ASSERT(bd->SharedMetalContext.residencySet != nil && error == nil);
+
+    [bd->SharedMetalContext.commandQueue addResidencySet:bd->SharedMetalContext.residencySet];
+
     MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init];
     depthStencilDescriptor.depthWriteEnabled = NO;
     depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways;
     bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor];
     MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init];
+    samplerDescriptor.supportArgumentBuffers = YES;
     samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear;
     samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
     samplerDescriptor.mipFilter = MTLSamplerMipFilterLinear;
@@ -398,61 +377,84 @@
     samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest;
     samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest;
     bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor];
-#ifdef IMGUI_IMPL_METAL_CPP
-    [samplerDescriptor release];
-    [depthStencilDescriptor release];
-#endif
+
+    NSMutableArray<id<MTLBuffer>>* constantBuffers = [NSMutableArray array];
+    ImGui_Metal4_ConstantData** constantBufferContentsArray = (ImGui_Metal4_ConstantData**)malloc(sizeof(ImGui_Metal4_ConstantData*) * bd->SharedMetalContext.framesInFlight);
+    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];
+    }
+    bd->SharedMetalContext.constantBuffers = constantBuffers;
+    bd->SharedMetalContext.constantBufferContentsArray = constantBufferContentsArray;
+
+    MTL4ArgumentTableDescriptor* argumentTableDescriptor = [[MTL4ArgumentTableDescriptor alloc] init];
+    argumentTableDescriptor.maxBufferBindCount = 8;
+    argumentTableDescriptor.maxTextureBindCount = 8;
+    argumentTableDescriptor.maxSamplerStateBindCount = 8;
+    argumentTableDescriptor.supportAttributeStrides = YES; // required: vertex buffer is bound via setAddress:stride:atIndex: for stage_in fetch
+
+    bd->SharedMetalContext.argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error];
+    IM_ASSERT(bd->SharedMetalContext.argumentTable != nil && error == nil);
 
     return true;
 }
 
-void ImGui_ImplMetal_DestroyDeviceObjects()
+void ImGui_ImplMetal4_DestroyDeviceObjects()
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
 
     // Destroy all textures
     for (ImTextureData* tex : ImGui::GetPlatformIO().Textures)
         if (tex->RefCount == 1)
-            ImGui_ImplMetal_DestroyTexture(tex);
+            ImGui_ImplMetal4_DestroyTexture(tex);
 
     [bd->SharedMetalContext.renderPipelineStateCache removeAllObjects];
     bd->SharedMetalContext.samplerStateLinear = nil;
     bd->SharedMetalContext.samplerStateNearest = nil;
 }
 
-bool ImGui_ImplMetal_Init(id<MTLDevice> device)
+bool ImGui_ImplMetal4_Init(id<MTLDevice> device, id<MTL4CommandQueue> commandQueue, int framesInFlight)
 {
     ImGuiIO& io = ImGui::GetIO();
     IMGUI_CHECKVERSION();
     IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!");
+    IM_ASSERT(framesInFlight > 0 && "framesInFlight must be at least 1!");
 
-    ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)();
+    ImGui_ImplMetal4_Data* bd = IM_NEW(ImGui_ImplMetal4_Data)();
     io.BackendRendererUserData = (void*)bd;
-    io.BackendRendererName = "imgui_impl_metal";
+    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.
 
     ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
-    platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal_DrawCallback_ResetRenderState;
-    platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal_DrawCallback_SetSamplerLinear;
-    platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal_DrawCallback_SetSamplerNearest;
+    platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal4_DrawCallback_ResetRenderState;
+    platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal4_DrawCallback_SetSamplerLinear;
+    platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal4_DrawCallback_SetSamplerNearest;
 
     bd->SharedMetalContext = [[MetalContext alloc] init];
     bd->SharedMetalContext.device = device;
-
+    bd->SharedMetalContext.commandQueue = commandQueue;
+    bd->SharedMetalContext.framesInFlight = (NSUInteger)framesInFlight;
+    NSMutableArray<NSMutableArray<MetalBuffer*>*>* bufferCaches = [NSMutableArray array];
+    for (NSUInteger i = 0; i < framesInFlight; i++)
+        [bufferCaches addObject:[NSMutableArray array]];
+    bd->SharedMetalContext.bufferCaches = bufferCaches;
     return true;
 }
 
-void ImGui_ImplMetal_Shutdown()
+void ImGui_ImplMetal4_Shutdown()
 {
-    ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData();
+    ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData();
     IM_UNUSED(bd);
     IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?");
     ImGuiIO& io = ImGui::GetIO();
     ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO();
 
-    ImGui_ImplMetal_DestroyDeviceObjects();
-    ImGui_ImplMetal_DestroyBackendData();
+    ImGui_ImplMetal4_DestroyDeviceObjects();
+    ImGui_ImplMetal4_DestroyBackendData();
 
     io.BackendRendererName = nullptr;
     io.BackendRendererUserData = nullptr;
@@ -460,8 +462,6 @@
     platform_io.ClearRendererHandlers();
 }
 
-#pragma mark - MetalBuffer implementation
-
 @implementation MetalBuffer
 - (instancetype)initWithBuffer:(id<MTLBuffer>)buffer
 {
@@ -477,7 +477,7 @@
 #pragma mark - FramebufferDescriptor implementation
 
 @implementation FramebufferDescriptor
-- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor
+- (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor
 {
     if ((self = [super init]))
     {
@@ -542,39 +542,57 @@
     if ((self = [super init]))
     {
         self.renderPipelineStateCache = [NSMutableDictionary dictionary];
-        self.bufferCache = [NSMutableArray array];
         self.bufferCacheLock = [[NSObject alloc] init];
         _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds();
     }
     return self;
 }
 
+- (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
 {
     double now = GetMachAbsoluteTimeInSeconds();
+    NSMutableArray<MetalBuffer*>* slotCache = self.bufferCaches[self.currentFrameSlot];
 
     @synchronized(self.bufferCacheLock)
     {
         // Purge old buffers that haven't been useful for a while
         if (now - self.lastBufferCachePurge > 1.0)
         {
-            NSMutableArray* survivors = [NSMutableArray array];
-            for (MetalBuffer* candidate in self.bufferCache)
-                if (candidate.lastReuseTime > self.lastBufferCachePurge)
-                    [survivors addObject:candidate];
-            self.bufferCache = [survivors mutableCopy];
+            for (NSMutableArray<MetalBuffer*>* cache in self.bufferCaches)
+            {
+                NSMutableArray* survivors = [NSMutableArray array];
+                for (MetalBuffer* candidate in cache)
+                    if (candidate.lastReuseTime > self.lastBufferCachePurge)
+                        [survivors addObject:candidate];
+                [cache setArray:survivors];
+            }
             self.lastBufferCachePurge = now;
         }
 
-        // See if we have a buffer we can reuse
+        // See if we have a buffer we can reuse, from this frame-in-flight slot's own cache
         MetalBuffer* bestCandidate = nil;
-        for (MetalBuffer* candidate in self.bufferCache)
+        for (MetalBuffer* candidate in slotCache)
             if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime))
                 bestCandidate = candidate;
 
         if (bestCandidate != nil)
         {
-            [self.bufferCache removeObject:bestCandidate];
+            [slotCache removeObject:bestCandidate];
             bestCandidate.lastReuseTime = now;
             return bestCandidate;
         }
@@ -582,51 +600,54 @@
 
     // No luck; make a new buffer
     id<MTLBuffer> backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared];
+    [self.residencySet addAllocation:backing];
     return [[MetalBuffer alloc] initWithBuffer:backing];
 }
 
-// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling.
+const char* shaderCode = R"(
+#include <metal_stdlib>
+using namespace metal;
+
+struct Uniforms {
+    float4x4 projectionMatrix;
+};
+
+struct VertexIn {
+    float2 position  [[attribute(0)]];
+    float2 texCoords [[attribute(1)]];
+    uchar4 color     [[attribute(2)]];
+};
+
+struct VertexOut {
+    float4 position [[position]];
+    float2 texCoords;
+    float4 color;
+};
+
+vertex VertexOut vertex_main(VertexIn in                 [[stage_in]],
+                             constant Uniforms &uniforms [[buffer(1)]])
+{
+    VertexOut out;
+    out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);
+    out.texCoords = in.texCoords;
+    out.color = float4(in.color) / float4(255.0);
+    return out;
+}
+
+fragment half4 fragment_main(VertexOut in [[stage_in]],
+                             texture2d<half, access::sample> texture [[texture(0)]],
+                             sampler textureSampler [[sampler(0)]])
+{
+    half4 texColor = texture.sample(textureSampler, in.texCoords);
+    return half4(in.color) * texColor;
+}
+)";
+
 - (id<MTLRenderPipelineState>)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id<MTLDevice>)device
 {
     NSError* error = nil;
 
-    NSString* shaderSource = @""
-    "#include <metal_stdlib>\n"
-    "using namespace metal;\n"
-    "\n"
-    "struct Uniforms {\n"
-    "    float4x4 projectionMatrix;\n"
-    "};\n"
-    "\n"
-    "struct VertexIn {\n"
-    "    float2 position  [[attribute(0)]];\n"
-    "    float2 texCoords [[attribute(1)]];\n"
-    "    uchar4 color     [[attribute(2)]];\n"
-    "};\n"
-    "\n"
-    "struct VertexOut {\n"
-    "    float4 position [[position]];\n"
-    "    float2 texCoords;\n"
-    "    float4 color;\n"
-    "};\n"
-    "\n"
-    "vertex VertexOut vertex_main(VertexIn in                 [[stage_in]],\n"
-    "                             constant Uniforms &uniforms [[buffer(1)]]) {\n"
-    "    VertexOut out;\n"
-    "    out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n"
-    "    out.texCoords = in.texCoords;\n"
-    "    out.color = float4(in.color) / float4(255.0);\n"
-    "    return out;\n"
-    "}\n"
-    "\n"
-    "fragment half4 fragment_main(VertexOut in [[stage_in]],\n"
-    "                             texture2d<half, access::sample> texture [[texture(0)]],\n"
-    "                             sampler textureSampler [[sampler(0)]]) {\n"
-    "    half4 texColor = texture.sample(textureSampler, in.texCoords);\n"
-    "    return half4(in.color) * texColor;\n"
-    "}\n";
-
-    id<MTLLibrary> library = [device newLibraryWithSource:shaderSource options:nil error:&error];
+    id<MTLLibrary> library = [device newLibraryWithSource:[NSString stringWithUTF8String:shaderCode] options:nil error:&error];
     if (library == nil)
     {
         NSLog(@"Error: failed to create Metal library: %@", error);
diff --git a/examples/example_sdl3_metal4/Makefile b/examples/example_sdl3_metal4/Makefile
new file mode 100644
index 0000000..55c8341
--- /dev/null
+++ b/examples/example_sdl3_metal4/Makefile
@@ -0,0 +1,48 @@
+#
+# You will need SDL3 (http://www.libsdl.org):
+#   brew install sdl3
+#
+
+#CXX = g++
+#CXX = clang++
+
+EXE = example_sdl3_metal4
+IMGUI_DIR = ../..
+SOURCES = main.mm
+SOURCES += $(IMGUI_DIR)/imgui.cpp $(IMGUI_DIR)/imgui_demo.cpp $(IMGUI_DIR)/imgui_draw.cpp $(IMGUI_DIR)/imgui_tables.cpp $(IMGUI_DIR)/imgui_widgets.cpp
+SOURCES += $(IMGUI_DIR)/backends/imgui_impl_sdl3.cpp $(IMGUI_DIR)/backends/imgui_impl_metal4.mm
+OBJS = $(addsuffix .o, $(basename $(notdir $(SOURCES))))
+
+LIBS = -framework Metal -framework MetalKit -framework Cocoa -framework IOKit -framework CoreVideo -framework QuartzCore
+LIBS += `pkg-config --libs sdl3`
+LIBS += -L/usr/local/lib -L/opt/local/lib
+
+CXXFLAGS += `pkg-config --cflags sdl3`
+CXXFLAGS += -I/usr/local/include -I/opt/local/include
+CXXFLAGS += -std=c++11 -I$(IMGUI_DIR) -I$(IMGUI_DIR)/backends
+CXXFLAGS += -Wall -Wformat
+CFLAGS = $(CXXFLAGS)
+
+%.o:%.cpp
+	$(CXX) $(CXXFLAGS) -c -o $@ $<
+
+%.o:$(IMGUI_DIR)/%.cpp
+	$(CXX) $(CXXFLAGS) -c -o $@ $<
+
+%.o:$(IMGUI_DIR)/backends/%.cpp
+	$(CXX) $(CXXFLAGS) -c -o $@ $<
+
+%.o:%.mm
+	$(CXX) $(CXXFLAGS) -ObjC++ -fobjc-weak -fobjc-arc -c -o $@ $<
+
+%.o:$(IMGUI_DIR)/backends/%.mm
+	$(CXX) $(CXXFLAGS) -ObjC++ -fobjc-weak -fobjc-arc -c -o $@ $<
+
+all: $(EXE)
+	@echo Build complete
+
+$(EXE): $(OBJS)
+	$(CXX) -o $@ $^ $(CXXFLAGS) $(LIBS)
+
+clean:
+	rm -f $(EXE) $(OBJS)
diff --git a/examples/example_sdl3_metal4/main.mm b/examples/example_sdl3_metal4/main.mm
new file mode 100644
index 0000000..f0bb8f0
--- /dev/null
+++ b/examples/example_sdl3_metal4/main.mm
@@ -0,0 +1,239 @@
+// Dear ImGui: standalone example application for SDL3 + Metal 4
+// (SDL is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.)
+
+// Learn about Dear ImGui:
+// - FAQ                  https://dearimgui.com/faq
+// - Getting Started      https://dearimgui.com/getting-started
+// - Documentation        https://dearimgui.com/docs (same as your local docs/ folder).
+// - Introduction, links and more at the top of imgui.cpp
+
+#include "imgui.h"
+#include "imgui_impl_sdl3.h"
+#include "imgui_impl_metal4.h"
+#include <stdio.h>          // printf, fprintf
+#include <SDL3/SDL.h>
+
+#import <Metal/Metal.h>
+#import <QuartzCore/QuartzCore.h>
+
+#define FRAMES_IN_FLIGHT 3
+
+// Main code
+int main(int, char**)
+{
+    // Setup SDL
+    // [If using SDL_MAIN_USE_CALLBACKS: all code below until the main loop starts would likely be your SDL_AppInit() function]
+    if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD))
+    {
+        printf("Error: SDL_Init(): %s\n", SDL_GetError());
+        return 1;
+    }
+
+    // Create SDL window graphics context
+    float main_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
+    SDL_WindowFlags window_flags = SDL_WINDOW_METAL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY;
+    SDL_Window* window = SDL_CreateWindow("Dear ImGui SDL3+Metal example", (int)(1280 * main_scale), (int)(800 * main_scale), window_flags);
+    if (window == nullptr)
+    {
+        printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError());
+        return 1;
+    }
+    SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
+    SDL_ShowWindow(window);
+
+    // Create Metal device _before_ creating the view/layer
+    id<MTLDevice> metalDevice = MTLCreateSystemDefaultDevice();
+    if (!metalDevice)
+    {
+        printf("Error: failed to create Metal device.\n");
+        SDL_DestroyWindow(window);
+        SDL_Quit();
+        return 1;
+    }
+    SDL_MetalView view = SDL_Metal_CreateView(window);
+    CAMetalLayer* layer = (__bridge CAMetalLayer*)SDL_Metal_GetLayer(view);
+    layer.device = metalDevice;
+    layer.pixelFormat = MTLPixelFormatBGRA8Unorm;
+
+    id<MTL4CommandQueue> commandQueue = [layer.device newMTL4CommandQueue];
+    id<MTLSharedEvent> sharedEvent = [layer.device newSharedEvent];
+    MTL4RenderPassDescriptor* renderPassDescriptor = [MTL4RenderPassDescriptor new];
+
+    [commandQueue addResidencySet:layer.residencySet];
+
+    id<MTL4CommandBuffer> commandBuffers[FRAMES_IN_FLIGHT];
+    id<MTL4CommandAllocator> commandAllocators[FRAMES_IN_FLIGHT];
+    for (int i = 0; i < FRAMES_IN_FLIGHT; i++)
+    {
+        commandAllocators[i] = [layer.device newCommandAllocator];
+        commandBuffers[i] = [layer.device newCommandBuffer];
+    }
+    uint32_t frameIndex = 0;
+    uint32_t frameInFlight = 0;
+
+    // Setup Dear ImGui context
+    IMGUI_CHECKVERSION();
+    ImGui::CreateContext();
+    ImGuiIO& io = ImGui::GetIO(); (void)io;
+    io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;     // Enable Keyboard Controls
+    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;      // Enable Gamepad Controls
+
+    // Setup Dear ImGui style
+    ImGui::StyleColorsDark();
+    //ImGui::StyleColorsLight();
+
+    // Setup scaling
+    ImGuiStyle& style = ImGui::GetStyle();
+    style.ScaleAllSizes(main_scale);        // Bake a fixed style scale. (until we have a solution for dynamic style scaling, changing this requires resetting Style + calling this again)
+    style.FontScaleDpi = main_scale;        // Set initial font scale. (in docking branch: using io.ConfigDpiScaleFonts=true automatically overrides this for every window depending on the current monitor)
+
+    // Setup Platform/Renderer backends
+    ImGui_ImplMetal4_Init(layer.device, commandQueue, FRAMES_IN_FLIGHT);
+    ImGui_ImplSDL3_InitForMetal(window);
+
+    // Load Fonts
+    // - If fonts are not explicitly loaded, Dear ImGui will select an embedded font: either AddFontDefaultVector() or AddFontDefaultBitmap().
+    //   This selection is based on (style.FontSizeBase * style.FontScaleMain * style.FontScaleDpi) reaching a small threshold.
+    // - You can load multiple fonts and use ImGui::PushFont()/PopFont() to select them.
+    // - If a file cannot be loaded, AddFont functions will return a nullptr. Please handle those errors in your code (e.g. use an assertion, display an error and quit).
+    // - Read 'docs/FONTS.md' for more instructions and details.
+    // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use FreeType for higher quality font rendering.
+    // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ !
+    //style.FontSizeBase = 20.0f;
+    //io.Fonts->AddFontDefaultVector();
+    //io.Fonts->AddFontDefaultBitmap();
+    //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf");
+    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf");
+    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf");
+    //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf");
+    //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf");
+    //IM_ASSERT(font != nullptr);
+
+    // Our state
+    bool show_demo_window = true;
+    bool show_another_window = false;
+    float clear_color[4] = { 0.45f, 0.55f, 0.60f, 1.00f };
+
+    // Main loop
+    bool done = false;
+    while (!done)
+    {
+        @autoreleasepool
+        {
+            // Poll and handle events (inputs, window resize, etc.)
+            // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs.
+            // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
+            // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
+            // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags.
+            // [If using SDL_MAIN_USE_CALLBACKS: call ImGui_ImplSDL3_ProcessEvent() from your SDL_AppEvent() function]
+            SDL_Event event;
+            while (SDL_PollEvent(&event))
+            {
+                ImGui_ImplSDL3_ProcessEvent(&event);
+                if (event.type == SDL_EVENT_QUIT)
+                    done = true;
+                if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window))
+                    done = true;
+            }
+
+            // [If using SDL_MAIN_USE_CALLBACKS: all code below would likely be your SDL_AppIterate() function]
+            if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED)
+            {
+                SDL_Delay(10);
+                continue;
+            }
+
+            int width, height;
+            SDL_GetWindowSizeInPixels(window, &width, &height);
+
+            layer.drawableSize = CGSizeMake(width, height);
+            id<CAMetalDrawable> drawable = [layer nextDrawable];
+
+            uint64_t waitValue = (frameIndex >= FRAMES_IN_FLIGHT) ? (frameIndex - FRAMES_IN_FLIGHT + 1) : 0;
+            [sharedEvent waitUntilSignaledValue:waitValue timeoutMS:1000];
+            [commandAllocators[frameInFlight] reset];
+            [commandBuffers[frameInFlight] beginCommandBufferWithAllocator:commandAllocators[frameInFlight]];
+
+            renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color[0] * clear_color[3], clear_color[1] * clear_color[3], clear_color[2] * clear_color[3], clear_color[3]);
+            renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
+            renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
+            renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
+            id <MTL4RenderCommandEncoder> renderEncoder = [commandBuffers[frameInFlight] renderCommandEncoderWithDescriptor:renderPassDescriptor];
+            [renderEncoder pushDebugGroup:@"ImGui demo"];
+
+            // Start the Dear ImGui frame
+            ImGui_ImplMetal4_NewFrame(renderPassDescriptor, frameInFlight);
+            ImGui_ImplSDL3_NewFrame();
+            ImGui::NewFrame();
+
+            // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!).
+            if (show_demo_window)
+                ImGui::ShowDemoWindow(&show_demo_window);
+
+            // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window.
+            {
+                static float f = 0.0f;
+                static int counter = 0;
+
+                ImGui::Begin("Hello, world!");                          // Create a window called "Hello, world!" and append into it.
+
+                ImGui::Text("This is some useful text.");               // Display some text (you can use a format strings too)
+                ImGui::Checkbox("Demo Window", &show_demo_window);      // Edit bools storing our window open/close state
+                ImGui::Checkbox("Another Window", &show_another_window);
+
+                ImGui::SliderFloat("float", &f, 0.0f, 1.0f);            // Edit 1 float using a slider from 0.0f to 1.0f
+                ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color
+
+                if (ImGui::Button("Button"))                            // Buttons return true when clicked (most widgets return true when edited/activated)
+                    counter++;
+                ImGui::SameLine();
+                ImGui::Text("counter = %d", counter);
+
+                ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
+                ImGui::End();
+            }
+
+            // 3. Show another simple window.
+            if (show_another_window)
+            {
+                ImGui::Begin("Another Window", &show_another_window);   // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
+                ImGui::Text("Hello from another window!");
+                if (ImGui::Button("Close Me"))
+                    show_another_window = false;
+                ImGui::End();
+            }
+
+            // Rendering
+            ImGui::Render();
+            ImDrawData* draw_data = ImGui::GetDrawData();
+            ImGui_ImplMetal4_RenderDrawData(draw_data, commandBuffers[frameInFlight], renderEncoder);
+
+            [renderEncoder popDebugGroup];
+            [renderEncoder endEncoding];
+            [commandBuffers[frameInFlight] endCommandBuffer];
+
+            id<MTL4CommandBuffer> to_commit[] = { commandBuffers[frameInFlight] };
+
+            [commandQueue waitForDrawable:drawable];
+            [commandQueue commit:to_commit count:1];
+
+            [commandQueue signalEvent:sharedEvent value:frameIndex + 1];
+            [commandQueue signalDrawable:drawable];
+            [drawable present];
+
+            frameIndex++;
+            frameInFlight = (frameInFlight + 1) % FRAMES_IN_FLIGHT;
+        }
+    }
+
+    // Cleanup
+    // [If using SDL_MAIN_USE_CALLBACKS: all code below would likely be your SDL_AppQuit() function]
+    ImGui_ImplMetal4_Shutdown();
+    ImGui_ImplSDL3_Shutdown();
+    ImGui::DestroyContext();
+
+    SDL_DestroyWindow(window);
+    SDL_Quit();
+
+    return 0;
+}