Weird Science - Tales from the
Vectrex Academy Lab
Vectrex
Project Title
- An analysis of the original HYPERCHASE
cartridge and source code
Project
Status
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
Latest
modification on 02/12/2023, 11:15
|