MULTIMEDIA

OpenGL on OS X : Full-Screen Rendering

4/14/2013 7:28:03 PM

Many OpenGL applications need to render to the entire screen, rather than live within the confines of a window. This would include many games, media players, kiosk-hosted applications, and other specialized types of applications. One way to accomplish this is to simply make a large window that is the size of the entire display. Prior to OS X 10.6 (Snow Leopard), this was not the most optimal approach, and it was necessary to use the CGL functions to “capture” the display for full-screen rendering to get the best results.

With Snow Leopard, these APIs are still supported but are no longer necessary, and in fact the screen capturing technique is discouraged by Apple. When rendering to a full-screen window, you set a special context flag, and OS X automatically tries to optimize the rendering output in the manner that the old screen capturing technique did. However, by not capturing the display, critical UI messages or other windows are also allowed to pop up over the full-screen window. Capturing the display by modern standards is a bit heavy handed. There is even a simple way now to render into a smaller back buffer to improve fill performance without having to change the display resolution. Let’s start by creating a full-screen version of SphereWorld, SphereWorldFS.

Going Full-Screen with Cocoa

To begin our new version of SphereWorld, we again start with a brand new Xcode Cocoa project, which we call SphereWorldFS. Like in the previous example, we add the OpenGL framework, add the GLTools library, rename our .m files to .mm, and copy over the SphereWorldView Cocoa class and the SphereWorld.cpp rendering code along with the texture files that we add to the /Resources folder of the project. This time, however, we are not going to touch Interface Builder. Instead we are going to create and manage our window manually. The application delegate in a Cocoa-based program has a method called applicationDidFinishLaunching that is called as soon as your application has successfully launched. In our new project, this is located in the file SphereWorldFSAppDelegate.mm.

Selecting a Pixel Format

Before OpenGL can be initialized for a window, you must first select an appropriate pixel format. A pixel format describes the hardware buffer configuration for 3D rendering—things like the depth of the color buffer, the size of the stencil buffer, and whether the buffer is on-screen (the default) or off-screen. The pixel format is described by the Cocoa data type NSOpenGLPixelFormat.

To select an appropriate pixel format for your needs, you first construct an array of integer attributes. For example, the following array requests a double-buffered pixel format with red, green, blue, and alpha components in the destination buffer, a 16-bit depth buffer, and you want an accelerated pixel format, not the software OpenGL renderer. You may get other attributes as well, but you are essentially saying these are all you really care about:

NSOpenGLPixelFormatAttribute attrs[] = {

        // Set up our other criteria
        NSOpenGLPFAColorSize, 32,
        NSOpenGLPFADepthSize, 16,
        NSOpenGLPFADoubleBuffer,
        NSOpenGLPFAAccelerated,
        0
    };

Note that you must terminate the array with 0 or nil. Next, you allocate the pixel format using this array of attributes. If the pixel format cannot be created, the allocation routine returns nil, and you should do something appropriate because as far as your OpenGL rendering is concerned, it’s game over.

NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc]
                                         initWithAttributes:attrs];
    if(pixelFormat == nil)
        NSLog(@"No valid matching OpenGL Pixel Format found");

Most attributes are either a Boolean flag or contain an integer value. The Boolean flags set the attribute by simply being present, for example, NSOpenGLPFADoubleBuffer in the preceding example. An integer flag on the other hand, such as NSOpenGLPFADepthSize, is expected to be followed by an integer value that specifies the number of bits desired for the depth buffer. The available attributes and their meanings are listed in Table 1.

Table 1. Cocoa Pixel Format Attributes
AttributeMeaning
NSOpenGLPFAAllRenderersBoolean: Allow all available renderers.
NSOpenGLPFADoubleBufferBoolean: Must be double buffered.
NSOpenGLPFAStereoBoolean: Must be stereo.
NSOpenGLPFAAuxBuffersInteger: Number of auxiliary buffers.
NSOpenGLPFAColorSizeInteger: Depth in bits of the color buffer (default matches the screen).
NSOpenGLPFAAlphaSizeInteger: Depth in bits for alpha in the color buffer.
NSOpenGLPFADepthSizeInteger: Depth in bits for the depth buffer.
NSOpenGLPFAStencilSizeInteger: Depth in bits for the stencil buffer.
NSOpenGLPFAAccumSizeInteger: Depth in bits for the accumulation buffer (deprecated in OpenGL 3.x).
NSOpenGLPFAMinimumPolicyBoolean: Only buffers greater than or equal to the depths specified are considered.
NSOpenGLPFAMaximumPolicyBoolean: Use the largest depth values available of any buffers requested.
NSOpenGLPFAOffScreenBoolean: Use only renderers that can render off-screen.
NSOpenGLPFAFullScreenBoolean: Use only renderers that can render to a full-screen context. This flag implies the NSOpenGLPFASingleRenderer flag.
NSOpenGLPFASampleBuffersInteger: Number of multisample buffers.
NSOpenGLPFASamplesInteger: Number of samples per multisample buffer.
NSOpenGLPFAAuxDepthStencilStandalone: Each auxiliary buffer has its own depth stencil.
NSOpenGLPFAColorFloatBoolean: Select a floating-point color buffer.
NSOpenGLPFAMultisampleBoolean: Prefer multisampling.
NSOpenGLPFASupersampleBoolean: Prefer supersampling.
NSOpenGLPFASampleAlphaBoolean: Update multisample alpha values.
NSOpenGLPFARendererIDInteger: Use a specific renderer identified by the integer specified.
NSOpenGLPFASingleRendererBoolean: Force a single renderer on a single monitor.
NSOpenGLPFANoRecoveryBoolean: Forces continued rendering on a single context when resources have run out. Not generally useful.
NSOpenGLPFAAcceleratedBoolean: Only select a hardware accelerated renderer.
NSOpenGLPFAClosestPolicyBoolean: Select the color buffer closest to the one specified.
NSOpenGLPFARobustBoolean: Select only renderers that do not have failure modes due to lack of resources. Not generally useful.
NSOpenGLPFABackingStoreBoolean: Select only a renderer with a back buffer equal in size to the front buffer. Additionally, guarantees the back buffer’s contents remain intact after the flushBuffer call.
NSOpenGLPFAMPSafeBoolean: Select a multiprocessor safe renderer.
NSOpenGLPFAWindowBoolean: Select only a renderer that can render to a window.
NSOpenGLPFAMultiScreenBoolean: Select only a renderer capable of driving multiple screens.
NSOpenGLPFACompliantBoolean: Use only OpenGL-compliant renderers.
NSOpenGLPFAScreenMaskInteger: A bit mask of supported physical screens.
NSOpenGLPFAPixelBufferBoolean: Allow rendering to a pixel buffer.
NSOpenGLPFARemotePixelBufferBoolean: Allow rendering to an offline pixel buffer.
NSOpenGLPFAAllowOffLineRenderersBoolean: Allow offline renderers.
NSOpenGLPFAAcceleratedComputeBoolean: Select only renderers that also support OpenGL.
NSOpenGLPFAVirtualScreenCountInteger: The number of virtual screens required.

The Full-Screen App Core

Now let’s take a look at what is essentially the main program body for the full-screen version of SphereWorld. Listing 1 shows the entire applicationDidFinishLaunching implementation.

Listing 1. Creating and Managing Our Full-Screen Window
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {

    NSOpenGLPixelFormatAttribute attrs[] = {

        NSOpenGLPFAFullScreen,  1,// Full Screen context flag

        // Which screen do we want to appear on (you must do this for
        // full screen contexts)
        NSOpenGLPFAScreenMask,
               CGDisplayIDToOpenGLDisplayMask(kCGDirectMainDisplay),

        // Set up our other criteria
        NSOpenGLPFAColorSize, 24,
        NSOpenGLPFADepthSize, 16,
        NSOpenGLPFADoubleBuffer,
        NSOpenGLPFAAccelerated,
        0
    };


    NSOpenGLPixelFormat* pixelFormat =
                [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs];
    if(pixelFormat == nil)
        NSLog(@"No valid matching OpenGL Pixel Format found");


    NSRect mainDisplayRect = [[NSScreen mainScreen] frame];
    NSWindow *pMainWindow =
        [[NSWindow alloc] initWithContentRect: mainDisplayRect
        styleMask:NSBorderlessWindowMask
        backing:NSBackingStoreBuffered defer:YES];

    [pMainWindow setLevel:NSMainMenuWindowLevel+1];
    [pMainWindow setOpaque:YES];
    [pMainWindow setHidesOnDeactivate:YES];


    NSRect viewRect = NSMakeRect(0.0, 0.0,
         mainDisplayRect.size.width, mainDisplayRect.size.height);
    SphereWorldView *fullScreenView =
         [[SphereWorldView alloc] initWithFrame:viewRect
         pixelFormat: [ pixelFormat autorelease] ];
    [pMainWindow setContentView: fullScreenView];
    [pMainWindow makeKeyAndOrderFront:self];

    // Hide the cursor
    CGDisplayHideCursor (kCGDirectMainDisplay);

    bool bQuit = false;
    while(!bQuit) {
        // Check for and process input events.
        NSEvent *event;
        event = [NSApp nextEventMatchingMask:NSAnyEventMask
           untilDate:[NSDate distantPast]
           inMode:NSDefaultRunLoopMode dequeue:YES];
        if(event != nil)
            switch ([event type]) {
                case NSKeyDown:
                    [fullScreenView keyDown:event];

                    if((int)[[event characters]
                       characterAtIndex:0] == 27) // ESC Exits
                       bQuit = true;
                    break;

                case NSKeyUp:
                    [fullScreenView keyUp:event];
                break;

                default:
                    break;
            }

        [fullScreenView drawRect:viewRect];
    }

    // Show the cursor again
    CGDisplayShowCursor(kCGDirectMainDisplay);

    // Terminate the application
    [NSApp terminate:self];
    }

					  

The first order of business is to create a full-screen pixel format. Note the use of the flag NSOpenGLPFAFullScreen and the accompanying NSOpenGLPFAScreenMask. You must use these two flags together to get a valid pixel format for a full-screen context and get the full benefit of Snow Leopard’s ability to optimize the rendering for full-screen windows.

Second, we create a main window that is the size of the current desktop. Here we use the NSBorderlessWindowMask to eliminate the caption, minimize buttons, and so on.

NSRect mainDisplayRect = [[NSScreen mainScreen] frame];
    NSWindow *pMainWindow =
        [[NSWindow alloc] initWithContentRect: mainDisplayRect
        styleMask:NSBorderlessWindowMask
        backing:NSBackingStoreBuffered defer:YES];

A couple of other settings also come in useful for a true full-screen experience. We set the window level to be above the menu bar, make sure the window is not transparent, and set the setHideOnDeactivate flag. This hides the window whenever you switch away from the window and then automatically restores the full-screen status when you reactivate the application.

[pMainWindow setLevel:NSMainMenuWindowLevel+1];
[pMainWindow setOpaque:YES];
[pMainWindow setHidesOnDeactivate:YES];

Next we create the actual OpenGL-based view based on our previously constructed SphereWorldView class. This view is assigned to the main window, which is then activated and displayed. Although Interface Builder gives us some control over the pixel format, the option to create your NSOpenGLView classes with the initWithFrame method gives you the ultimate control and flexibility.

NSRect viewRect = NSMakeRect(0.0, 0.0,
         mainDisplayRect.size.width, mainDisplayRect.size.height);
    SphereWorldView *fullScreenView =
         [[SphereWorldView alloc] initWithFrame:viewRect
         pixelFormat: [ pixelFormat autorelease] ];
    [pMainWindow setContentView: fullScreenView];
    [pMainWindow makeKeyAndOrderFront:self];

Once we have created the full-screen window, we usually do not need to display the mouse cursor. The Core GL (CGL) method CGDisplayHideCursor hides the cursor for us until either the application terminates or we call the corresponding CGDisplayShowCursor.

CGDisplayHideCursor(kCGDirectMainDisplay);

For a full-screen window, we need to process the Cocoa event loop ourselves. The NSApp method nextEventMatchingMask retrieves the latest event and removes it from the event queue. In the case of an NSKeyDown event, we forward it to the SphereWorldView class, which checks for arrow keys to facilitate camera movement. We also check here for the escape key, which if pressed changes the value of bQuit to true, terminating the event loop and finally terminating the application.

[NSApp terminate:self];

View Class Changes

There is one additional change we need to make to the SphereWorldView class when using our own pixel format in full-screen mode. At the end of the drawRect method, we replace glFlush with a call to flushBuffer on the OpenGL context:

[[self openGLContext ]flushBuffer];

This performs the equivalent of a buffer swap for our double-buffered renderer. Because we are no longer in “windowed” mode, just flushing the command buffer is not enough; we also need the buffer swap, which now is actually taking place.

Given that we have gone full-screen, we’ve also thrown in a few other goodies. For one, we ported the GLString class from one of the Apple OpenGL demos (Apple’s own CocoGL demo) to use only the new OpenGL core profile and the stock shaders in GLTools. We use this in the SphereWorldView class to calculate and display the frame rate of our new full-screen SphereWorld, which is running unrestrained as fast as it can. Figure 1 shows our SphereWorld running at 199 frames per second.

Figure 1. SphereWorld with fps display.


Finally, we also modified our keyboard handling of movement to smooth things out. Instead of moving the camera when a key is pressed, we set a flag for each movement key to true when the key is pressed and to false when the key is released. In the render function, we then move based on the state of those flags. This keeps movement smooth and less jerky as a result of the latency inherent in keyboard messages. Compare the navigation between SphereWorldFS and the original SphereWorld in a window yourself!

Other  
 
Most View
Acer Aspire R7 - A Flexible Form Factor With A Reasonable Price (Part 2)
Windows 7 : Troubleshooting and Repairing Problems - Easy Repair Options at Boot Time, Startup Repair
Gaming Headset Shootout (Part 1) : Asus republic of gamers Orion Pro, Roccat Kave 5.1
Epson Moverio BT-100 – “Bring The Future One Step Closer”
Fujifilm X-E1 - Is This The Perfect Enthusiast CSC? (Part 2)
Crucial Adrenaline 50GB - Undeniably A Fine Drive
LINQ to Objects : How to Join with Data in Another Sequence (part 1) - Cross Joins
Sony VAIO Tap 11 Ultrabook - Sleek And Sexy
Music And More To Go
Windows 7 : Understanding Libraries (part 1) - Virtual Folders 101, Libraries and Windows 7
Top 10
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 4) - Preventing Internal Servers from Being Filtered
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 3) - Defining Block List Exceptions and Global Allow/Block Lists
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 2) - Filtering Connections with IP Block Lists
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 1) - Filtering Spam and Other Unwanted E-Mail by Sender, Filtering Spam and Other Unwanted E-Mail by Recipien
Microsoft Exchange Server 2010 : Creating and Managing Remote Domains (part 3) - Configuring Messaging Options for Remote Domains , Removing Remote Domains
Microsoft Exchange Server 2010 : Creating and Managing Remote Domains (part 2) - Creating Remote Domains
Microsoft Exchange Server 2010 : Creating and Managing Remote Domains (part 1) - Viewing Remote Domains
Microsoft Exchange Server 2010 : Creating and Managing E-Mail Address Policies (part 3) - Editing and Applying E-Mail Address Policies , Removing E-Mail Address Policies
Microsoft Exchange Server 2010 : Creating and Managing E-Mail Address Policies (part 2) - Creating E-Mail Address Policies
Microsoft Exchange Server 2010 : Creating and Managing E-Mail Address Policies (part 1) - Viewing E-Mail Address Policies