2006/02/27
Quartz Musings (IV): Bitmap & Textures
CGBitmapContext has some common point with CGLayer, but is more specialized: drawing into a bitmap (with associated colorspace, bits depth, bytes per row, etc). CGLayer is more a generic drawing cache, optimized for a given context type. It's easy to reuse a CGLayer by drawing it to a graphic context. And it's easy to use a bitmap context to generate a CGImage (the generic CoreGraphics image abstraction), or keep control on its data buffer.
So, what could we do with a CGBitmapContext and control over its buffer ? A texture. Let's build another way of using the same drawing code.
- Create a buffer
- Create a bitmap context using the buffer as raw storage
- Draw on the context
- Bind an OpenGL texture on the buffer
- Draw it through OpenGL
-(id)initWithCoder:(id)o
{
[super initWithCoder:o];
[self setPixelFormat: [self myPixelFormat]] ;
// Additional setup code
return self;
}
Nothing notable in the pixel format and OpenGL setup: double buffered (I won't run full screen, no justification for direct mode), 16 bits color, enabled for texture.
- (NSOpenGLPixelFormat*)myPixelFormat
{
NSOpenGLPixelFormatAttribute pixelAttribs[ 4 ];
int pixNum = 0;
pixelAttribs[ pixNum++ ] = NSOpenGLPFADoubleBuffer;
pixelAttribs[ pixNum++ ] = NSOpenGLPFAColorSize;
pixelAttribs[ pixNum++ ] = 16;
pixelAttribs[ pixNum ] = 0;
return [[[NSOpenGLPixelFormat alloc] initWithAttributes:pixelAttribs]
autorelease];
}
-(void)prepareOpenGL
{
glEnable( GL_TEXTURE_2D );
glShadeModel( GL_SMOOTH );
glClearColor( 0.0f, 0.0f, 0.0f, 0.5f );
glGenTextures(1, &texture );
}
Now, the real thing: generating the texture for a given width and height. As I am using a GL_TEXTURE_2D,
WIDTH and HEIGHT must be equal and a power of 2. The important point here is to use compatible bitmap format
between the CGBitmapContext creation and the glTexImage2D parameters.
- (void)generateTexture
{
CGRect r = CGRectMake(0,0,WIDTH, HEIGHT);
if (!setup)
{
data = malloc( r.size.width * r.size.height * 4);
cs = CGColorSpaceCreateDeviceRGB();
context = CGBitmapContextCreate(data, r.size.width, r.size.height,
8, r.size.width * 4, cs, kCGImageAlphaPremultipliedFirst);
setup = TRUE;
}
// My rendering code, using context
CGContextFlush(context);
glBindTexture(GL_TEXTURE_2D, texture );
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, r.size.width, r.size.height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
}
The drawing code: update the texture on each frame, clear everything, bind the texture and
finally draw it.
- (void) drawRect:(NSRect)rect
{
[self generateTexture];
GLfloat texturewidth = 1.0f;
GLfloat textureheight = 1.0f;
glBindTexture( GL_TEXTURE_2D, texture );
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex2f(-1.0f, 1.0f);
glTexCoord2f(0.0f, textureheight );
glVertex2f(-1.0f, -1.0f);
glTexCoord2f(texturewidth, textureheight );
glVertex2f(1.0f, -1.0f);
glTexCoord2f(texturewidth, 0.0f );
glVertex2f(1.0f, 1.0f);
glEnd();
[[self openGLContext] flushBuffer ];
}
It is actually possible to do better, and easier. First by using the rectangle texture extension, which work on all modern cards which allows for rectangular and non power of 2 sizes. To use it, we need to replace all GL_TEXTURE_2D with GL_TEXTURE_RECTANGLE_EXT. There is another trick actually: texture coordinates must be expressed in pixels and not in normalized coords, so the texturewidth and textureheight variable affectations must be replaced with:
GLfloat texturewidth = WIDTH; GLfloat textureheight = HEIGHT;The other trick we can use is OpenGL Apple extensions to improve texture upload performance. There is an interesting sample code describing this. The first extension is GL_UNPACK_CLIENT_STORAGE_APPLE which avoid one data copy if the application retains the data, that's the case here (we don't recreate the bitmap context and its buffer on each frame). The other trick is a hint passed to the system, telling that you want VRAM or AGP texturing, GL_STORAGE_CACHED_APPLE is good for textures cached and reused, and GL_STORAGE_SHARED_APPLE works best for one-shot textures, what we have here.
I just have to insert before glTexImage2D in the texture generation code the following snippet (it also works for GL_TEXTURE_2D):
glTexParameteri(GL_TEXTURE_RECTANGLE_EXT,
GL_TEXTURE_STORAGE_HINT_APPLE, GL_STORAGE_SHARED_APPLE);
glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE);
Some measures. With these OpenGL extensions, we're more than tripling the performance !
| Regular CGContext, CGLayer | 178 |
| CGBitmapContext, CGLayer, OpenGL texture | 63 |
| CGBitmapContext, CGLayer, OpenGL texture (RECT, CACHED) | 211 |
We must also remember that it's not only texture upload and drawing we're measuring but also the rendering in the bitmap context. If the bitmap context rendering takes too much time, the OpenGL texture upload might not be competitive enough.
But of course, what is really interesting here is that from this point, I'm directly
dealing with OpenGL, so it's a piece of cake to achieve some fun effects, for instance
by mapping my rendering on a cube, etc.
But there's other ways to apply effects on a rendering on OS X, easier than raw OpenGL programming. More about that later.