Eric Graham's 1987 Amiga Juggler raytracer (Part 2): Porting to SDL
In Part 1 of this blog series, we recovered the disk image, extracted the source files, and received permission to publish Eric Graham's 1987 Juggler raytracer source.
At that point, we have a snapshot of history that can be viewed from afar, like a dusty post-mortem report of an adventurous polar expedition. But I wanted to breathe life back into the Eric Graham raytracer, like getting Ken Burns to make a cool documentary with old images brought back to motion.
So my next goal was to get the original C raytracer compiling on a modern Windows, Linux, or MacOS machine, while changing the original source as little as possible so that it still was an original reference to personal-computer raytracing in about 1987.
That sounds easy, but the original source depends on the Amiga graphics environment. The raytracer did not write a portable output image file (even in an old format like TGA or IFF-ILBM). It opened Amiga libraries, created a HAM custom screen, set Amiga palette registers, selected pens, and wrote pixels through Amiga display calls. So the question became: how much Amiga do I need to emulate? Clearly some of Intuition.library and Graphics.library and hopefully not much or either of them and not much more. There's a plot twist in here too (like the Franklin expedition's unknowing lead poisoning...) in that the behavior of the 12-bit per pixel HAM/6 display encoding method is kinda voodoo ( https://en.wikipedia.org/wiki/Hold-And-Modify ) and is implemented by the Denise display chip of the Amiga's hardware -- which no non-Amiga system implements.
To be transparent from the start, the code authoring below is almost entirely the work of OpenAI Codex (see my AI/LLM Declaration in the repository's README.md). I was the one planning and specifying the project, but I didn't write or port the code from scratch. I know I could, I just didn't have the quantity of free time to spend on it.
The original code landscape
The original C source is split across three files:
rt1.c is the raytracer. It does the ray generation, sphere intersection, sky gradient, ground plane, lighting, highlights, and vector math.
rt2.c sets up the observer and the simple one-sphere scene (it doesn't actually render the famous "Juggler" image, though it includes a proto-data-file for it), then converts RGB brightness values into HAM color components.
rt3.c is the Amiga display code. It opens graphics.library, intuition.library, and dos.library, creates the screen and window, manages color registers, and writes pixels.
This was incredibly fortunate. It meant I could leave the raytracing code mostly alone (beyond fixing K&R C grammar/dialect issues) and concentrate the modern platform work around rt3.c.
I'm not trying to rewrite the raytracer into a modern graphics app. That would be easier in some ways, but it would also destroy the useful relationship between the published source and the working version. I wanted a modern operational equivalent, with diffs that still made sense against the original code, and that could be examined against the descriptions in the original Eric Graham AmigaWorld article.
Making K&R C acceptable to modern C++
The source was written for a K&R-era C compiler. Modern C++ compilers do not accept that style directly.
The minimal cleanup was:
- add a shared
rt.hto handle typedefs, structs, function prototypes, etc. - move duplicated structs and constants into that header
- add function prototypes
- convert K&R function definitions into typed parameter lists
- add standard headers such as
math.h,stdio.h, andstdlib.h - add a few explicit casts and returns where modern compilers require them
One small annoyance was labels at the end of blocks. Old C code can get away with a label immediately before a closing brace. Modern compilers won't allow this. A label has to label a statement, and a brace is not a statement. The fix was just a null statement:
cont:
;
That is ordinary porting work. It is a long chain of small decisions where each change should be boring and explainable.
Replacing the Amiga display system
The Amiga side needed a moderate shim.
I added small local headers under src/exec/ and src/intuition/ so as to satisfy the old include statements without changing the C source. They define only the types and functions this program uses but they are not an Amiga SDK replacement.
The minimal emulation implementation lives in src/emulate-amiga.c. It provides functions with familiar Amiga names:
OpenLibrary()OpenScreen()OpenWindow()CloseWindow()ViewPortAddress()SetRGB4()SetAPen()WritePixel()
Behind those calls, the shim uses SDL.
At first I wired this for SDL3. Later I added a CMake option to build against SDL2 as well for those who don't have SDL3 as an option for some reason (looking at you Ubuntu 24):
cmake -S . -B build -DJUGGLER_SDL_VERSION=3
cmake -S . -B build-sdl2 -DJUGGLER_SDL_VERSION=2
SDL3 remains the default. The vcpkg manifest uses features, so selecting SDL2 installs SDL2 and selecting SDL3 installs SDL3. It does not install both unless someone asks for both.
HAM mode
The original display code targets Amiga HAM mode.
HAM is awkward because the meaning of a pixel can depend on the pixel to its left. A pixel can either select one of 16 palette registers or completely substitute-modify one channel (R, G or B) from the previous pixel. That behavior was implemented by the Amiga display hardware and is an absolutely brilliant hack that allows for lower-bandwidth chroma-encoding. It effectively encodes a 12-bit per pixel display in 6 bits per pixel in order to live within the constraints of the Amiga's chipset bandwidth, at the cost of not always being able to achieve the exact color you want at every pixel ("HAM fringing"). No other personal computer could put a 12-bit-per-pixel nominally truecolor (4096 possible colors onscreen at once) display up in 1985. Later advancements like palette per-scanline vertical slicing, aka "sliced HAM" or "SHAM" https://handwiki.org/wiki/Hold-And-Modify#Sliced_HAM_mode_(SHAM) could make the same hardware able to achieve even greater color accuracy and reduced fringing on the same 1985 chips. But it makes encoding (and in our case, decoding) HAM image data complex.
The shim keeps a 6-bit HAM buffer and a secret RGB buffer. When rt3.c calls SetAPen() and WritePixel(), the shim stores the HAM pen value, interprets it using the previous pixel in the scanline, expands the 4-bit Amiga DAC values into 8-bit display values, and updates the SDL texture.
This is a very limited HAM emulator. It assumes pixels are always written left to right. That is only fine for this raytracer because the render loop writes rows in order (top down, left to right).
If someone wanted to use this as a real HAM display library, it would need to recompute scanlines when pixels change out of order. I didn't need that capability.
Pixel aspect ratio
A 320 by 200 Amiga image is not supposed to look like a 320 by 200 square-pixel (VGA, etc) image. The first SDL preview looked too wide. The sphere was visibly flattened. The raytracer was not wrong but the SDL display model was slightly wrong.
I adjusted the preview so the logical render buffer is still the original size, but the window presents it with an NTSC-style non-square pixel aspect. In practical terms, a 320 by 200 logical image displays as 640 by 480, and the skipped preview size uses the same display aspect.
I also set the SDL texture scale mode to nearest neighbor. Blurry pixels are not useful here. The point is to see the original low-resolution output cleanly in its original vibe, crunchy pixel edges and all (in reality, on CRT monitors they blurred and blended).
Remainging issues I was too lazy to hunt down and fix
There is still an intermittent SDL presentation problem in the live preview.
Sometimes the bottom scanlines appear black in the SDL window even though the render loop wrote every pixel. I added diagnostics that counted every logical pixel, dumped the final scanline RGB values, bypassed HAM, bypassed 4-bit palette quantization, and delayed presentation. The pixels were computed and stored correctly. The issue appears to be somewhere in the SDL/window presentation path, or in how the texture update and final presentation interact on some runs. I am leaving that as a problem for later me (or someone else with more patience).
The modernized code compiles and displays the result to my satisfaction, but the live SDL preview still has an intermittent presentation artifact on my current Windows test path. The rendered data itself is not the problem. I actually made a slight bypass of the whole HAM system to see the original 24-bit image computed by the raytracer without the 12-bit quantization and HAM encoding. It's quite breathtaking.
Extracting the Juggler animations
While working through the display side, I also wanted better tools for the original Eric Graham media files (sme of which we no longer have the tools and data to reproduce from source).
Ernie Wright had already done the important work here. His jug2tga.c, dated February 15, 1998, documents the private Juggler movie.data format and extracts the animation frames to TGA. It also uses rotate.c, an 8 by 8 bitmap rotation routine based on Sue-Ken Yap's Graphics Gems II code.
The tools/jug2tga.py utility is an enhanced Python port of that decoder (Python because it's easy and portable for most people).
The Python version keeps the same structure where practical:
MOVIEandRGBframe0,frame1,buf,index, andrgbrotate8x8unHAMframe_firstframe_nextsave_frame
The data is Amiga 68000 data, so the multi-byte fields are big-endian.
The tool writes TGA by default (as did Ernie's), but I also added PNG and WebM output:
python tools/jug2tga.py Raytracer_1987_Graham_Source_Code/movie.data
python tools/jug2tga.py Raytracer_1987_Graham_Source_Code/movie.data --format png --scale 3
python tools/jug2tga.py Raytracer_1987_Graham_Source_Code/movie.data --format webm --scale 2
PNG output uses a small built-in PNG writer. WebM output streams raw RGB frames to ffmpeg.
New features include --scale, using nearest neighbor scaling, and pixel-aspect metadata for PNG and WebM. The image should not have to rely on the viewer guessing the old Amiga display pixel-aspect.
It's worth noting that I haven't seen the "Byte by Byte" billboard version of the Juggler animated in recent history. This may be the first time these photons have been emitted in a very long time. (Click to play animation.)
Extracting dragon and elephant
The ADF also contains dragon and ele, plus dragon.dat and ele.dat.
The .dat files are scene descriptions (though the source of the tool that reads them and generates the images is currently lost). The dragon and ele files are already-rendered still images. They are 48,052 bytes each: a 320 by 200 header and palette followed by 48,000 bytes of six-bitplane image data. They are not IFF/ILBM HAM files.
A second small tool, tools/ss-convert.py, converts those stills. It is basically a stripped-down version of jug2tga.py. It reads the still image header, palette, and six contiguous bitplanes, then uses the same bit rotation and HAM-to-RGB conversion approach.
Example:
python tools/ss-convert.py Raytracer_1987_Graham_Source_Code/dragon --format png --scale 2
python tools/ss-convert.py Raytracer_1987_Graham_Source_Code/ele --format tga
That gave me modern PNG/TGA versions of the dragon and elephant renders without running the original Amiga ss display program under emulation (since many people can't).
![]()
You can see variants of these images printed in Eric Graham's AmigaWorld article from May 1987 (page 20-22). "Ele"(phant) lacks the tree that is shown in the AmigaWorld article, but Dragon seems to be a pretty close match. https://archive.org/download/amiga-world-1987-05
What changed in the repository
The current repository now has:
- original extracted ADF contents
- modernized
src/rt1.c,src/rt2.c, andsrc/rt3.c src/rt.h- minimal Amiga compatibility headers
- an SDL-backed Amiga display shim
- CMake and vcpkg manifest support
- SDL2 or SDL3 selection
tools/jug2tga.pytools/ss-convert.py- README files for the media conversion tools
I also added a screenshot of the SDL window, although I am not pretending that the SDL preview bug is fully understood yet.
https://github.com/AlphaPixel/Eric-Graham-1987-Juggler-Raytracer-1.0
The development work was done in the https://github.com/AlphaPixel/Eric-Graham-1987-Juggler-Raytracer-1.0/milestone/1 milestone, but should be merged into main by the time you read this blog.
Future
The next practical work is to decide how much of the old scene functionality to reconstruct. The original disk has scene files for the dragon, elephant, and robot. The modernized C source currently keeps the built-in one-sphere scene. Reconnecting the scene parser would be useful, but I do not want to do it by casually rewriting the renderer into something it isn't. Eric Graham has stated he believes the source to the renderer that can read robot.dat, dragon.dat and ele.dat is forever lost. But I believe it should be possible to reverse engineer a parser that builds a scene in rt2.c's setup() function, which itself says "This function is really a stub, you should change it to suit your needs". Perhaps I will, Eric. Perhaps I will.