Thanks for the reply, great info.
So actually it's probably the case that in iOS 18 the system has added another input source to the runloop, whereas in earlier versions when I invalidated the displayLink the thread basically busy-looped on the run method so the performBlock happened essentially immediately.
I've switched the code for exiting the render loop to this, which shouldn't rely on any of the undefined behaviour and has been working in my testing.
// Schedule a block on the run loop to allow the thread to exit
CFRunLoopRef rl = [_renderThreadRunLoop getCFRunLoop];
CFRunLoopPerformBlock(rl, kCFRunLoopDefaultMode, ^{
self->_continueRunLoop = NO;
// Exit current runMode:beforeDate: method
CFRunLoopStop(rl);
});
// Wake up the run loop to execute the scheduled block immediately
CFRunLoopWakeUp(rl);
Can you clarify the circumstances in which mach ports are leaked? The while loop in the original post is in a function launched as a new thread with pthread_create, my code only adds a CADisplayLink as an input source before the while loop, and when ending the thread I call [displayLink invalidate] and then set the loop variable so the pthread function can exit cleanly. Is there still a mach port leak in that case?
To give a bit more context, this is a dedicated render thread for a custom metal view. The starting point for my code came from this Apple sample:
https://developer.apple.com/documentation/metal/onscreen_presentation/creating_a_custom_metal_view?language=objc
It spins up a render thread when didMoveToWindow is called, as that's the first time there's a UIScreen available to get a CADisplayLink, and that's the only app-side input source the run loop will ever need. When the view moves off a window it then [displayLink invalidate] is called which removes the source from the run loop.
I saw a couple of issues with the code in that example - firstly the render thread isn't stopped when the display link is invalidated. If the system hasn't added any other input sources, the render thread will be effectively busy-looping at this point (runMode would return immediately as there are no sources).
Secondly, the next time the view is added back to a window, a new render thread is started. There's some code that sets _continueRunLoop to NO before starting the new render thread to allow any previous render threads to exit, but no actual guarantee the render thread has checked that flag and exited before setting the same ivar back to YES to get the new thread to run.
My code fixes these issues (and also switched to pthreads) but it still seemed reasonable to me to have the lifetime of the render thread (and related display link, and run loop) all driven by whether the view is attached to a window. If it's not possible to implement that without leaking mach ports then I can have a rethink...