3

I have an application which renders OpenGL content on Mac OS X. Originally it was rendering to an NSOpenGLView, then I changed it to render to a CAOpenGLLayer subclass.

When I did so I saw a huge performance loss: halved framerate, lower mouse responsivity, stuttering (stops from time to time, up to a second, during which profiler activity reports waiting on mutex for data to load on GPU ram), and doubled CPU usage.

I'm investigating this issue and had a few questions:

  • Has a similar performance hit been seen by someone else?
  • Am I doing something wrong with my CAOpenGLLayer setup?
  • How is CAOpenGLLayer and the Core Animation framework implemented, i.e. what path does my OpenGL content do from my glDrawElements calls up to my screen, and how should I do things on my side to optimize performance with such setup?

Here's my code for CAOpenGLLayer setup:

// my application's entry point (can't be easily changed):
void AppUpdateLogic(); //update application logic. Will load textures
void AppRender(); //executes drawing
void AppEventSink(NSEvent* ev); //handle mouse and keyboard events.
                                //Will do pick renderings

@interface MyCAOpenGLLayer: CAOpenGLLayer
{
    CGLPixelFormatObj   pixelFormat;
    CGLContextObj       glContext;
}
@end

@implementation MyCAOpenGLLayer

- (id)init {
    self = [super init];

    CGLPixelFormatAttribute attributes[] =
    {
        kCGLPFAAccelerated,
        kCGLPFAColorSize, (CGLPixelFormatAttribute)24,
        kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8,
        kCGLPFADepthSize, (CGLPixelFormatAttribute)16,
        (CGLPixelFormatAttribute)0
    };

    GLint numPixelFormats = 0;
    CGLChoosePixelFormat(attributes, &pixelFormat, &numPixelFormats);

    glContext = [super copyCGLContextForPixelFormat:mPixelFormat];

    return self;
}

- (void)drawInCGLContext:(CGLContextObj)inGlContext
             pixelFormat:(CGLPixelFormatObj)inPixelFormat
            forLayerTime:(CFTimeInterval)timeInterval
             displayTime:(const CVTimeStamp *)timeStamp 
{
    AppRender();
    [super drawInCGLContext:inGlContext
                pixelFormat:inPixelFormat
               forLayerTime:timeInterval
                displayTime:timeStamp ]
}

- (void)releaseCGLPixelFormat:(CGLPixelFormatObj)pixelFormat {
    [self release];
}

- (CGLPixelFormatObj)copyCGLPixelFormatForDisplayMask:(uint32_t)mask {
    [self retain];
    return pixelFormat;
}

- (CGLContextObj)copyCGLContextForPixelFormat:(CGLPixelFormatObj)pixelFormat {
    [self retain];
    return glContext;
}

- (void)releaseCGLContext:(CGLContextObj)glContext {
    [self release];
}

@end

@interface MyMainViewController: NSViewController {
    CGLContextObj   glContext;
    CALayer*        myOpenGLLayer;
}

-(void)timerTriggered:(NSTimer*)timer;
@end


@implementation MyMainViewController

-(void)viewDidLoad:(NSView*)view {
    myOpenGLLayer = [[MyCAOpenGLLayer alloc] init];
    [view setLayer:myOpenGLLayer];
    [view setWantsLayer:YES];

    glContext = [myOpenGLLayer copyCGLContextForPixelFormat:nil];

    [NSTimer scheduledTimerWithTimeInterval:1/30.0
                                     target:self
                                   selector:@selector(timerTriggered:)
                                   userInfo:nil
                                    repeats:YES ];
}

- (void)timerTriggered:(NSTimer*)timer {
    CGLContextObj oldContext = CGLContextGetCurrent();
    CGLContextSetCurrent(glContext);
    CGLContextLock(glContext);

    AppUpdateLogic();
    [myOpenGLLayer  setNeedsDisplay:YES];

    CGLContextUnlock(glContext);
    CGLContextSetCurrent(oldContext);
}

- (void)mouseDown:(NSEvent*)event {
    CGLContextObj oldContext = CGLContextGetCurrent();
    CGLContextSetCurrent(glContext);
    CGLContextLock(glContext);

    AppEventSink(event);

    CGLContextUnlock(glContext);
    CGLContextSetCurrent(oldContext);
}
@end

It may be useful to know my video card isn't very powerful (Intel GMA with 64 MB of shared memory).

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
pqnet
  • 6,070
  • 1
  • 30
  • 51

1 Answers1

3

In one of my applications, I switched from NSOpenGLView to a CAOpenGLLayer, then ended up going back because of a few issues with the update mechanism on the latter. However, that's different from the performance issues you're reporting here.

In your case, I believe that the way you're performing the update of your layer contents may be to blame. First, using NSTimer to trigger a redraw does not guarantee that the update events will align well with the refresh rate of your display. Instead, I'd suggest setting the CAOpenGLLayer's asynchronous property to YES and using the –canDrawInCGLContext:pixelFormat:forLayerTime:displayTime: to manage the update frequency. This will cause the OpenGL layer to update in sync with the display, and it will avoid the context locking that you're doing.

The downside to this (which is also a problem with your NSTimer approach) is that the CAOpenGLLayer delegate callbacks are triggered on the main thread. If you have something that blocks the main thread, your display will freeze. Likewise, if your OpenGL frame updates take a while, they may cause your UI to be less responsive.

This is what caused me to use a CVDisplayLink to produce a triggered update of my OpenGL content on a background thread. Unfortunately, I saw some rendering artifacts when updating my CAOpenGLLayer with this, so I ended up switching back to an NSOpenGLView. Since then, I've encountered a way to potentially avoid these artifacts, but the NSOpenGLView has been fine for our needs so I haven't switched back once again.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • calls to the three App* functions are expected to be run from the same thread, so I can't really take advantage of the possibility of running logic update in a different thread from the drawing thread, and anyway it's like that even for the NSOpenGLView sample. I'll try using the CanDrawInCGLContext:pixelFormat:forLayerTime:displayTime: and asynchronous approach, will tell if it helps – pqnet May 25 '11 at 09:32
  • @pqnet - For interactive items that change something about the scene, I've used a flag that I set when something needed to be changed, then read that flag when the render callback was triggered. I would then update the scene right before redrawing it. Recently, I've employed GCD to do updates to the OpenGL context, while preventing simultaneous access to that context on multiple threads. I talk a little more about it in my answer here: http://stackoverflow.com/questions/5944050/cadisplaylink-opengl-rendering-breaks-uiscrollview-behaviour/5956119#5956119 – Brad Larson May 25 '11 at 15:03
  • @BradLarson - I came across this answer because I'm looking for a way to have a `CAOpenGLLayer`draw from a background thread. You mentionded that you have found a way to use a `CVDisplayLink`with `CAOpenGLLayer`without artifacts. Is that still valid? Have you got some references that explain how to achieve that? Thank you – Andrea3000 Jul 10 '12 at 18:44
  • 1
    @Andrea3000 - See some of the discussion in the comments on this answer: http://stackoverflow.com/a/4740299/19679 . I believe that in my case, it may have been due to the use of `[CATransaction flush]` on a background thread, but the use of a begin / commit block might prevent this, as would running the flush on the main thread. I remember experimenting with something along those lines, but I've only recently started looking at this again. I may give this another try. – Brad Larson Jul 10 '12 at 18:51
  • @BradLarson - Thank you very much for your fast reply! I've only a little experience in programming therefore it will take some time before I will fully understand the discussion and build a test app. Anyway, I'll let you know if I'll be able to avoid artifacts on OSX with that solution. Thank you! – Andrea3000 Jul 10 '12 at 19:04
  • @BradLarson - I have a complementary question. Does the combination of `CVDisplayLink`and `CAOpenGLLayer`provide FPS as high as the combination of `CVDisplayLink` and `NSOpenGLView`? I'm actually using the latter to achieve 60+ FPS, but I need to host a layer on top on the view in which I perform OpenGL drawings. Therefore I need to mix the pros of both `CAOpenGLLayer` and `NSOpenGLView` because I need to draw from a background thread and have layers on top of the OpenGL one. – Andrea3000 Jul 10 '12 at 20:11
  • 1
    @Andrea3000 - Nominally, they should have the same performance characteristics. I was able to update a CAOpenGLLayer at 60 FPS with no problem, but the issue I had encountered was that the standard update mechanism for that layer runs on the main thread. This would cause my video feed to pause while doing things like interacting with a menu. CAOpenGLLayer is perfectly capable of being updated quickly. – Brad Larson Jul 10 '12 at 20:15