How 'Geiss' Worked

Last Updated: Dec 20, 2022

From a graphics perspective, Geiss - both the screensaver and the Winamp plug-in version - worked by essentially a fast repetition of 2 simple steps:

  1. Draw some audio waveform into an image.
  2. Warp the image.
The first step is pretty cheap; the second step is the computationally expensive one. On modern PCs, the GPU can do this warp for you in its sleep (and in fact, this is how Milkdrop works). But this was more or less before GPUs existed.

To do such a warp, for each pixel in the new frame, you have to sample (read) a pixel in the old frame. But you don't want to just sample one pixel - that would make things look very super blocky. So instead you sample 2x2 pixels, computing a weighted average of the 4 pixel values, where the weights are determined by the ideal (sub-pixel) coordinates. (The technique is well-known and is called bilinear interpolation.) In Geiss, the weights summed to 256, and then a right-shift by 8 bits was used (to divide by 256 in a single clock cycle -- whereas "real" division is normally super slow).

Computing the warp coordinates and weights on the fly was too slow back then though. So the trick that Geiss used (admittedly inspired by many a demo from the incredible "demo scene" in Europe) was to precompute all of this. It would generate a big "warp map" with six bytes of information per pixel (IIRC): 2 bytes for the address (relative to the previous pixel's source sampling address) of the upper-left pixel in the source 2x2 pixel cluster we want, and 4 bytes containing the 4 weights.

Then, to perform the warp, for each destination pixel, you just read in the 6 bytes; update your source (read) pixel coordinate; sample the 4 pixels; multiply them by the weights; add them together; divide by 256 (using shift-right); and then write the new pixel value to the new frame (image) you want to show.

A few things made it really fast.
  • One was writing this core loop in inline assembly.
  • Another was unrolling the loop so that it processed 2 pixels per loop (to reduce the loop overhead), and interleaving the instructions a little bit in the middle.
  • I actually forgot about this until I recently looked at the code again, but I also used a little bit of on-the-fly assembly code hacking to make it all work. The problem was that there weren't enough registers to hold all the variables I needed to access, so I ended up breaking the main (assembly) inner loop into about 10 chunks, where the boundaries of the chunks were any place that I either needed to (a) hardcode some value (embedded in the instruction as a literal, instead of in a precious register), or (b) do any kind of jump (since the jump-to addresses would need to be updated if when we put these pieces together, dynamically). Then I stitched together the actual assembly code to run by fetching these ~10 blocks, manually overwriting the embedded literals where needed (including both variables and jump addresses).
  • And finally, adding a little bit of prefetch when reading the warp map (to look ahead ~64 or 128 bytes and get the CPU to fetch the data early -- before you actually need it) reduced the average latency of reads from memory, and resulted in another 30-40% speed gain. (This last one I had known nothing about, but two very nice gents from Cyrix (not AMD as I mistakenly wrote in my Reddit post) - Joe Kreiner (~dev rel) and Allen Hansen (~assembly) - reached out to me, taught me about it, and helped me implement it.)
There were actually 2 of these big warp maps. One was actively in use; the other was generated, a row at a time, in the background, while you watch the first one. Once it was done, it would wait for a good beat, and then it would suddenly switch to the new warp map, clear the old one, and start working on another new warp map in the background.

There was one more cool trick that I should share, though. When I shifted right, I didn't just throw away those lowest 8 bits -- I carried them to the next pixel (adding them to its sum). This helped avoid rounding error, and helped preserve “mass”, locally, in the image, more accurately over time. And visually, it introduced a cheap but effective kind of error-diffusion dithering that looked really nice - it kind of added a subtle texture or grain to the movement. I think this lent a higher quality to the look of the rendering in Geiss; in similar plugins, I never saw others use this trick.

Other fun history

I originally came up with the 'warping' part of Geiss in early 1998. I was trying to make a smoke effect, but I got lucky and it turned into something cooler.

The original version was just called "FX" (a placeholder name) and the earliest snapshot I have of it is a binary from March 6, 1998. If interested, you can check out the original FX source code or windows binary. There was no audio tie-in yet, and I think the only resolution supported was 320x240.

After that, I thought it would be really cool to add audio waveforms as the 'seed' for the effect. This wasn't exactly my original idea, though -- I had seen a program called Cthugha a few years prior which did something similar, and there were probably programs before Cthugha that did it, too. But I think Geiss did add a lot to this chain of progressively improving visualizers - in particular, high resolutions and framerates, and some nice new artistic elements.

Anyway, later that year I turned it into a screensaver and added audio input (drawing the waveform into the image). Around the same time, Winamp was just starting to turn into a big thing, and they had just opened up their visualization plug-in API, so I also modified the code to produce a Winamp plug-in version.

At some point, I was hanging out with my good friend Evan List, and lamenting the crappy name choice of "FX", andi told him I was struggling to come up with a better name for it. I then vividly remember him saying: "Dude... you should just call it Geiss! That's such a cool name!" So I renamed it. (And I do agree, I think it's a cool name).

At the time, I even remember thinking how cool it would be if someday, just one person recognized my name and asked me if I wrote this program. What I didn't know yet was that it would go viral, and that it would be a little embarrassing for me that I named it after myself. :P

[ return to the geiss main page ]

Ryan Geiss ( e-mail: ).