CHIP-8 port pt 3 - Chip8View.m

This view is going to do 3 things:

  1. Display the CHIP-8 framebuffer (on a timer) using OpenGL

  2. Owner the Chip8Machine object - allocate it and start the emulation

  3. Get key events and pass them to the Chip8Machine object


If you haven't already done this, go into the Chip8View.h file and make sure that Chip8View inherits from NSOpenGL View. I'll also make a few class variables that I'll use.

   #import <AppKit/AppKit.h>    
   #import <OpenGL/OpenGL.h>    
   #import <OpenGL/gl.h>    
   #import "Chip8Machine.h"

   @interface Chip8View : NSOpenGLView    
       NSTimer                *renderTimer;    
       Chip8Machine           *chip8;    
       unsigned char          *buffer8;    

   - (void)timerFired:(id)sender;    

Since we didn't use NSOpenGLView in our XIB file, we'll have to implement initWithFrame and initialize the NSOpenGLView there:

   - (id)initWithFrame:(NSRect)frame {

    NSOpenGLPixelFormatAttribute att[] =    
        NSOpenGLPFAColorSize, 24,    

    NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:att];    

    if(self = [super initWithFrame:frame pixelFormat:pixelFormat]) {    
           buffer8 = calloc( 64*32,1);    

           //a 16ms time interval to get 60 fps    
           renderTimer = [NSTimer timerWithTimeInterval:0.016    
           [[NSRunLoop currentRunLoop] addTimer:renderTimer forMode:NSDefaultRunLoopMode];    

    [pixelFormat release];    

    return self;    

Let's just make sure everything is working. Temporarily implement drawRect like this:

   - (void)drawRect:(NSRect)rect    
       [self.openGLContext makeCurrentContext];


       [self.openGLContext flushBuffer];    

Build and run the project - If everything is set up OK, then your window should be displayed and filled with a dark blue color. If you don't that, something went wrong with your project setup - double check the instructions, or compare against my final code.

Now that we know everything is working, I'm going to go modify drawRect to display the game's image. First this code will expand the 1 bit per pixel (BPP) game image to 8 BPP. Then it creates a texture. Finally it draws one textured quad that fills the view.

   - (void)drawRect:(NSRect)rect {    
       [self.openGLContext makeCurrentContext];    
       int i;

       //Convert the game's 1-bit bitmap into    
       // something OpenGL can swallow - in this case    
       // GL_LUMINANCE    
       uint8_t *b8 = buffer8;    
       uint8_t *fb = [chip8 framebuffer];    

       for (i=0; i < ((64/8)*32); i++)    
           uint8_t bw_pix = fb[0];    
           if (bw_pix & 0x80) b8[0] = 0xFF; else b8[0] = 0;    
           if (bw_pix & 0x40) b8[1] = 0xFF; else b8[1] = 0;    
           if (bw_pix & 0x20) b8[2] = 0xFF; else b8[2] = 0;    
           if (bw_pix & 0x10) b8[3] = 0xFF; else b8[3] = 0;    
           if (bw_pix & 0x08) b8[4] = 0xFF; else b8[4] = 0;    
           if (bw_pix & 0x04) b8[5] = 0xFF; else b8[5] = 0;    
           if (bw_pix & 0x02) b8[6] = 0xFF; else b8[6] = 0;    
           if (bw_pix & 0x01) b8[7] = 0xFF; else b8[7] = 0;    
           b8 += 8;    

       //Create the texture.  Have to set the FILTER so GL knows the    
       // texture is not mipmapped    
       glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 64,32, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, buffer8);    


       //Draw the textured quad    
           glTexCoord2f(0.0, 1.0);    
           glVertex2f(-1.0, -1.0);    
           glTexCoord2f(1.0, 1.0);    
           glVertex2f(1.0, -1.0);    
           glTexCoord2f(1.0, 0.0);    
           glVertex2f(1.0, 1.0);    
           glTexCoord2f(0.0, 0.0);    
           glVertex2f(-1.0, 1.0);    

       [self.openGLContext flushBuffer];    

In OpenGL, the following is true (as long as we don't change it):

  1. The view is set to have an orthographic 2D projection where the x and y coordinates are both have a range of -1.0 to 1.0.

  2. The GL_TEXTURE_ENV_MODE is GL_MODULATE. We could colorize the pixels by inserting a glColor3f() command just before the draw.


In the initWithFrame above, we allocated and started a timer. Make sure to implement timerFired to trigger a the view to redraw.

   - (void)timerFired:(id)sender    
       [self setNeedsDisplay:YES];    


The project should already be configured to pass keyboard events to this view. If we implement Cocoa's KeyDown and KeyUp they should get called properly. Here is KeyDown (KeyUp is similar):

   - (void) keyDown: (NSEvent *) event    
       NSString *characters;    
       characters = [event characters];

       unichar character;    
       character = [characters characterAtIndex: 0];    

       [chip8 KeyDown:character];    

We already implemented the rest of the keyboard handling in the machine object (and the emulator).

Odds and ends

In the code you download, I implemented reshape so the view always retains aspect and expands if you resize the window. Take a look at that if you're interested.

OK - that's it! If you get it all done just right, maybe you'll see something like this:

Chip 8 Maze

View my CHIP-8 emulator source for OS X, XCode 4.2, tested on OS X 10.7.

← Prev: chip-8-port-pt-2---machine-object   Next: 6502-emulator →