Prof. Dr. rer. nat. Peer Johannsen

Weird Science - Tales from the Vectrex Academy Lab

Vectrex Project Title

  • An analysis of the original HYPERCHASE cartridge and source code

Project Status

  • Ongoing research

Synopsis

  • Digital Archeology - An investigation of the Hyperchase cartridge
  • In a more general discussion of Vectrex wobble and flicker (see here), Malban mentioned that several of the original GCE games deliberately set the refresh timer to a value other than 50Hz. One of those games is Hyperchase, running at 33Hz, which results in a noticeable flicker on real consoles. I became interested in why that specific framerate was chosen in Hyperchase. Especially, because in comparison to other games, there are not that many things drawn on the screen. So why such a low framerate?
  • Here is a summary of some interesting, yet probably utterly useless findings...

An analysis of the HYPERCHASE source code

  • These findings are deductions (and some speculations) based on a full disassembly of the original Hyperchase cartridge code.
  • Disclaimer: I hold the highest respect for all the designers and programmers who worked on the original Vectrex games back in the Eighties, and who were using development environments which are completely different than those of our modern times. So, if some of what I write here might sound negative or presumptuous, it is not meant like that at all.
  • Hyperchase sets the refresh timer to 45.000 cycles, which corresponds to 33Hz. The average cycle count per frame during a complete race is around ~53.000 cycles (with peaks up to 74.000 cycles). So we have a more or less noticeable flicker on a real console. The code uses (almost) the complete 4K, only the last 8 bytes seem to be unused garbage.
  • The code implements a really elaborate graphics engine, which does lots of trigonometric computations, some of them in 16-bit resolution. The engine is of a pseudo-3D type, computing a central projection with all elements aligned in reference to a flexible vanishing point, also called apex (the point where the sides of the street meet on the horizon line). In each frame, all placements of objects are re-evaluated and re-computed on-the-fly. This, of course, takes a large proportion of cycles, but it also makes the code and the engine very versatile and flexible. Just move the apex, and everything else will be correctly realigned.
  • My feeling is that the programmers likely converted a high-level programming language text-book example of such a graphics engine (as e.g. taught in Computer Graphics lectures) to assembly code, including all the mathematics. The engines works very well, but in parts it might be a bit over-powered for the specific purposes of this game.
  • The engine does not use a Cartesian coordinate system. Objects (scenery and cars) have polar coordinates (an angle and a distance) in reference to the apex on the horizon. All screen elements (except for the texts, the horizon lines and the street lines) are always placed in the following fashion: First, the beam is reset to the center of the screen, and then a Moveto_d_7F() is done to the apex position. From there, the object's angle and distance (== scale) are used to move the beam to the effective object position, and there the object is drawn.
  • The second move (from apex to object position) is done by means of Mov_Draw_VL_d(), which does a move to the coordinates stored in the d register, and then draws a vector list of length Vec_Misc_Count (the vector list is pointed to in the x register). But in Hyperchase, here Vec_Misc_Count is always zero, and x contains some arbitrary value! So this routine effectively does only a move, and draws nothing. I do not see any good reason for the use of Mov_Draw_VL_d() here, as a single move can easier and faster be achieved by a simple Moveto_d() call. Each of those Mov_Draw_VL_d() calls takes 22 cycles longer than a Moveto_d(). On average, ~144 cycles per frame are thus wasted.
  • Objects to be drawn (cars and scenery) are stored in a singly linked list, stored in RAM, with each entry having a pointer to the next element in the list. Again, this design is an elegant textbook example from Computer Science, which, seen from the teacher's point of view, makes me happy. But the game-programming-hacker inside me cries out, as this (just as the sophisticated 3D graphics engine) seems a bit over-engineered for the purposes of this game. Inserting new objects in such lists is easy and efficient, but removing objects is computationally expensive. Once inserted, the Hyperchase code actually does not remove any objects again from the list, but just flags them as inactive. New objects are inserted by traversing the list, looking for an inactive slot. Only if none is found, then the new objects is appended at the end of the list. This is a (textbook) design pattern often used in combination with operating systems which offer dynamic memory allocation. On the Vectrex, there is no performance advantage in comparison to simply using a flat object array with a first-empty-slot pointer. Though such a thing is rather a dirty non-textbook solution, it is very likely faster if cleverly implemented.
  • There is a funny bug in the code, related to processing the object list. The list is processed in three different places, and at the beginning, there is always a check, if the pointer to the first list element matches the pointer to the end-of-list element, in which case processing of the list is skipped. The code looks like this:

            ldu  RAM_ptr_obj_list_start
            cmpu  RAM_ptr_obj_list_end
            beq somewhere

  • In one place, instead of the "cmpu", there is an accidental "cmps". This is clearly a bug, most likely a typo. The funny, or rather lucky thing is, that this bug never comes into effect, as the object list is never empty, and thus all these checks are essentially redundant.
  • Cars on the screen (player as well as opponents) appear rotated according to their specific view-point angle. Those rotations are computed on-the-fly by means of Rot_VL_Mode(). And they are always computed, even if objects are inactive. This, of course, wastes quite a lot of CPU cycles. I did an alternate implementation, which uses lookup-tables and pre-rotated lists stored in ROM, at the expense of an additional 1.800 bytes. This saves a whopping 21.300 cycles per frame! Of course such a comparison is unfair, as ROM space was still very expensive when the game was programmed. I wonder if using 8K for this game instead of 4K was ever suggested to management, and then probably declined. 8K would also have allowed for implementing an additional race track, with alternate sceneries. Nevertheless, I think having realized Hyperchase the way it is in 4K was an amazing job!
  • There seems to be another potential bug in the computation of the rotations of the opponent cars. If the first object of the object list is a car, then its angle offset is taken from the wrong memory location due to an incorrectly initialized pointer. This bug also does not seem to come into effect, as the first object of the list seems never to be a car, but a scenery element. I could not yet find any hard proof for this in the code, but at least this was the case in all my experiments.
  • Track data (the bending of the road), scenery data, and data determining when and where cars appear, is stored in separate tables. Those tables have inter-dependencies. Modifying only one table essentially breaks the correct appearance of the track. E.g. certain scenery elements are placed and drawn correctly only on a certain side of the street and/or only at a certain degree of bending of the street. So all those data entries have to match in certain ways. Also, altering the scheme of the other cars appearing has an influence on the total length of the track, as it seems to be taken into account how cars are passed. It must have been quite some work and effort to design and tweak the data tables in order to get the track as we know it.
  • The game uses the very same explosion effect as Bedlam. Just check it out, try Bedlam, then try Hyperchase. It is not just the same effect, but also the very same code that is used in both games. So there was definitely some code sharing between the programmers at WT. I am pretty sure that the same code is also used in Cosmic Chasm, and Rip Off, and maybe even more games. I did not check the code itself, but only did a quick visual comparison.
  • The game does not use the BIOS built-in Joy_Anlog() routine, but comes with its own implementation, which also checks and computes the Y position of the joystick of controller 1. The Y value is stored, but not evaluated or used in the game code. Maybe it was used in early stages of the game, but then the programmers stopped using it and forgot to remove the respective code parts. This part unnecessarily wastes some ~490 cycles per frame.
  • There is one thing about Hyperchase which has always annoyed me. There are some upcoming cars approaching so fast that the only way to avoid a crash is by memorizing the track and the pattern of the cars and the time when they occur. In my opinion this makes the game depend a bit too much on chance, and thus a bit less fun. I would prefer if the emphasis was a bit more on skill with the analog joystick control. I have analyzed the mechanics behind this. Each car has its own speed, and the on-screen-movement of each car is the results of its own speed in relation to the speed of the player's car. So, if the player is driving at top speed, and an opponent car is spawned with an extremely low speed of its own, then this causes the very situation where a car is approaching ultra-fast on the screen. In that sense, the graphics-engine (or rather the physics-engine in this context) is true to reality. What is not true to reality is that opponent cars can pass through each other, which makes those fast approaching cars even harder to detect. I have tried a patch which prevents that there is too much a difference between the player's current speed and the speed of a newly spawned opponent car. This makes the game much more playable, but it has a negative side effect. In total, it now takes longer to pass such cars (they are now moving at a similar speed as the player), and this somehow causes more and more new cars to be spawned. At some point, the street gets really crowded (which is sort of fun and challenging), but soon after the game crashes, because the object list overflows. There is no check or code to prevent this. So the whole game relies on not too many cars accumulating on the screen, and the original track design is tuned to assure this. I have not yet understood enough about the interweaving of the track data tables to make my patch work. An appropriate solution could be to introduce a maximum-size for the object list and add a respective check (kind of funny that they added the redundant emptyness check for the list, see above, but not an overflow check). Such a patch would probably make the 50Hz version (see below) really cool.
  • To be continued. Comments are welcome!

Source Code

  • There are still some parts of the code which I have not yet fully analyzed, this is still ongoing work. I did not make the code disassembly beautiful in any way. I have not fully documented it, and some comments might be outdated. If you spot any mistakes, or if you have additional insight, please let me know.

Credits

  • This analysis would not have been possible without Vide.
  • Many thanks to Malban for adding tons of features I suggested or asked for.

Patched Hyperchase Versions: Scenic Drive + Alternate Landscape + 50Hz

  • Please note, these are just experimental patches, not meant as alternate game versions.
  • hyperchase_scenic.bin, containing the following modifications:
  • Title text changed to "Hyperchase Scenic"
  • Collision detection with other cars disabled. You can still crash into the street boundaries.
  • End message changed to smilies in order to prevent abuse in tournaments ;-)
  • And yes, I also have a god-mode version, with all collision checks disabled.
  • If anyone wants this, let me know.

  • hyperchase_alt.bin, containing the following modifications:
  • Title text changed to "Hyperchase Alt"
  • Some quick modifications of the vector lists of the scenery elements, producing an alternate landscape. I did not put any effort in this, but simply tried some easy things.
  • Feel welcome to suggest some more elaborate drawings.

  • hyperchase_50hz.bin, containing the following modifications:
  • Refresh timer set to 50Hz
  • Title text changed to "Hyperchase 50Hz"
  • Only X-axis of controller 1 is evaluated (speedup)
  • Mov_Draw_VL_d() calls replaced by Moveto_d() calls (speedup)
  • On-the-fly rotations replaced by lookup tables and prerotated vector lists (speedup)
  • Some few low-level code optimizations, replacing instructions by quicker alternatives (speedup)
  • The average cycle count is now around 33.000 cycles per frame. This looks awesome on a real console, without any noticeable flicker. But it also make the game incredibly hard.
  • Enjoy the speed. If you can... ;-)

  • I strongly suggest that you play these on a real console, and that you play the original Hyperchase first and then the patched versions, in order to fully experience the difference. Let me know what you think!
  • Downloads are free and for non-commercial use only. Use at your own risk.
  • Please respect the copyright and credit the author and the origin of this game.

Online Playing

  • Link to Dr. Snuggles' online emulator to directly play the game in your browser:
  • For comparison (though comparing this on real consoles is much more telling):

Author

  • Peer Johannsen

Latest modification on 05/12/2024, 11:15
  • Page updated