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
Attribute | Meaning |
---|
NSOpenGLPFAAllRenderers | Boolean: Allow all available renderers. |
NSOpenGLPFADoubleBuffer | Boolean: Must be double buffered. |
NSOpenGLPFAStereo | Boolean: Must be stereo. |
NSOpenGLPFAAuxBuffers | Integer: Number of auxiliary buffers. |
NSOpenGLPFAColorSize | Integer: Depth in bits of the color buffer (default matches the screen). |
NSOpenGLPFAAlphaSize | Integer: Depth in bits for alpha in the color buffer. |
NSOpenGLPFADepthSize | Integer: Depth in bits for the depth buffer. |
NSOpenGLPFAStencilSize | Integer: Depth in bits for the stencil buffer. |
NSOpenGLPFAAccumSize | Integer: Depth in bits for the accumulation buffer (deprecated in OpenGL 3.x). |
NSOpenGLPFAMinimumPolicy | Boolean: Only buffers greater than or equal to the depths specified are considered. |
NSOpenGLPFAMaximumPolicy | Boolean: Use the largest depth values available of any buffers requested. |
NSOpenGLPFAOffScreen | Boolean: Use only renderers that can render off-screen. |
NSOpenGLPFAFullScreen | Boolean: Use only renderers that can render to a full-screen context. This flag implies the NSOpenGLPFASingleRenderer flag. |
NSOpenGLPFASampleBuffers | Integer: Number of multisample buffers. |
NSOpenGLPFASamples | Integer: Number of samples per multisample buffer. |
NSOpenGLPFAAuxDepthStencil | Standalone: Each auxiliary buffer has its own depth stencil. |
NSOpenGLPFAColorFloat | Boolean: Select a floating-point color buffer. |
NSOpenGLPFAMultisample | Boolean: Prefer multisampling. |
NSOpenGLPFASupersample | Boolean: Prefer supersampling. |
NSOpenGLPFASampleAlpha | Boolean: Update multisample alpha values. |
NSOpenGLPFARendererID | Integer: Use a specific renderer identified by the integer specified. |
NSOpenGLPFASingleRenderer | Boolean: Force a single renderer on a single monitor. |
NSOpenGLPFANoRecovery | Boolean: Forces continued rendering on a single context when resources have run out. Not generally useful. |
NSOpenGLPFAAccelerated | Boolean: Only select a hardware accelerated renderer. |
NSOpenGLPFAClosestPolicy | Boolean: Select the color buffer closest to the one specified. |
NSOpenGLPFARobust | Boolean: Select only renderers that do not have failure modes due to lack of resources. Not generally useful. |
NSOpenGLPFABackingStore | Boolean:
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. |
NSOpenGLPFAMPSafe | Boolean: Select a multiprocessor safe renderer. |
NSOpenGLPFAWindow | Boolean: Select only a renderer that can render to a window. |
NSOpenGLPFAMultiScreen | Boolean: Select only a renderer capable of driving multiple screens. |
NSOpenGLPFACompliant | Boolean: Use only OpenGL-compliant renderers. |
NSOpenGLPFAScreenMask | Integer: A bit mask of supported physical screens. |
NSOpenGLPFAPixelBuffer | Boolean: Allow rendering to a pixel buffer. |
NSOpenGLPFARemotePixelBuffer | Boolean: Allow rendering to an offline pixel buffer. |
NSOpenGLPFAAllowOffLineRenderers | Boolean: Allow offline renderers. |
NSOpenGLPFAAcceleratedCompute | Boolean: Select only renderers that also support OpenGL. |
NSOpenGLPFAVirtualScreenCount | Integer: 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.
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.
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!