4
votes

In an OpenGL app under OSX, the rendering code typically runs on the DisplayLink thread, which is separate from the Main thread.

When performing tasks in the background, such as loading GL assets, it's important to synchronize the threads, so the rendering thread is not trying to draw from models that are actively being changed by the background thread.

When rendering occurs on the main thread, I've been using GCD to dispatch critical sections of the background tasks to the main dispatch queue like so:

dispatch_async(dispatch_get_main_queue(), ^{ [self doCriticalThing]; });

But this doesn't work when rendering occurs on the DisplayLink thread, because, not surprisingly, I get thread conflicts between the main thread trying to run the critical task while the DisplayLink is attempting to render.

Is it possible to use GCD to dispatch tasks to the DisplayLink thread, instead of to the main thread?

Or do I need to revert to using something like:

performSelector:onThread:withObject:waitUntilDone:

to directly assign the task to the DisplayLink thread?

1

1 Answers

3
votes

There appears to not be any built-in API to achieve this. It'd be fairly straightforward to cook up though. It might look like this:

static CVReturn DispatchDisplayLinkOneshotCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext);

void dispatch_async_displaylink(CVDisplayLinkRef dl, dispatch_block_t block)
{
    if (!block || !dl)
        return;

    CVDisplayLinkRef privateDisplayLink = nil;
    if (kCVReturnSuccess != CVDisplayLinkCreateWithCGDisplay(CVDisplayLinkGetCurrentCGDisplay(dl), &privateDisplayLink) || !privateDisplayLink)
    {
        NSLog(@"Couldn't create display link for dispatch");
        return;
    }

    CVDisplayLinkSetOutputCallback(privateDisplayLink, DispatchDisplayLinkOneshotCallback, (void*)CFBridgingRetain([block copy]));
    CVDisplayLinkStart(privateDisplayLink);
}

static CVReturn DispatchDisplayLinkOneshotCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
{
    dispatch_block_t block = (dispatch_block_t)CFBridgingRelease(displayLinkContext);
    block();
    CVDisplayLinkStop(displayLink);
    CVDisplayLinkSetOutputCallback(displayLink, NULL, NULL);
    CVDisplayLinkRelease(displayLink);
    return kCVReturnSuccess;
}

I do notice, empirically, that the ordering of the eventual execution of blocks enqueued using this approach is not maintained, so if that's important to you, you would probably want to take a different approach, but if all you're looking for is "invoke this block on a display link callback" this ought to do it.

Also, FWIW, this approach is not technically executing on the callback of the exact CVDisplayLink you're passing into it, but rather it's querying that one for the display, then making a new "one-shot" CVDisplayLink so if you need strong guarantees about which exact CVDisplayLink's callback is being used, again, you'd need to add some extra machinery around this.

-performSelector:onThread:withObject:waitUntilDone: is runloop based, and CVDisplayLink threads don't appear have a runloop (they've got something but it doesn't look like a CFRunLoop) EDIT: Apparently this works after all. Not sure what that's about, but it is an option.

EDIT: OK, I couldn't resist. Here's an alternate example that maintains order and uses the specific CVDisplayLink that you pass into it at init time. If you have a "primary" callback that also needs to get called, you have to pass that (and its context) in at init time (since there's no API to fish an existing callback out of a CVDisplayLink once its been set, and we need to set a callback in order to service our queue.)

typedef CVReturn (^CVDisplayLinkBlock)(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut);

@interface MYDisplayLinkDispatcher : NSObject

- (instancetype)initWithDisplayLink: (CVDisplayLinkRef)dl primaryCallback: (CVDisplayLinkOutputCallback)primaryCallback primaryCallbackContext: (void*)pcc;
- (instancetype)initWithDisplayLink: (CVDisplayLinkRef)dl; // No primary callback

- (void)performDisplayLinkBlock: (CVDisplayLinkBlock)block;
- (void)performBlock: (dispatch_block_t)block;

@end

@implementation MYDisplayLinkDispatcher
{
    CVDisplayLinkRef _displayLink;
    NSMutableArray* _performQueue;
    dispatch_queue_t _guardQueue;
    CVDisplayLinkOutputCallback _primaryCallback;
    void* _primaryCallbackContext;
    BOOL _didStartLink;
}

static CVReturn MYDisplayLinkDispatcherOutputCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext);

- (instancetype)initWithDisplayLink: (CVDisplayLinkRef)dl primaryCallback: (CVDisplayLinkOutputCallback)primaryCallback primaryCallbackContext: (void*)pcc
{
    if (self = [super init])
    {
        if (!dl)
        {
            return nil;
        }

        _displayLink = CVDisplayLinkRetain(dl);
        _performQueue = [NSMutableArray array];
        _guardQueue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
        _primaryCallback = primaryCallback;
        _primaryCallbackContext = pcc;

        if (kCVReturnSuccess != CVDisplayLinkSetOutputCallback(_displayLink, MYDisplayLinkDispatcherOutputCallback, (__bridge void *)(self)))
        {
            NSLog(@"Couldn't set output callback on displayLink");
            return nil;
        }
    }
    return self;
}

- (instancetype)initWithDisplayLink: (CVDisplayLinkRef)dl
{
    return [self initWithDisplayLink: dl primaryCallback: NULL primaryCallbackContext: NULL];
}

- (void)dealloc
{
    // If we started it, we should stop it too...
    if (_didStartLink) CVDisplayLinkStop(_displayLink);
    CVDisplayLinkSetOutputCallback(_displayLink, NULL, NULL);
    CVDisplayLinkRelease(_displayLink);
}

- (void)performDisplayLinkBlock: (CVDisplayLinkBlock)block
{
    if (block)
    {

        block = ^CVReturn(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut)
        {
            // Capture self so self is assured to stick around through the execution of the block.
            id localSelf __attribute__ ((objc_precise_lifetime)) = self;
            return block(displayLink, inNow, inOutputTime, flagsIn, flagsOut);
#pragma unused(localSelf)
        };

        // If it's not running we need to start it, otherwise this block will never run.
        const BOOL isRunning = CVDisplayLinkIsRunning(_displayLink);
        dispatch_sync(self->_guardQueue, ^{
            [_performQueue insertObject: [block copy] atIndex: 0];
            _didStartLink = _didStartLink || !isRunning;
        });

        if (!isRunning)
        {
            CVDisplayLinkStart(_displayLink);
        }
    }
}

- (void)performBlock: (dispatch_block_t)block
{
    if (block)
    {
        [self performDisplayLinkBlock:^CVReturn(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut) {
            block();
            return kCVReturnSuccess;
        }];
    }
}

static CVReturn MYDisplayLinkDispatcherOutputCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
{
    MYDisplayLinkDispatcher* self = (__bridge MYDisplayLinkDispatcher *)(displayLinkContext);

    // Always do the primary callback first.
    if (self->_primaryCallback)
    {
        CVReturn result = self->_primaryCallback(displayLink, inNow, inOutputTime, flagsIn, flagsOut, self->_primaryCallbackContext);
        if (kCVReturnSuccess != result)
        {
            return result;
        }
    }

    // Then service the queue until its empty or until we get an error condition.
    __block CVDisplayLinkBlock block = nil;
    do
    {
        dispatch_sync(self->_guardQueue, ^{
            block = [self->_performQueue lastObject];
            [self->_performQueue removeLastObject];
        });

        if (block)
        {
            CVReturn result = block(displayLink, inNow, inOutputTime, flagsIn, flagsOut);
            if (kCVReturnSuccess != result)
            {
                return result;
            }
        }
    } while (block);

    return kCVReturnSuccess;
}

@end

Then to use it, you might do something like this:

CVDisplayLinkRef dl = nil;
if (kCVReturnSuccess != CVDisplayLinkCreateWithActiveCGDisplays(&dl))
{
    NSLog(@"Problem");
}

MYDisplayLinkDispatcher* dld = [[MYDisplayLinkDispatcher alloc] initWithDisplayLink: dl];

for (NSUInteger i = 0; i < 100; i++)
{
    [dld performBlock:^{
        NSLog(@"Whee! Hello from the display link thread! i: %@", @(i));
    }];
}