Annali da Samarcanda

Alberto Marnetto's Notebook


SuperSight: a graphical enhancement mod for Brøderbund's Stunts

The SuperSight series: Part I · Part II · Part III · Appendixes

Appendix 1: setting a breakpoint in the graphic options menu

Here is how I managed to locate at which address DOSBox debug loads Stunts’ menu code. My plan to find out was the following:

  1. Locate the code in Ghidra and note its address (x).
  2. Determine the offset between DOS and Ghidra address (y).
  3. Calculate x-y and set a breakpoint there

Point 1 was, surprisingly, the hardest part. Ghidra does not allow to search for assembly instructions1, so I needed to get the corresponding machine code. Also, anything with symbolic names was out of the question, since the symbols are different between Ghidra and the assembly. Anyway, the task seemed simple, since the menu code contains the instruction cmp ax, 9 which seemed special enough. To check its uniqueness, I grepped for it in the codebase. There was still a small trap: I needed to redirect the result of grep to a file and then cat it; skipping this step and displaying the results of grep directly on the screen resulted in just whitespace, because the console is confused by the crlf line endings.

src/restunts/asmorig$ grep --line-number -P 'cmp(\s)*ax,(\s)*9[[:space:]]' *asm > /tmp/matches
src/restunts/asmorig$ cat /tmp/matches
seg004.asm:6123:    cmp     ax, 9
seg008.asm:4621:    cmp     ax, 9
seg008.asm:5358:    cmp     ax, 9

Three hits, few enough. Then I needed to find corresponding the machine code. An easy thing, no? Just put the instruction in an assembler and read the result. There is a very convenient online assembler, made by Jonathan Salwan, which supports 16-bit x86. It informed me that the cmp instruction translates to 83 F8 09. I searched for the sequence in Ghidra and, astonishingly, it did not appear! I tried in vain other search parameters, but nothing came to light. How was this possible? The solution was revealed when I asked a second opinion to the most energy-inefficient assembler of the planet:

ChatGPT decompiles the instruction to 3D 09 00
I mocked Copilot in my previous project, but this time I could not afford such luxury. Either LLMs have made huge progresses in the last two months, or ChatGPT is simply better than the Microsoft product in this domain, as we'll see later.

I was a victim of Intel’s CISC architecture, where even a trivial instruction such as cmp ax, 9 can be encoded in different ways. Sure enough, a search for 3D 09 00 yielded exactly three results, as expected from grep, and a quick examination brought me to the right point: 274B:2BE8.

Now, phase 2: where is Ghidra’s 274B:2BE8 address mapped to? A simple plan was to calculate the reverse: start the game in DOSBox, break it at a random location, note down the instructions being executed, look them up in Ghidra. Here I was lucky: stopping the game in the main menu I found instructions that are also in Ghidra’s 274B segment, and in DOSBox are loaded in segment 18F0:

ChatGPT decompiles the instruction to 3D 09 00

Recapitulating:

To stop the game in the menu, I set a breakpoint at 18F0:2BE8. Mission complete: The breakpoint was hit as soon as I selected an option.

At the breakpoint, the register AX holds 1, consistent with me choosing the second option from the top.

The data segment register is set at 2D1C, so the instruction at 2BED copies the detail level into 2D1C:018A, and all the other variables in the data segment are now easy to find. Granted, I could have spared the effort by noticing that the data segment pointer never changes (the MODEL MEDIUM in the assembly ensures that), but it was a good learning exercise.

Appendix 2: the civilized alternative to printf

While I was indagating the resource usage of the graphical primitives I soon found the need to have a pleasant way to print debug information on the screen. Fortunately, Restunts already identified and labeled the function that shows the elapsed time during the race. It’s called intro_draw_text and is very easy to understand: just pass the string to print, the coordinates and the color, and the text appears in the game screen in a relatively pleasant typeface.

The only non-trivial task, then, was to setup a key to toggle the functionality. Since the function recognizing the hotkeys was not ported to C, I had to change the .asm files. I wanted to fiddle as little as possible there, and seven lines proved sufficient: with my patch, pressing the F5 key would make the assembly routine handle_ingame_kb_shortcuts toggle a bit in the global data segment. Such bit would be picked up by the C function update_frame to determine whether to display the debug information.

The dataflow of the “activate debug” bit, from the keypress to its effect.

In the beginning I only printed the number of discarded tiles, but I quickly expanded the string to include the number and size of the graphical primitives and, near the end, whether the “reveal illusions” mode was active.

The debug info in a scenery-rich spot. The track is Rayskate by “Zak McKracken”, founder of the ZakStunts tournament. ZakStunts is still running today, 24 years after its foundation, and last month it reached the highest number of active participants in its history.

The video shows how the debug statistics are displayed in the current release of SuperSight. The message is white if all the foreseen 110 tiles around the car are rendered with maximum detail, yellow if the detail or the tiles have been reduced to avoid overstepping the size of the dedicated buffers. Note that some frames exceed the limits of the original game (400 polygons, 10400 memory bytes), thanks to the extension of the polygon buffer implemented in the later versions of the mod, as told in part III.

Only much later I discovered I had inadvertently eschewed disaster. The successful compilation of Restunts demands that all the various segments making up the assembly code start on a paragraph boundary, i.e. their starting address must be a multiple of 16 (more in-depth explanations have been given by llm in the Stunts Forum, e.g. here). The need to keep alignment usually requires every alteration in the assembly functions to be padded with an appropriate number of nop instructions so that its “modulo 16 length” stays unchanged. When I first introduced the “print debug” bit, however, I was blissfully unaware of this and just shifted the existing code around with the gracefulness of an elephant in a china shop. But it must have been my lucky day because my changes did not corrupt the executable: apparently the instructions I inserted added exactly 16 extra bytes, so that nothing was broken.

Appendix 3: undoing Restunts

After the publication of Restunts v1.0, some users noticed some discrepancies with respect to the original game. In particular,

I soon discovered that both these issues were already present in the master branch of Restunts, which was a relief – this time I was not guilty. However, I had assumed that the C functions of Restunts were a verified perfect equivalent of the original assembly, and these incidents proved it was not the case.

To be honest, I could have noticed the limitations of the rewrite already when reading the rendering function. Line 2297 is rarely triggered since not many objects use primitive type 5, but it leads to a crash if one e.g. tries to drive the user-made car Caterham Super Seven.

Finding and fixing all possible errors in Restunts was an impossible task, but I had a simpler solution: undo most of the work done by the Restunts contributors. After all, the only C function I needed to modify was update_frame, all the others could be ditched in favour of the original disassembled machine code.

So I did, and the following is the result (git diff master supersight --stat):

 src/restunts/c/externs.h                     |   64 +
 src/restunts/c/fileio.c                      | 1082 ------
 src/restunts/c/fileio.h                      |   56 -
 src/restunts/c/frame.c                       | 2655 ++++++++------
 src/restunts/c/heapsort.c                    |   70 -
 src/restunts/c/keyboard.c                    |  233 --
 src/restunts/c/keyboard.h                    |   18 -
 src/restunts/c/makefile                      |   67 +-
 src/restunts/c/math.c                        | 1102 ------
 src/restunts/c/math.h                        |    2 +-
 src/restunts/c/memmgr.c                      |  667 ----
 src/restunts/c/memmgr.h                      |   43 -
 src/restunts/c/restunts.c                    | 1718 ---------
 src/restunts/c/shape2d.c                     |  648 ----
 src/restunts/c/shape2d.h                     |   87 -
 src/restunts/c/shape3d.c                     | 5067 --------------------------
 src/restunts/c/shape3d.h                     |   49 -
 src/restunts/c/state.c                       |  317 --
 src/restunts/c/statecar.c                    |  841 -----
 src/restunts/c/statecrs.c                    |  321 --
 src/restunts/c/stateply.c                    | 3357 -----------------

Most of the C files have been erased. The only survivors are frame.c, which contains the tile-rendering routine, and some auxiliary files. Adjusting the makefile to account for that was quick and easy.

After performing this operation all the discrepancies between SuperSight and the vanilla game disappeared. For my personal curiosity, I still tried to find out where the differences lie. Analysing the replay desync was too complex: it would require understanding Stunts’ physics model which remains terra incognita to this day. But the memory exhaustion problem was interesting.

Stunts allocates part of its resources in the data segment and part in a dynamically allocated heap. Interestingly, the C rewrite had changed the memory allocator for the latter: while the original game employs the unusual2 “Allocate memory” service of DOS (int 21h, AH=48h), Restunts calls instead the malloc function provided by the C runtime. Apparently the latter function was not able to deliver as much memory as the OS interrupt.

After obtaining a block of memory, Stunts has an internal allocator that reserves chunks of it for various resources. I did not decode the whole algorithm, but I instrumented the function to get a glimpse at how the memory occupancy evolved. Since each chunk is associated with a resource name, the resulting picture is very informative. Here a diff comparing two runs, one featuring a Countach-Countach race, the other a Countach-Diablo duel.

Unpaid ad: this diff program is P4 Merge, my favourite tool for conflict resolution in git.

The first two numbers after each entry represent offset and size of each resource. The meaning of the third number is unknown (“resunk”) but I suspect that a value of 0 marks the entry as deleted. The allocator seems happy to shift resources around, and the functions wishing to get a resource can probably request its current address by passing the name as key.

I think that the entries start at about 18K because the previous part is taken by other things like the buffer for the 3D primitives, which prompted me to be prudent when sizing it. Apart for that, one can see how the run on the right has to additionally allocate the 3D shape of the Diablo (stDIA3.3sh), and curiously this has cascade effects on the position of the other resources. Notice how some entries are allocated at the end of the chunk. Some resources seem to overlap, but when that happens one of those has a resunk value of 0. Maybe it was erased and moved in another place (notice the duplicate names) when a previous element had to be enlarged? There is still a lot to discover in this game. As for me, I am for now happy with what I found out during my project, and will leave the rest to future explorers!


  1. Actually, Ghidra does allow to search for assembly instructions, but at the time I was unable to get the functionality to work. I made so many mistakes in this project that I keep finding them even several months after its conclusion. 

  2. In modern programming, asking the OS for more memory is normal. Not so in DOS: this OS is happy to give the user the whole amount of RAM at program start if the user wants. 


No GitHub account? You can also send me your comment by mail at alberto.m.dev@gmail.com