Diplograph

Turning ZSNES Raw Videos into an Image Sequence

September 2010

I found myself needing some images of old videogame characters for a silly side project I'm working on. I don't trust the random sprite sheets on the internet, mainly because I don't trust some forum poster named xxYoShIDuDe64.

The emulator I like, ZSNES, can record gameplay movies. This is awesome because it means I can concentrate on doing something in the game and then replay it later to copy out whatever bits I'm interested in.

ZSNES doesn't have an option to dump a movie as a series of images, and while the lossless movie formats might be considered a step in the right direction they require MEncoder to be installed. For some reason MEncoder and I don't get along very well, and these days I passively loathe it with the force of a thousand "whatever"s.

What ZSNES does have is a "Raw Video" option, which creates a large, mysterious rawvideo.bin file. It's actually a really simple pixel dump, so lacking more useful things to do with my life I whipped up a short program to take the video and save it out as a sequence of PNG images. It uses Foundation and Core Graphics, it's only 64 lines long, and since you've got nothing better to do either let's talk about it.

It's actually awesome how C is sometimes the right language to write a small "script" in.

Reading the Frame

There are really two steps we need to do for each frame in the movie. First, we read in a frame and twiddle things around into a format we can use.

Yes, the movie dump file format really is that simple. There's no header information, no compression, no frame markers, no metadata whatsoever. It's just pure, delicious pixels.

fread(frameBufferBGR, 1, FRAME_SIZE * 3, rawMovieFile);
for (unsigned pixelIndex = 0; pixelIndex < FRAME_SIZE; pixelIndex++) {
    frameBufferRGBA[pixelIndex * 4] = frameBufferBGR[pixelIndex * 3 + 2];
    frameBufferRGBA[pixelIndex * 4 + 1] = frameBufferBGR[pixelIndex * 3 + 1];
    frameBufferRGBA[pixelIndex * 4 + 2] = frameBufferBGR[pixelIndex * 3];
}

Unfortunately we can't use the frame data right away. The movie dump is in 24-bit BGR, or eight bits of blue, eight bits of green, and eight bits of red in that order. We want to turn that into 32-bit RGBA.

This is how the bits are arranged in the movie file.

And this is how we want them to look.

It's pretty simple to make a copy with the channels in the right order.

CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bitmapContext = CGBitmapContextCreate(frameBufferRGBA, 
                                                   FRAME_WIDTH,
                                                   FRAME_HEIGHT,
                                                   8,               // bits per pixel
                                                   4 * FRAME_WIDTH, // pixels per row
                                                   rgbColorSpace,
                                                   kCGImageAlphaNoneSkipLast);
CGImageRef frameImage = CGBitmapContextCreateImage(bitmapContext);
In the actual code the bitmap context is created outside the loop and reused. It's really not the best idea, but here we can just keep writing into the backing buffer.

Next we move the pixels into Core Graphics. First we create a CGBitmapContext, a thing that we can "draw" into. In this case we've already drawn into it because we ask it to use the RGBA pixel buffer we made earlier as a backing store. kCGImageAlphaNoneSkipLast tells CoreGraphics to ignore the alpha channel, the last byte of each pixel. We don't care about it here.

Then we take the bitmap context and create a CGImage from that.

Saving the Frame

CFStringRef outputFilePath = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%06d.png"), frameIndex);
CFURLRef outputFileURL = CFURLCreateWithFileSystemPath(NULL, outputFilePath, kCFURLPOSIXPathStyle, 0);
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithURL(outputFileURL, kUTTypePNG, 1, NULL);
CGImageDestinationAddImage(imageDestination, frameImage, NULL);
CGImageDestinationFinalize(imageDestination);

We're in the home stretch now. We figure out what we want to call the image, create a CGImageDestination which will encode the image as a PNG and save it out to a file for us, hand it the image, and then clean things up.

That's it! That's the entire guts of the program.


Look, the code is actually terrible. I'm not doing any error checking, and it will happily dump millions of files in your working directory without so much as blinking. It's leak free, works great even on long movies, and is actually really fast, but I'm only going to put up source code. Download it, build it, please use it if you like, but I'm hoping you know what you're doing first. It uses APIs only available on Mac OS X, and the format of the raw video is of course subject to change at any time by the ZSNES developers.

I've spent more time writing this blog post than I did writing the program, which means it's time to go to bed.

Download
ZSNESMovieDumper Source Code