From a7b5537e8cdcfb222d50fc53cbf7cf8a1dc1d9b7 Mon Sep 17 00:00:00 2001 From: l33tP4j33t <52216194+l33tP4j33t@users.noreply.github.com> Date: Thu, 27 Nov 2025 20:41:00 +0300 Subject: [PATCH 1/6] added display link feature for better latency on OSX --- src/gpu/metal/SDL_gpu_metal.m | 174 +++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 2 deletions(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index fd6c399d9a05f..0c3e5ebc7dfe9 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -22,10 +22,17 @@ #include "SDL_internal.h" #ifdef SDL_GPU_METAL +#if defined(SDL_PLATFORM_MACOS) && !defined(SDL_GPU_NO_DISPLAYLINK) +#define SDL_USE_DISPLAY_LINK +#endif #include #include - +#ifdef SDL_USE_DISPLAY_LINK +#include // CVDisplayLink +#include // NSWindow +#include // CGDirectDisplayID +#endif #include "../SDL_sysgpu.h" // Defines @@ -460,6 +467,19 @@ static MTLDepthClipMode SDLToMetal_DepthClipMode( MetalTextureContainer textureContainer; SDL_GPUFence *inFlightFences[MAX_FRAMES_IN_FLIGHT]; Uint32 frameCounter; + +#ifdef SDL_USE_DISPLAY_LINK + CVDisplayLinkRef displayLink; + // used for checking if the window is moved to a different monitor + CGDirectDisplayID monitor; + // number of command buffers waiting for presentation + SDL_AtomicInt undisplayedFrameCount; + + bool displayLinkSignaled; + SDL_Mutex* displayReadyMutex; + SDL_Condition* displayReadyConditional; + Uint32 monitorCheckInterval; +#endif } MetalWindowData; typedef struct MetalShader @@ -3724,6 +3744,120 @@ static bool METAL_SupportsPresentMode( } } +#ifdef SDL_USE_DISPLAY_LINK +static CVReturn METAL_INTERNAL_DisplayLinkCallback(CVDisplayLinkRef displayLink, + const CVTimeStamp *now, + const CVTimeStamp *outputTime, + CVOptionFlags flagsIn, + CVOptionFlags *flagsOut, + void *displayLinkContext) { + + MetalWindowData *windowData = (MetalWindowData *)displayLinkContext; + SDL_LockMutex(windowData->displayReadyMutex); + windowData->displayLinkSignaled = true; + SDL_SignalCondition(windowData->displayReadyConditional); + SDL_UnlockMutex(windowData->displayReadyMutex); + return kCVReturnSuccess; +} + +// must be only called from the ui thread! +static CGDirectDisplayID METAL_INTERNAL_GetWindowMonitor(SDL_Window* window) { + SDL_PropertiesID props = SDL_GetWindowProperties(window); + if (props == 0) { + SDL_LogError( + SDL_LOG_CATEGORY_GPU, + "Cannot get screen id for window data"); + return 0; + } + + // Query the NSWindow pointer + NSWindow* cocoaWindow = (__bridge NSWindow*)SDL_GetPointerProperty( + props, + SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, // property name + NULL + ); + + CGDirectDisplayID id = + (CGDirectDisplayID)[cocoaWindow.screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue]; + + return id; +} + +static void METAL_INTERNAL_CreateDisplayLink(SDL_Window* window) { + MetalWindowData *windowData = METAL_INTERNAL_FetchWindowData(window); + + windowData->monitor = METAL_INTERNAL_GetWindowMonitor(window); + windowData->displayReadyConditional = SDL_CreateCondition(); + windowData->displayReadyMutex = SDL_CreateMutex(); + + CVDisplayLinkCreateWithActiveCGDisplays(&windowData->displayLink); + CVDisplayLinkSetOutputCallback(windowData->displayLink, &METAL_INTERNAL_DisplayLinkCallback, windowData); + CVDisplayLinkStart(windowData->displayLink); + CVDisplayLinkSetCurrentCGDisplay(windowData->displayLink, windowData->monitor); +} + +static void METAL_INTERNAL_ReleaseDisplayLink(SDL_Window* window) { + MetalWindowData *windowData = METAL_INTERNAL_FetchWindowData(window); + if(windowData->displayLink == NULL) + return; + + CVDisplayLinkStop(windowData->displayLink); + CVDisplayLinkRelease(windowData->displayLink); + SDL_DestroyMutex(windowData->displayReadyMutex); + SDL_DestroyCondition(windowData->displayReadyConditional); +} + +// usually when the window is minimized or not in focus, refresh rate massively goes down +// so we can wait for that (display link doesn't know this and still fires at monitor refresh rate) +// if we don't do this, the four frame queue will persist once the window goes back in focus +#define MAX_UNDISPLAYED_FRAMES 2 +static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData) { + + SDL_LockMutex(windowData->displayReadyMutex); + while(!windowData->displayLinkSignaled) + SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); + + // usually when the window is minimized or not in focus, refresh rate massively goes down + // so we can wait for that (display link doesn't know this and still fires at monitor refresh rate) + // if we don't do this, the four frame queue will persist once the window goes back in focus + if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) >= MAX_UNDISPLAYED_FRAMES) { + windowData->displayLinkSignaled = false; + while(!windowData->displayLinkSignaled) + SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); + } + + SDL_UnlockMutex(windowData->displayReadyMutex); +} + +static bool METAL_INTERNAL_DisplayLinkReady(MetalWindowData* windowData) { + if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) >= MAX_UNDISPLAYED_FRAMES) + return false; + + bool result; + SDL_LockMutex(windowData->displayReadyMutex); + result = windowData->displayLinkSignaled; + SDL_UnlockMutex(windowData->displayReadyMutex); + + return result; +} +#undef kMaxUndisplayedFrames + +static void METAL_INTERNAL_CheckMonitorChange(MetalWindowData* windowData) { + windowData->monitorCheckInterval++; + + // check every 120 refreshes or so if the window was dragged across onto another moniter + if( windowData->monitorCheckInterval >= 120) { + windowData->monitorCheckInterval = 0; + CGDirectDisplayID newMonitorId = METAL_INTERNAL_GetWindowMonitor(windowData->window); + if(newMonitorId != windowData->monitor) { + windowData->monitor = newMonitorId; + CVDisplayLinkSetCurrentCGDisplay(windowData->displayLink, windowData->monitor); + } + } +} + +#endif + static bool METAL_ClaimWindow( SDL_GPURenderer *driverData, SDL_Window *window) @@ -3750,6 +3884,10 @@ static bool METAL_ClaimWindow( renderer->claimedWindows[renderer->claimedWindowCount] = windowData; renderer->claimedWindowCount += 1; +#ifdef SDL_USE_DISPLAY_LINK + METAL_INTERNAL_CreateDisplayLink(window); +#endif + SDL_UnlockMutex(renderer->windowLock); return true; @@ -3795,6 +3933,10 @@ static void METAL_ReleaseWindow( } SDL_UnlockMutex(renderer->windowLock); +#ifdef SDL_USE_DISPLAY_LINK + METAL_INTERNAL_ReleaseDisplayLink(window); +#endif + SDL_free(windowData); SDL_ClearProperty(SDL_GetWindowProperties(window), WINDOW_PROPERTY_DATA); @@ -3813,6 +3955,10 @@ static bool METAL_WaitForSwapchain( SET_STRING_ERROR_AND_RETURN("Cannot wait for a swapchain from an unclaimed window!", false); } +#ifdef SDL_USE_DISPLAY_LINK + METAL_INTERNAL_WaitForDisplayLink(windowData); +#endif + if (windowData->inFlightFences[windowData->frameCounter] != NULL) { if (!METAL_WaitForFences( driverData, @@ -3853,6 +3999,21 @@ static bool METAL_INTERNAL_AcquireSwapchainTexture( if (windowData == NULL) { SET_STRING_ERROR_AND_RETURN("Window is not claimed by this SDL_GPUDevice", false); } + +#ifdef SDL_USE_DISPLAY_LINK + if(block) { + METAL_INTERNAL_WaitForDisplayLink(windowData); + } else { + if(!METAL_INTERNAL_DisplayLinkReady(windowData)) + return true; + } + + SDL_LockMutex(windowData->displayReadyMutex); + windowData->displayLinkSignaled = false; + SDL_UnlockMutex(windowData->displayReadyMutex); + METAL_INTERNAL_CheckMonitorChange(windowData); +#endif + // Update the window size drawableSize = windowData->layer.drawableSize; @@ -4043,6 +4204,15 @@ static bool METAL_Submit( // Enqueue present requests, if applicable for (Uint32 i = 0; i < metalCommandBuffer->windowDataCount; i += 1) { MetalWindowData *windowData = metalCommandBuffer->windowDatas[i]; + +#ifdef SDL_USE_DISPLAY_LINK + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, +1); + + [windowData->drawable addPresentedHandler:^(id drawable) { + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, -1); + }]; +#endif + [metalCommandBuffer->handle presentDrawable:windowData->drawable]; windowData->drawable = nil; @@ -4055,7 +4225,7 @@ static bool METAL_Submit( // Notify the fence when the command buffer has completed [metalCommandBuffer->handle addCompletedHandler:^(id buffer) { - SDL_AtomicIncRef(&metalCommandBuffer->fence->complete); + SDL_AtomicIncRef(&metalCommandBuffer->fence->complete); }]; // Submit the command buffer From ead6151bbad25aa94a092b26ae18c5bcc2fa35ec Mon Sep 17 00:00:00 2001 From: Alexey Tselousov <52216194+alexgu754@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:32:06 +0300 Subject: [PATCH 2/6] Update SDL_gpu_metal.m added @available guards to allow it to compile on older versions, made use of renderer->allowedFramesInFlight instead of making it a constant --- src/gpu/metal/SDL_gpu_metal.m | 70 ++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index 0c3e5ebc7dfe9..86d430c9b3775 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -3807,11 +3807,7 @@ static void METAL_INTERNAL_ReleaseDisplayLink(SDL_Window* window) { SDL_DestroyCondition(windowData->displayReadyConditional); } -// usually when the window is minimized or not in focus, refresh rate massively goes down -// so we can wait for that (display link doesn't know this and still fires at monitor refresh rate) -// if we don't do this, the four frame queue will persist once the window goes back in focus -#define MAX_UNDISPLAYED_FRAMES 2 -static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData) { +static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData, int maxUndisplayedFrames) { SDL_LockMutex(windowData->displayReadyMutex); while(!windowData->displayLinkSignaled) @@ -3819,18 +3815,27 @@ static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData) { // usually when the window is minimized or not in focus, refresh rate massively goes down // so we can wait for that (display link doesn't know this and still fires at monitor refresh rate) - // if we don't do this, the four frame queue will persist once the window goes back in focus - if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) >= MAX_UNDISPLAYED_FRAMES) { - windowData->displayLinkSignaled = false; - while(!windowData->displayLinkSignaled) - SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); + // if we don't do this, the four frame queue will persist once the window goes back in focus. + // + // currently the frames in flight implementation is totally broken and instead caps command buffer + // count with a busy loop so we can't rely on that, the two could be combined in a future pr + // + // has to be guarded by @availble since we don't know how many frames are waiting for presentation + // before that version. it could be tied to the number of active command buffers in those older + // versions, this is something to be tested, but its not the case at all anymore + if(@available(macOS 10.15.4, *)) { + if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) > maxUndisplayedFrames) { + windowData->displayLinkSignaled = false; + while(!windowData->displayLinkSignaled) + SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); + } } SDL_UnlockMutex(windowData->displayReadyMutex); } -static bool METAL_INTERNAL_DisplayLinkReady(MetalWindowData* windowData) { - if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) >= MAX_UNDISPLAYED_FRAMES) +static bool METAL_INTERNAL_DisplayLinkReady(MetalWindowData* windowData, int maxUndisplayedFrames) { + if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) > maxUndisplayedFrames) return false; bool result; @@ -3840,7 +3845,6 @@ static bool METAL_INTERNAL_DisplayLinkReady(MetalWindowData* windowData) { return result; } -#undef kMaxUndisplayedFrames static void METAL_INTERNAL_CheckMonitorChange(MetalWindowData* windowData) { windowData->monitorCheckInterval++; @@ -3956,7 +3960,9 @@ static bool METAL_WaitForSwapchain( } #ifdef SDL_USE_DISPLAY_LINK - METAL_INTERNAL_WaitForDisplayLink(windowData); + if(windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { + METAL_INTERNAL_WaitForDisplayLink(windowData, renderer->allowedFramesInFlight); + } #endif if (windowData->inFlightFences[windowData->frameCounter] != NULL) { @@ -3999,19 +4005,21 @@ static bool METAL_INTERNAL_AcquireSwapchainTexture( if (windowData == NULL) { SET_STRING_ERROR_AND_RETURN("Window is not claimed by this SDL_GPUDevice", false); } - + #ifdef SDL_USE_DISPLAY_LINK - if(block) { - METAL_INTERNAL_WaitForDisplayLink(windowData); - } else { - if(!METAL_INTERNAL_DisplayLinkReady(windowData)) - return true; - } + if(windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { + if(block) { + METAL_INTERNAL_WaitForDisplayLink(windowData, renderer->allowedFramesInFlight); + } else { + if(!METAL_INTERNAL_DisplayLinkReady(windowData, renderer->allowedFramesInFlight)) + return true; + } - SDL_LockMutex(windowData->displayReadyMutex); - windowData->displayLinkSignaled = false; - SDL_UnlockMutex(windowData->displayReadyMutex); - METAL_INTERNAL_CheckMonitorChange(windowData); + SDL_LockMutex(windowData->displayReadyMutex); + windowData->displayLinkSignaled = false; + SDL_UnlockMutex(windowData->displayReadyMutex); + METAL_INTERNAL_CheckMonitorChange(windowData); + } #endif @@ -4206,11 +4214,13 @@ static bool METAL_Submit( MetalWindowData *windowData = metalCommandBuffer->windowDatas[i]; #ifdef SDL_USE_DISPLAY_LINK - SDL_AddAtomicInt(&windowData->undisplayedFrameCount, +1); + if(@available(macOS 10.15.4, *)) { + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, +1); - [windowData->drawable addPresentedHandler:^(id drawable) { - SDL_AddAtomicInt(&windowData->undisplayedFrameCount, -1); - }]; + [windowData->drawable addPresentedHandler:^(id drawable) { + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, -1); + }]; + } #endif [metalCommandBuffer->handle presentDrawable:windowData->drawable]; @@ -4726,7 +4736,7 @@ static void METAL_INTERNAL_DestroyBlitResources( // Remember debug mode renderer->debugMode = debugMode; - renderer->allowedFramesInFlight = 2; + renderer->allowedFramesInFlight = 1; // Set up colorspace array SwapchainCompositionToColorSpace[0] = kCGColorSpaceSRGB; From 413ca8d5864b073916d0816442bfb88bc35e9257 Mon Sep 17 00:00:00 2001 From: Alexey Tselousov <52216194+alexgu754@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:35:18 +0300 Subject: [PATCH 3/6] Update SDL_gpu_metal.m oops, accidentally left in allowedFramesInFlight = 1 --- src/gpu/metal/SDL_gpu_metal.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index 86d430c9b3775..8176512d90031 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -4736,7 +4736,7 @@ static void METAL_INTERNAL_DestroyBlitResources( // Remember debug mode renderer->debugMode = debugMode; - renderer->allowedFramesInFlight = 1; + renderer->allowedFramesInFlight = 2; // Set up colorspace array SwapchainCompositionToColorSpace[0] = kCGColorSpaceSRGB; From 2a8b6a6e106252236a3b5d595178cd0242aabadd Mon Sep 17 00:00:00 2001 From: Alexey Tselousov <52216194+alexgu754@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:10:31 +0300 Subject: [PATCH 4/6] Update SDL_gpu_metal.m made display link toggleable with SDL.gpu.device.create.metal.use_display_link props id in device creation --- src/gpu/metal/SDL_gpu_metal.m | 66 +++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index 8176512d90031..b3dbcf4d3889a 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -23,12 +23,12 @@ #ifdef SDL_GPU_METAL #if defined(SDL_PLATFORM_MACOS) && !defined(SDL_GPU_NO_DISPLAYLINK) -#define SDL_USE_DISPLAY_LINK +#define SDL_DISPLAY_LINK_AVAILABLE #endif #include #include -#ifdef SDL_USE_DISPLAY_LINK +#ifdef SDL_DISPLAY_LINK_AVAILABLE #include // CVDisplayLink #include // NSWindow #include // CGDirectDisplayID @@ -468,7 +468,7 @@ static MTLDepthClipMode SDLToMetal_DepthClipMode( SDL_GPUFence *inFlightFences[MAX_FRAMES_IN_FLIGHT]; Uint32 frameCounter; -#ifdef SDL_USE_DISPLAY_LINK +#ifdef SDL_DISPLAY_LINK_AVAILABLE CVDisplayLinkRef displayLink; // used for checking if the window is moved to a different monitor CGDirectDisplayID monitor; @@ -651,6 +651,7 @@ static MTLDepthClipMode SDLToMetal_DepthClipMode( id queue; bool debugMode; + bool useDisplayLink; SDL_PropertiesID props; Uint32 allowedFramesInFlight; @@ -3744,7 +3745,7 @@ static bool METAL_SupportsPresentMode( } } -#ifdef SDL_USE_DISPLAY_LINK +#ifdef SDL_DISPLAY_LINK_AVAILABLE static CVReturn METAL_INTERNAL_DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, @@ -3807,6 +3808,12 @@ static void METAL_INTERNAL_ReleaseDisplayLink(SDL_Window* window) { SDL_DestroyCondition(windowData->displayReadyConditional); } +static void waitForDisplay2(MetalWindowData* windowData) { + SDL_LockMutex(windowData->displayReadyMutex); + while(!windowData->displayLinkSignaled) + SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); + SDL_UnlockMutex(windowData->displayReadyMutex); +} static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData, int maxUndisplayedFrames) { SDL_LockMutex(windowData->displayReadyMutex); @@ -3821,8 +3828,8 @@ static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData, int m // count with a busy loop so we can't rely on that, the two could be combined in a future pr // // has to be guarded by @availble since we don't know how many frames are waiting for presentation - // before that version. it could be tied to the number of active command buffers in those older - // versions, this is something to be tested, but its not the case at all anymore + // before that. it could be tied to the number of active command buffers in those older versions, + // this is something to be tested, but thats not the case at all anymore if(@available(macOS 10.15.4, *)) { if(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) > maxUndisplayedFrames) { windowData->displayLinkSignaled = false; @@ -3888,10 +3895,10 @@ static bool METAL_ClaimWindow( renderer->claimedWindows[renderer->claimedWindowCount] = windowData; renderer->claimedWindowCount += 1; -#ifdef SDL_USE_DISPLAY_LINK - METAL_INTERNAL_CreateDisplayLink(window); +#ifdef SDL_DISPLAY_LINK_AVAILABLE + if(renderer->useDisplayLink) + METAL_INTERNAL_CreateDisplayLink(window); #endif - SDL_UnlockMutex(renderer->windowLock); return true; @@ -3936,11 +3943,10 @@ static void METAL_ReleaseWindow( } } SDL_UnlockMutex(renderer->windowLock); - -#ifdef SDL_USE_DISPLAY_LINK - METAL_INTERNAL_ReleaseDisplayLink(window); +#ifdef SDL_DISPLAY_LINK_AVAILABLE + if(renderer->useDisplayLink) + METAL_INTERNAL_ReleaseDisplayLink(window); #endif - SDL_free(windowData); SDL_ClearProperty(SDL_GetWindowProperties(window), WINDOW_PROPERTY_DATA); @@ -3958,13 +3964,11 @@ static bool METAL_WaitForSwapchain( if (windowData == NULL) { SET_STRING_ERROR_AND_RETURN("Cannot wait for a swapchain from an unclaimed window!", false); } - -#ifdef SDL_USE_DISPLAY_LINK - if(windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { +#ifdef SDL_DISPLAY_LINK_AVAILABLE + if(renderer->useDisplayLink && windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { METAL_INTERNAL_WaitForDisplayLink(windowData, renderer->allowedFramesInFlight); } #endif - if (windowData->inFlightFences[windowData->frameCounter] != NULL) { if (!METAL_WaitForFences( driverData, @@ -4005,9 +4009,8 @@ static bool METAL_INTERNAL_AcquireSwapchainTexture( if (windowData == NULL) { SET_STRING_ERROR_AND_RETURN("Window is not claimed by this SDL_GPUDevice", false); } - -#ifdef SDL_USE_DISPLAY_LINK - if(windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { +#ifdef SDL_DISPLAY_LINK_AVAILABLE + if(renderer->useDisplayLink && windowData->presentMode != SDL_GPU_PRESENTMODE_IMMEDIATE) { if(block) { METAL_INTERNAL_WaitForDisplayLink(windowData, renderer->allowedFramesInFlight); } else { @@ -4022,7 +4025,6 @@ static bool METAL_INTERNAL_AcquireSwapchainTexture( } #endif - // Update the window size drawableSize = windowData->layer.drawableSize; windowData->textureContainer.header.info.width = (Uint32)drawableSize.width; @@ -4213,20 +4215,26 @@ static bool METAL_Submit( for (Uint32 i = 0; i < metalCommandBuffer->windowDataCount; i += 1) { MetalWindowData *windowData = metalCommandBuffer->windowDatas[i]; -#ifdef SDL_USE_DISPLAY_LINK +#ifdef SDL_DISPLAY_LINK_AVAILABLE if(@available(macOS 10.15.4, *)) { - SDL_AddAtomicInt(&windowData->undisplayedFrameCount, +1); + if(renderer->useDisplayLink) { + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, +1); - [windowData->drawable addPresentedHandler:^(id drawable) { - SDL_AddAtomicInt(&windowData->undisplayedFrameCount, -1); - }]; + [windowData->drawable addPresentedHandler:^(id drawable) { + SDL_AddAtomicInt(&windowData->undisplayedFrameCount, -1); + }]; + } } #endif [metalCommandBuffer->handle presentDrawable:windowData->drawable]; windowData->drawable = nil; - windowData->inFlightFences[windowData->frameCounter] = (SDL_GPUFence *)metalCommandBuffer->fence; + if(@available(macOS 10.15.4, *)) { + + } else { + windowData->inFlightFences[windowData->frameCounter] = (SDL_GPUFence *)metalCommandBuffer->fence; + } (void)SDL_AtomicIncRef(&metalCommandBuffer->fence->referenceCount); @@ -4737,6 +4745,10 @@ static void METAL_INTERNAL_DestroyBlitResources( // Remember debug mode renderer->debugMode = debugMode; renderer->allowedFramesInFlight = 2; + renderer->useDisplayLink = SDL_GetBooleanProperty( + props, + "SDL.gpu.device.create.metal.use_display_link", + true); // Set up colorspace array SwapchainCompositionToColorSpace[0] = kCGColorSpaceSRGB; From 1321b045e5d2f903601e77439da88dd361c2ce8b Mon Sep 17 00:00:00 2001 From: Alexey Tselousov <52216194+alexgu754@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:15:56 +0300 Subject: [PATCH 5/6] Update SDL_gpu_metal.m left in preliminary code accidentally --- src/gpu/metal/SDL_gpu_metal.m | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index b3dbcf4d3889a..e1db49aa6667a 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -3808,12 +3808,6 @@ static void METAL_INTERNAL_ReleaseDisplayLink(SDL_Window* window) { SDL_DestroyCondition(windowData->displayReadyConditional); } -static void waitForDisplay2(MetalWindowData* windowData) { - SDL_LockMutex(windowData->displayReadyMutex); - while(!windowData->displayLinkSignaled) - SDL_WaitCondition(windowData->displayReadyConditional, windowData->displayReadyMutex); - SDL_UnlockMutex(windowData->displayReadyMutex); -} static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData, int maxUndisplayedFrames) { SDL_LockMutex(windowData->displayReadyMutex); @@ -3824,8 +3818,8 @@ static void METAL_INTERNAL_WaitForDisplayLink(MetalWindowData* windowData, int m // so we can wait for that (display link doesn't know this and still fires at monitor refresh rate) // if we don't do this, the four frame queue will persist once the window goes back in focus. // - // currently the frames in flight implementation is totally broken and instead caps command buffer - // count with a busy loop so we can't rely on that, the two could be combined in a future pr + // currently the frames in flight implementation is broken and we can't rely on that, + // the two could be combined in a future pr // // has to be guarded by @availble since we don't know how many frames are waiting for presentation // before that. it could be tied to the number of active command buffers in those older versions, @@ -4230,11 +4224,7 @@ static bool METAL_Submit( [metalCommandBuffer->handle presentDrawable:windowData->drawable]; windowData->drawable = nil; - if(@available(macOS 10.15.4, *)) { - - } else { - windowData->inFlightFences[windowData->frameCounter] = (SDL_GPUFence *)metalCommandBuffer->fence; - } + windowData->inFlightFences[windowData->frameCounter] = (SDL_GPUFence *)metalCommandBuffer->fence; (void)SDL_AtomicIncRef(&metalCommandBuffer->fence->referenceCount); From a81e068620bf88410bf7cbc48a89de82dcb4fd69 Mon Sep 17 00:00:00 2001 From: Alexey Tselousov <52216194+alexgu754@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:00:06 +0300 Subject: [PATCH 6/6] Update SDL_gpu_metal.m need to wait for addPresentedHandler calls in cleanup, since they still fire even after SDL_Metal_DestroyView is called --- src/gpu/metal/SDL_gpu_metal.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gpu/metal/SDL_gpu_metal.m b/src/gpu/metal/SDL_gpu_metal.m index e1db49aa6667a..9399d91b950cc 100644 --- a/src/gpu/metal/SDL_gpu_metal.m +++ b/src/gpu/metal/SDL_gpu_metal.m @@ -3919,6 +3919,14 @@ static void METAL_ReleaseWindow( } METAL_Wait(driverData); +#ifdef SDL_DISPLAY_LINK_AVAILABLE + if(renderer->useDisplayLink) + METAL_INTERNAL_ReleaseDisplayLink(window); + + while(SDL_GetAtomicInt(&windowData->undisplayedFrameCount) != 0) { + // spin + } +#endif SDL_Metal_DestroyView(windowData->view); for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; i += 1) { if (windowData->inFlightFences[i] != NULL) { @@ -3937,10 +3945,6 @@ static void METAL_ReleaseWindow( } } SDL_UnlockMutex(renderer->windowLock); -#ifdef SDL_DISPLAY_LINK_AVAILABLE - if(renderer->useDisplayLink) - METAL_INTERNAL_ReleaseDisplayLink(window); -#endif SDL_free(windowData); SDL_ClearProperty(SDL_GetWindowProperties(window), WINDOW_PROPERTY_DATA);