Presenting drawables when window occluded hangs app on macOS

I am having an issue with CAMetalLayer on macOS.


My app has an NSView subclass which has a CAMetalLayer. The renderer has a single command buffer per frame which has only a single renderpass for the drawable from the layer. The app update-render cycle is called by a millisecond timer on the main thread.


The first issue is with MTLCommandBuffer presentDrawable. If I use this method to present, CAMetalLayer nextDrawable hangs for about a second when the app goes to the background (after this, everything goes back to normal, 60fps). This is a problem for my application, so instead I use MTLCommandBuffer addScheduledHandler and MTLDrawable present, which does not have the one second blocking problem. However, if I set maximumDrawableCount on my CAMetalDrawable to 3 (the default), nextDrawable behaves rather strangely. One frame it blocks for about 16ms (minus the app time) while every other frame it returns almost immediately. This causes an average 120fps resulting in unneccessary CPU and GPU usage. According to Metal System Trace in Instruments, these "short" frames are dropped by the compositor. I have found two ways of fixing this problem. I can set maximumDrawableCount to 2. The other, rather strange fix is to acquire the next drawable from the layer before commiting the command buffer. This however, adds an unneccessary 16ms delay before submitting work to the GPU.


Using a display link callback to signal a semaphore and synchronize the main thread after presenting solves the 120 fps problem, but has another strange issue. If the frame time gets between approx. 5 and 8 milliseconds (for any reason, e.g. sleep) the compositor starts dropping frames. According to Instruments the app uses only two buffers during normal operation, while occasionally using a third when the frame time gets in the problematic range, resulting in frame drops. If it gets any higher than 8ms, the problem goes away, no frame drops and only two buffers are used.


I'm seeing this on Mojave (10.14.6) on a 15" 2019 MBP, both on the integrated and the Radeon discrete GPUs. The app behaves the same on a 16" MBP running Catalina.


Has anyone experienced this issue? Any thoughts? Thanks!

Replies

An update:


I had the idea of circumventing this issue by using MTLCommandBuffer presentDrawable and not render to the window (hence, not requesting a drawable from the layer), if it is occluded. To that end I check the occlusionState of the NSWindow, and early out from the window render pass if it is occluded. I use the display link semaphore to keep the framerate at 60 fps. This way MTLCommandQueue commandBuffer blocks for about a second after a short while, when the window becomes occluded. When I limit the maximum number of uncomplete command buffers on the queue (via MTLDevice newCommandQueueWithMaxCommandBufferCount) MTLCommandQueue commandBuffer blocks almost immediately after the window becomes occluded. This leads me to believe, that the previously presented drawables are delaying the completion of the associated command buffers. I tried verifying this by tracking commited buffers and checking their status, but they all appear to be completed after a few frames even when the hang occurs. Am I right about this? If so, how can I circumvent this? I see no API to cancel presentation of in-flight drawables.


I tried using a separate command queue to create command buffers when the window is occluded. This works, but it doesn't seem to be a good idea.