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!
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:
Create a buffer of memory
Make a CGBitmap around that buffer using CGBitmapContextCreate
Copy the game's bitmap into the memory buffer
Make a CGImage from the CGBitmap
Draw the CGImage into the view
Release the CGImage
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); }
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.
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.
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.
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.