Playing Bad Apple on Y Ddraig
Why Bad Apple? #
I’ve seen the Bad Apple demo in the past and it’s been ported to quite a few different computer systems. I recently saw the Atari ST version running again with the smooth video and started wondering what it would take to get it running on my computer. The fact that it has been ported to so many systems, and that the Atari ST version is also a 68000 system, made it a particularly interesting target for Y Ddraig, my homebrew 68000-based computer.
The project started after looking through the source of Fenarinarsa’s Atari STE version. Unfortunately the STE and Y Ddraig are very different machines and the STE seems to be using a blitter which Y Ddraig lacks, but the Atari version demonstrated that a 68000-class system could produce a convincing result with careful preparation of the video data.
My first goal was deliberately simple, display a short, monochrome section of the animation at 320x200 and 30 frames per second. The important question was whether the computer could read and display the data quickly and consistently enough.
Starting with raw monochrome frames #
A 320x200 1-bit image occupies 8,000 bytes. At 30 frames per second, an uncompressed player needs to move about 240 KB every second. That seems a reasonable figure, but Y Ddraig has no system DMA controller so the 68000 is responsible for disk access, copying data and controlling the video hardware.
The first C prototype read complete frames from disk and copied them into the GfxVGA card’s video memory. Assembly routines were used where possible to speed up the data handling. Additionally, the player recorded detailed timing statistics after each run. The disk-read time, video-memory copy time, missed frame deadlines, and the worst frame in the sequence.
Disk performance was not very good but this is an OS level function. Some changes were made to improve the operating system’s disk transfer loop, and a 450-frame raw test ran without missing a timing deadline. That proved the basic idea, although raw video would have made the complete animation unnecessarily large and would not leave much room for higher-quality video or, eventually, music.
Timing statistics #
One thing I was starting to struggle with on the timing statistics was that it was hard to measure accurately in less than 10ms intervals. Up until now I was using the OS “tick count” or using the 60hz vertical blank as a counter. For this I wanted to see if I could get some more accurate timing information. The solution here was to repurpose the MC68230 PIT that the OS was using for the tick timer.
For profiling, the player configures it as a free-running counter using the 68230’s 10 MHz timer clock with a divide-by-32 prescaler. Each count is therefore 3.2 microseconds. The player samples the counter before and after a disk read, packet upload or decode operation, then reports average and worst-case measurements to the nearest tenth of a millisecond.
Sending only what changed #
The Bad Apple video data is well suited to delta compression. Large parts of the screen remain black or white, and many areas do not change at all between nearby frames.
The player uses two alternating framebuffers. While one is visible, the next frame is prepared in the other. When the player returns to a buffer, it still contains the picture from two frames earlier. The encoded frame therefore only needs to describe the difference between the new picture and that older one.
The resulting BA video format contains a small set of word-based commands:
- skip an unchanged area;
- copy a sequence of new words;
- fill an area with zero;
- fill an area with a repeated word or pixel pattern.
This reduced the disk traffic dramatically and frame performance improved as a result.
The first delta decoder ran entirely on the 68000. It worked, but complicated frames could take long enough to miss their display time. An interesting lesson from the tests was that the smallest file was not always the fastest file. Combining many tiny changes into slightly larger blocks increased the amount read from disk but greatly reduced the number of commands the CPU had to interpret.
Giving the video card a decoder #
The GfxVGA video card is FPGA-based, has its own SRAM and already has a command engine for operations such as drawing, filling and copying memory, so the next step was to add a small, general-purpose DMA/RLE operation for BA video data.
The encoded data for the frame is copied to the Video RAM and the decoder simply reads the word commands, skips, copies or fills the requested region of memory, and stops if either the source packet or destination would be overrun. That makes it potentially useful for other prepared bitmap animations, loading screens and compressed graphics.
Adding the hardware decoding changed the performance of the player drastically. Decoding a typical frame takes less than half a millisecond but copying each compressed packet from ordinary RAM into the card could take much longer. The decoder itself was no longer the bottleneck, more the data copy.
Removing an unnecessary copy #
The hardware decoding helped speed things up but there were still missed frames. Running the video in 1-bpp mode was fine but 4-bpp required some optimisation on the video encoding. Video could only use 4 shades of colour to keep the frame size low enough for streaming.
The data was being loaded into the computer SRAM and copied into the VRAM for decoding, that adds an extra memory copy stage that isn’t really needed. The video RAM is memory mapped so the fastest playback path now reads aligned disk sectors directly into a queue in the video card’s SRAM. Only packet lengths and partial sectors need to pass through a small buffer in normal memory. In effect, the data moves from disk to its final staging area without making an additional trip through the host RAM.
This was the change that made the more demanding modes practical. On the complete 320x200, 16-shade animation, the player averages roughly 117 KiB/s from disk, while the FPGA decoder averages about 0.4 ms per frame. The queue absorbs occasional slower reads, so all 6,536 frames can be presented without a missed deadline.
The instrumentation statistics remained valuable even after playback appeared smooth. One version produced occasional flickering pixels outside the video area and sometimes stopped with a decoder error. Because the fault occurred at the same places every time, it looked more like a parser problem than random data corruption.
The CPU decode path was still present in the code, and it displayed the video correctly, albeit slowly, so a special comparison mode decoded every frame twice, once with the CPU decoding routine and once in the FPGA. It compared the complete framebuffer and reported the first differing word and the command responsible. The actual problem turned out to be marginal SRAM read timing in the FPGA decode read interface. Adding one wait state fixed the fault, after which all 6,536 CPU and FPGA results matched exactly.
Resolution versus shades #
Several display formats were tested. A full 640x400 1-bit version looks sharp and also completes without missing a frame. However, monochrome conversion creates visible dithering around shaded and antialiased areas. Those patterns also change many pixels from frame to frame, which is not ideal for the delta compression.
The final choice is 320x200 chunky 4-bit video, centred in a 320x240 display. It provides 16 shades of grey and preserves much more of the antialiasing and soft gradients in the source. Although the nominal resolution is lower, to me it looks like the better version in motion.
The higher-resolution mode remains a useful demonstration of the available bandwidth, while the shaded mode gives the best balance of picture quality, file size and spare processing time for sound.
Completing the demo #
The music and video came from different versions of the demo, video frames from the Atari ST version and music from the C64 version, so I expected the synchronisation to need more work. The music required some retiming and fitting into the demo but it matches remarkably well.