Cocoa Port pt 3 - InvadersView

In the previous section we made the machine shell for the emulator. This section will write the code to put the game's image into the window. Let's do this in a few parts.

First, let's make sure everything is hooked up right in our project. Open the InvadersView.m file and add this routine:

   - (void)drawRect:(NSRect)dirtyRect    
   {    
       CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];    

       CGContextSetRGBFillColor (context, 0.0, 0.2, 0.5, 1.0);    
       CGContextFillRect(context, self.bounds);    
   }    

Now run your project. If you have the XIB and the view hooked up right, you should see a blue rectangle on your screen. If you see a window with just the solid gray background, you haven't constructed something correctly in your XIB file. Go back to the beginning of the Cocoa Platform section and double check all your steps. If you see the blue rectangle, continue on!

Drawing a raw memory bitmap using Core Graphics

The game image is drawn in black and white, one bit per pixel. We need to turn that image into something we can give to Core Graphics. If you can figure out how to make Core Graphics take a 1-bit bitmap you are more awesome than me. I make a 32bpp bitmap.

In 10.7, your display is in 32bpp. If you create a bitmap smaller than 32bpp, Core Graphics will almost certainly have to convert it in software. Since our colorspace comes from the device, and the depth of our bitmap matches the display, there is a chance that Core Graphics will not convert our bitmap again behind our backs. But it probably will.

Here is the algorithm:

  1. Create a buffer of memory

  2. Make a CGBitmap around that buffer using CGBitmapContextCreate

  3. Copy the game's bitmap into the memory buffer

  4. Make a CGImage from the CGBitmap

  5. Draw the CGImage into the view

  6. Release the CGImage

  7. Goto 3

The buffer and the CGBitmap stick around, and the CGImage continues to get released every frame. Here is the code for the bitmap handling:

   - (id)awakeFromNib    
   {    
    /*........*/    
           CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();    
           buffer8888 = malloc(4 * 224*256);    
           bitmapCtx = CGBitmapContextCreate(buffer8888, 224, 256, 8, 224*4, colorSpace, kCGImageAlphaNoneSkipFirst);    

    /*........*/    

       return self;    
   }    

   - (void)drawRect:(NSRect)dirtyRect    
   {    
       int i, j;    

       unsigned char *b = (unsigned char *)buffer8888;    
       unsigned char *fb = [invaders framebuffer];    
    /* code here to copy the game image into our bitmap*/    
        /*........*/    

       CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];    
       CGImageRef ir = CGBitmapContextCreateImage(bitmapCtx);    
       CGContextDrawImage(myContext, self.bounds, ir);    
       CGImageRelease(ir);    
   }    

Rotation

There is one more complication to deal with. The game's image is rotated 90 degrees in memory. There are a couple of ways we could handle that. One is we could ignore the rotation in our offscreen, then use a Core Graphics transform to rotate the CGImage when we draw. Two is to rotate it as we are transform it from black & white to 32bpp. I chose method number two. Here is that code:

       //Translate the 1-bit space invaders frame buffer into    
       // my 32bpp RGB bitmap.  We have to rotate and    
       // flip the image as we go.    
       //    
       unsigned char *b = (unsigned char *)buffer8888;    
       unsigned char *fb = [invaders framebuffer];    
       for (i=0; i < 224; i++)    
       {    
           for (j = 0; j < 256; j+= 8)    
           {    
               int p;    
               //Read the first 1-bit pixel    
               // divide by 8 because there are 8 pixels    
               // in a byte    
               unsigned char pix = fb[(i*(256/8)) + j/8];    

               //That makes 8 output vertical pixels    
               // we need to do a vertical flip    
               // so j needs to start at the last line    
               // and advance backward through the buffer    
               int offset = (255-j)*(224*4) + (i*4);    
               unsigned int*p1 = (unsigned int*)(&b[offset]);    
               for (p = 0; p < 8; p++)    
               {    
                   if ( 0!= (pix & (1<<p)))    
                       *p1 = RGB_ON;    
                   else    
                       *p1 = RGB_OFF;    
                   p1-=224;  //next line    
               }    
           }    
       }    

If you can't follow my convoluted algorithm to rotate the image, there are many ways to do it. Maybe just work it out for yourself and it will make more sense to you.

Draw on a timer

We are going to start a 60Hz timer in the init method of the NSView. Every time it gets called, we'll simply tell Cocoa we need to redraw. It will call our drawRect routine in the natural course of the application's run loop.

This is a chance for some latency - where the frame displayed may not be exactly the frame the game is working on. But, as long as the game plays OK, it is close enough for me. Fixing that problem can be an exercise for the reader!

Here is the whole final awakeFromNib routine that allocates the machine, makes the CGBitmap, starts the timer, and starts the emulation.


   - (void)awakeFromNib    
   {    
       invaders = [[SpaceInvadersMachine alloc] init];    

       CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();    
       buffer8888 = malloc(4 * 224*256);    
       bitmapCtx = CGBitmapContextCreate(buffer8888, 224, 256, 8, 224*4, colorSpace, kCGImageAlphaNoneSkipFirst);    

       //a 16ms time interval to get 60 fps    
       renderTimer = [NSTimer timerWithTimeInterval:0.016    
                                             target:self    
                                           selector:@selector(timerFired:)    
                                           userInfo:nil    
                                            repeats:YES];    

       [[NSRunLoop currentRunLoop] addTimer:renderTimer forMode:NSDefaultRunLoopMode];    
       [invaders startEmulation];    
   }    


   // Timer callback method    
   - (void)timerFired:(id)sender    
   {    
       [self setNeedsDisplay:YES];    
   }    


That's all there is to the timer. Cocoa makes timers really easy.

My Experience

When I was finally able to see my game running, I had all sorts of problems with my emulation even though I had made my 8080 emulator pass the CPUDiag test I mentioned before. I was able to work through some of the problems pretty easily. One problem was really hard, and I worked on it for almost a week before finally finding it. See the debugging section for details.

Source Code

My code up to this point is in the github project under CocoaPart3-Attract. It is a bundle of my platform, machine, and emulator code to this point. This project doesn't contain the ROM files, you'll have to find those and add them to the project yourself. When you do, this code should run the game in the Attract mode. Pretty exciting! Building the code will require XCode 7.

Use the code for reference, but please try to write your own. You may do things better than I do it, but you almost certainly won't learn if you don't try to do it yourself.

← Prev: cocoa-port-pt-2---machine-object   Next: debugging-tales →


Post questions or comments on Twitter @realemulator101, or if you find issues in the code, file them on the github repository.