We Solve Your Difficult Problems: Forward Porting with Legacy Media and Code – Part 3 – Adding SDL UI

AlphaPixel often gets called upon to work on legacy codebases, sometimes VERY legacy. We have contact with code from the 80s and 90s on a regular basis, in a variety of dialects and languages, and stored and archived in various difficult containers and mediums.

While NDAs and confidentiality mean we often can’t talk about our paid projects, we recently had an interesting side project that used the same processes, only it was all for fun, so we can talk all about it.

The task: Revive the Amiga source code for a basic educational raytracer written by our friend and early mentor, Brian Wagner.

In August/September 1991, during the era of powerful “big box” Amiga computers, when the Amiga was at the top of its game in the graphics and performance computation world, our friend Brian wrote a couple of articles for a short-lived programming magazine named AmigaWorld Tech Journal (henceforth, AWTJ). At the time, I read the article but never compiled and ran the code. Recently, while mentoring some computer graphics Padawans myself, I had the bright idea to revisit that super-simple explanation of ray-tracing in its most basic form. But there were Difficult Problems to Solve.

In Part 1 of this blog series, we located the legacy source code online and assembled a set of tools to bring the code into a modern environment intact. In Part 2, we updated the code and got it running with exactly the same abilities it originally had, using a modern compiler and operating system.

Now, in Part 3, we’re going to restore the GUI abilities that were lost in porting.

I’ll try to document every step and tool used.

We’re starting with GitHub commit hash 08545faee0a5bf1e15b8b6c668d4b2f1cb20ce17. This revision is known to build and run on Windows 32-bit, VS Code with the Visual Studio 2019 compiler. It may not compile on other toolchains though, due to some archaic practices in the C source, which we’ll start by cleaning up.

Clang is throwing an error and other compilers throw a warning similar to this:
/AmigaWorld-Raytracer-Brian-Wagner/load.c:22:6: error: conflicting types for 'convertcol'
[build]/AmigaWorld-Raytracer-Brian-Wagner/load.h:2:6: note: previous declaration is here
[build] VOID convertcol(LONG col, struct Polygon *poly);

This happens because where the Polygon structure type is used (as a pointer), it hasn’t been defined. Early C compilers permitted this because they just classified all pointers as the same thing – a pointer. You could often intermix pointers to different types without any fuss. Modern compilers, trying to keep programmers safe, will flag these incompatible interchanges, and will fuss if you try to work with a pointer of an unknown type. Different compilers will be more or less strict about it.

The most minimal-code solution seems to be to include the tracer.h file where the Polygon type is defined. However, this leads to multiple-definition problems in some places that include load.h AND tracer.h. So, along the principle of minimal changes, we’ll solve it by adding a preprocessor multiple include guard in tracer.h

#ifndef INCLUDE_POLYGON_H
#define INCLUDE_POLYGON_H
// guts of include file go here
#endif // INCLUDE_POLYGON_H

and then include tracer.h in load.h.

Since we benchmarked the original 68000 and last-production 68040 Amiga hardware in the footnote to Part 2, let’s do it here for fun. Running in screenmode 4, scale of 1.0, the resulting timestamps (start and end) do not differ by even one second). Therefore, the workload is too small to measure in this way. We can at least conclude it’s computing over 256,000 traced pixels per second, compared to the ~4000 per second of the Amiga 4000/040 or the ~10 per second of the 8MHz Amiga 500. Let’s add some sub-second time measurement. I’ll refer to this snippet: https://www.tutorialspoint.com/c_standard_library/c_function_clock.htm

Now, when we run, the output (for the computation time phase only, which is all that’s really important to us now) is

Total time taken by CPU: 0.282000

.28 seconds is just over a quarter of a second. So, this could run at 4fps. Not bad, considering there’s no optimization at all. I’m sure we can make it faster in the future.

Dividing 256,000 by 0.282 gives us an approximate benchmark of 907,801 traced pixels per second. This is 88,739 times faster than the Amiga 500 and 202 times faster than the Amiga 4000/040 (and we’re not using any multithreading or GPU capabilities of the modern hardware yet).

I’m committing this benchmark enhancement as 5b579d6ee4a8a8e8674d6027cd936a1d62f4b986

Now, let’s move ahead with rebuilding the GUI we had to #ifdef out in the very beginning to get the code to compile outside of the Amiga OS.

A really good facsimile of the Amiga OS graphics SDK in modern code is the popular SDL2 library. It’s zlib-licensed, well-supported, and cross-platform. The API calls actually are relatively similar too, so it should be an easy port. Let’s give it a go!

I’m going to introduce vcpkg for those who aren’t familiar with it, because it’s a super easy way to pull in third party components without a lot of fuss. To start using vcpkg, if you haven’t before, take a look at these instructions: https://learn.microsoft.com/en-us/vcpkg/get_started/get-started-vscode?pivots=shell-cmd

When you have vcpkg itself installed and path configured, run the following command in a shell while in the project folder ‘AmigaWorld-Raytracer-Brian-Wagner’:

vcpkg new --application

This should create ‘vcpkg.json’ and ‘vcpkg-configuration.json’ files.

Now do

vcpkg add port sdl2

This will add sdl2 to the vcpkg manifest, and your vcpkg.json file will look like:

{
  "dependencies": [
    "sdl2"
  ]
}

Now run 

vcpkg install

to rescan the manifest and install the necessary packages (SDL2). This may not really be necessary, but it’s how I did it.

Now open up CMakeLists.txt and (referring to https://vcpkg.link/ports/sdl2) add:

find_package(SDL2 CONFIG REQUIRED)

target_link_libraries(main
    PRIVATE
    $<TARGET_NAME_IF_EXISTS:SDL2::SDL2main>
    $<IF:$<TARGET_EXISTS:SDL2::SDL2>,SDL2::SDL2,SDL2::SDL2-static>
)

right after the project line.

(CMake may try to run when you save this, and complain, but we’re getting there…)

Add the CMakePresets.json and CMakeUserPresets.json files per the Getting Started guide above.

(at this point, I handed my messy, old, CMake config to our experienced developer John T, who schooled me in modern CMake. The results of his work are cleaner and more robust and are represented by his commit 97e604c25626d7a2626d840be4f45c2e6c064ea2. If you’re following along at home, you should start there.

If you remember, we #ifdef’ed out all the UI code with the define WINDOWED_UI. So now, all we need to do is revisit those locations and rewrite the Amiga SDK UI calls to something similar in SDL’s API, and we should be good. Interestingly, SDL and modern graphics APIs benefitted from the hindsight of well-designed earlier graphics APIs on the Amiga, the Macintosh, XWindows, etc. I can find no evidence that Sam Lantinga, the original author of SDL was ever connected to the Amiga (looks like he was more Unix and a smidge of BeOS), but the SDL API looks fairly similar to the Amiga code we’re going to be replacing.

One quirk is that the Amiga, and early desktop computers, often habitually changed the display card’s actual resolution/color configuration, because in early graphics hardware days you couldn’t have both maximum resolution AND maximum colors. To get the Amiga’s maximum of 4096 colors, you’d have to be in a 320×200 or 320×400 resolution. If you wanted 640 pixels wide you could only have 16 colors. The tracer program doesn’t even draw the pixels it calculates, grayscale or color. It simply plots a pixel of a predefined color everytime it finishes raytracing that image location.

So, we’ll be omitting all of the code that selects a GPU screen resolution, and pushing all pixels to the display in full 24-bit color, because we can. Also, we’re not actually going to process input events, because this is not an interactive program. It just runs until it’s done. Maybe we can add interactive processing in a future blog.

The Amiga OS SDK used a structure called a RastPort, which was a system handle for perform RASTer drawing operations. It also used a Window (duh, representing a window on the screen) and also a Screen (representing what Windows would call a Desktop now, except that each one could be a different resolution and color configuration). We won’t need to recreate the Screen, as we’ll just run on the existing OS Desktop, we will need a Window (SDL_Window) and the RastPort is roughly the equivalent of SDL_Renderer.

We’ll add the replacement

#include <SDL2/SDL.h>
everywhere that the old code includes the Amiga Intuition header
#include "intuition/intuition.h"

Now, in tracer.c we’ll replace the Amiga graphics/windowing handle datatypes

   struct Window *wp;
   struct RastPort *rp;

With SDL

    SDL_Renderer *rp;
    SDL_Window *wp;

We can remove a bunch of extraneous stuff like vmod (Video Mode – which chooses the resolution and available color depth) and IntuitionBase and GraphicsBase (handles for the Amiga’s runtime DLL loading architecture for two UI libraries). We can also remove the code that loads and unloads those DLLs and sets the vmod. 

SDL does some weird init stuff before calling the program’s main(), and this is choking on the old-style main arguments, so I’ve added an #ifdef to change this around when compiling with UI support. Later, when we do code cleanup more widely, I’ll address this and unify both paths to modern styles.

In the main drawing loop, we draw pixels by setting the drawing color, plotting one pixel, then pushing the current drawing buffer contents to the display. This is likely to be a slow and expensive operation, but it’s fine to start. Let’s try compiling and running:

Dang! First compile runs exactly as expected!

There are some issues. The drawing of every pixel to the screen is costly. Also, the fact that no event processing is being done means as soon as you try to move the window around or interact with it, it locks up.

One problem though – it appears the SDL_RenderPresent call, which in a double-buffered drawing stack syncs the invisible working back buffer to the visible front buffer, is kinda slow. It takes measurable minutes to render the whole scene again, a major step backward! The original Amiga code operated in a single-buffered manner, with all draw actions being performed immediately on the visible display surface. Since the updates were happening slowly, it didn’t matter. SDL is made for smooth animated game graphics, so it always operates in double-buffered mode, using a memory/bus transfer to get the finished image into GPU memory for display.

Various accounts claim that this operation will be ignored if it has been less than 15ms (about the time for one frame at 60Hz monitor display rate). It appears however, that this may not be true, and probably every one of the 256,000 SDL_RenderPresent calls is copying the entire finished image to the display for just one changed pixel. A smart program would sniff the display refresh rate, and use millisecond-accurate timers to predict when it had been about one frame’s worth of time since the last update. But that would be a lot more code, so what we’re going to do is simply guess that we should update the preview about every 6400 pixels, and add an interval counter to only fire the SDL_RenderPresent that often. This should give us 40 updates through the course of the 256,000 pixel trace.

A really concise and self-contained implementation of this would look like

{static unsigned int PresentInterval; if(!(++PresentInterval + 1 % 6400))SDL_RenderPresent(rp);}

Let’s unpack that.

First, we create a static unsigned int that will count how many pixels we’ve plotted. Why static? Several reasons. My goal here was to create a minimal-impact change that didn’t bleed outside the scope where we need the fix. So, I didn’t want to declare a counter variable AND add initialization code elsewhere, outside the loop where the test happens. In C, a static variable persists even after it program execution exits the scope where the variable was defined. In fact, it persists from program start to program termination, a sort of global-lifespan object, with local scope. So it’s perfect for lasting the entire duration of the loop, but also not needing to be maintained with code elsewhere. Secondly, the Current C Standard tells us (Section 6.7.9, paragraph 10):

If an object that has static or thread storage duration is not initialized explicitly, then:

— if it has arithmetic type, it is initialized to (positive or unsigned) zero;

So, we don’t have to worry about putting the initialization-to-zero code elsewhere, at the start of the loop. It will just come to us preset to zero! Perfect for the quirks of our use-case.

Now, as the pixel-counter increases, any time it is an exact multiple of 6400, the expression

PresentInterval % 6400

will become zero. This is because PresentInterval % 6400 is computing the modulo, or remainder, of  PresentInterval / 6400. When there is no remainder (for example, exact multiples of 6400), the modulo is 0. So, if we test for

if(!(++PresentInterval % 6400)

Then every 6400 pixels, the expression will be true, and we’ll invoke SDL_RenderPresent() to update the display.

There are two edge cases to consider. What happens if we render enough pixels that the PresentInterval variable overflows? This is not actually a problem, it will overflow back to zero and keep going. There is one more oddity. In our 256,000-pixel render, PresentInterval starts at zero and goes to 255999 (a total of 256000 pixels, but the first pixel isn’t numbered 1). So, we will stop rendering and updating with 6399 pixels not synced to the display, because 255999 is not an even multiple of 6400. We COULD solve this by initializing PresentInterval to 1, but then we get into the whole initialization-scope issue we’re trying to avoid.

The cleanest solution seems to be to compute the modulo not of PresentInterval itself, but rather, to temporarily add one to before taking the modulo. Hence:

if(!((++PresentInterval + 1) % 6400))

This means that compared to the original code, the FIRST pixel rendered (pixel zero, but modulo’ed as 1) will NOT trigger a display update, which is fine, not even a microsecond will have passed and there’s only one pixel to see, so we’re not losing anything. Then, the 6399th pixel (computed as 6399+1=6400) will modulo to 0 and trigger an update, only one pixel later than the original code. Finally, the LAST pixel rendered, number 255999, will be modulo’ed as 255999+1=256000, and will cleanly result in a zero modulus, triggering one final display update that the original code neglected.

Obviously this is a bit of a hack, but since all the supported screen resolutions are multiples of 6400, it should work fairly well. We now update only 40 times on a 256000-pixel trace, instead of … 256000 times. This is a good solution.

At this point, we’ve accomplished the basic goal of this stage, which is restoring the original progress UI (with an arguably better full-color live render preview compared to the original’s “we rendered this pixel but you can’t see what it is”). There are still issues relating to input event processing, but solving them will take more substantial and widespread code changes. So, for now, we’ll commit what we have as f33393ffa9c08c34c116c1fa23fad61c88ae477e.

In the next blog post we will add a unit test to make sure we don’t break anything while making further improvements, put in some quality-of-life convenience features like new formats, clean up the legacy datatypes, and maybe make the resolutions more flexible. Stay tuned!

AlphaPixel solves difficult problems every day for clients around the world. In the past we’ve done extraordinary things, like recovering deleted forensic video evidence from SD cards, having old failed hard drives refurbished to rescue critical data, reverse-engineering proprietary data formats to liberate valuable siloed information, even desoldering surface mount chips and sucking their brains out with hardware exploitation devices. We’ve worked on common and exotic architectures and operating systems and environments from the 8-bit to the 64-bit, Motorola, Intel, ARM, RISC-V, PA-RISC, DEC Alpha, Power, and microcontrollers.

The path to rescuing old code and data and making it useful again is a journey, often akin to building a rope suspension bridge one plank at a time, while suspended in mid-air. Sometimes while being attacked by natives, a la Indiana Jones. Often there are interlocking chicken and egg problems, where the obvious solution won’t work because of one minor issue, which then cascades and snowballs into bigger problems. It’s not uncommon to find a bug or omission in some old tool you need to use, and when you go to fix it, you find the tool can no longer be compiled by modern toolchains, necessitating reconstruction of a side environment just to fix the problem that’s blocking you on the main endeavor. We’ve jokingly used the term “Software Archaeologist” in some contexts like these.

If you have a difficult problem you’re facing and you’re not confident about solving it yourself, give us a call. We Solve Your Difficult Problems.

 

We Solve Your Difficult Problems: Forward Porting with Legacy Media and Code – Part 3 – Adding SDL UI
Scroll to top

connect

Have a difficult problem that needs solving? Talk to us! Fill out the information below and we'll call you as soon as possible.

Diagram of satellite communications around the Earth
Skip to content