Virtual Reality has gone through several waves of being “the next big thing” that would forever change the landscape of video games. In the early ’90s, the potential of Virtual Reality had captured the imagination of the public, with splashy features in newspapers, magazines, TV news channels, and even major motion pictures like The Lawnmower Man. While a few VR products eventually made it to market, many more companies experimented with the technology.
One of these companies was Sega, who sought to create an affordable VR solution as an add-on for their home console, the Sega Genesis. Unlike gargantuan virtual reality arcade machines that could cost well into the five figure range, Sega’s home VR product set a target MSRP of just $200. Could it be done? Well, sort of.
Equipped with a high-frequency inertial measurement unit and two LCD screens, the Sega VR headset shares a lot of fundamental design with today’s VR headsets. That design was nothing short of revolutionary when Sega officially unveiled the unit to journalists and retailers in 1993, promising to break new ground on the frontier of virtual reality. They miraculously hit their $200 target thanks to technology licensed from a start-up company called Ono-Sendai, whose patented tracking solution could be manufactured for just $1.
Sega’s official reason for cancelling their VR helmet was questionable: they claimed that the experience was so realistic and immersive that it posed a high risk of injury from players moving around while using it. However, a likely factor in the cancellation was feedback Sega received from the Stanford Research Institute, which warned of headaches, dizziness, and sickness, particularly in younger users and children. In an episode of Retro Gamer Podcast, former CEO of Sega of America Tom Kalinske confirmed these issues as major factors in the decision to abandon the project.
Until now, most of what we know about Sega VR comes from trade show appearances, marketing materials, patent documents, and firsthand accounts. This has meant that many of unit’s the technical details have remained speculative or completely unknown. When looking back and studying hardware that pushed so many of the technical boundaries of its time, however, those details are important! Whether Sega VR achieved its many ambitious goals or not, it remains a fascinating and notable entry in VR history.
In order to study hardware of this nature, if you don’t have access to the hardware or its implementation details, access to the software is often the next best thing. The software will tell you exactly what it expects of the hardware, and given those expectations, you might find that you have enough information to emulate the hardware. At the very least, you’ll have enough information to emulate a version of the hardware that conforms to the software’s expectations, and that’s exactly where we’re headed!
Several titles were in development for Sega VR, but because the hardware was never released, none of them ever saw the light of day — until now. Thanks to the incredible efforts of Dylan Mansfield over at Gaming Alexandria, one of them has been recovered. Dylan got in touch with Kenneth Hurley, co-founder of Futurescape Productions.
The Sega VR title his company worked on, Nuclear Rush, is a first-person action game best described, in the most 1993 manner possible, by the game’s own intro:
ELECTRICITY IS IN DEMAND,
BUT THE FOSSIL FUELS ARE GONE.
YOUR MISSION: ACQUIRE RADIO-ACTIVE FUEL…
BY ANY MEANS NECESSARY.
AN INFORMER, KNOWN ONLY AS THE CARETAKER,
TELLS YOU OF SECRET ZONES FULL OF THE WASTE
OF OLD-STYLE REACTORS.
The game has occasionally been mistaken for Iron Hammer, another Sega VR title, likely owing to a great many visual and descriptive similarities. In the few brief glimpses that were provided at Summer CES 1993, Nuclear Rush was also lacking the HUD used in the final version of the game, contributing to a more distinct appearance.
After Dylan contacted Hurley and explained his interest in Sega VR, Hurley dug up a CD-ROM dated August 6, 1994. Thankfully still intact after 26 years, the disc contained the complete source code for Nuclear Rush, as well as source code and tools for some of the other Sega Genesis games Hurley worked on.
This brings us to the beginning of our journey. We’ll start with getting Nuclear Rush back up and running, and by the end, we’ll have resurrected an incredible, unreleased piece of Sega history, more than 25 years after it was abandoned. Along the way, we’ll occasionally dive deep into the technical details, so feel free to use the table below if you find yourself lost.
- Building the ROM
- First Run
- Bug Hunting
- Virtual Virtual Reality
- Oh, My Eye Bits!
- An Original Developer’s Perspective
- The New Sega VR Experience
- Source Code and Downloads
- In Closing
Building the ROM
Although the source code for Nuclear Rush was all present on Hurley’s disc, there was no compiled binary to be found. In an effort to find someone who could put it all together again, Dylan reached out to me. At first glance, it seemed like we had just about everything we needed to build the game. All of the art dependencies were there, and most of the tools were present, albiet in the data for a different game that was on Hurley’s CD-ROM, Monster Hunter.
Most of the Nuclear Rush game code is written in C, relying on the Sierra 68000 C Compiler, with some boilerplate code (including the Sega VR headset driver) and a few other bits and pieces written in assembly. The most cycle-intensive part of the game, the scaler, is also written in C, but with a twist. The C code generates native M68K code on demand, then calls into the generated code, which is where most of the cycles are spent.
Beyond the standard build tools, the build process makes use of a few proprietary tools in order to ingest art data and spit it out in game-ready form. CVTSCE.EXE is used for static background/UI screens. ANM2FPA.EXE is used for scale-ready sprites and animations with optional RLE. Finally, LZSSC.EXE is used to compress the output of CVTSCE.EXE, applying standard fare (12-bit offset, 4-bit length) LZSS compression.
On my first build attempt, I ran into a missing tool, DUMP.EXE. Looking at the intended usage, it was apparent that this program was just opening a file and emitting text to stdout for each byte, so that the binary could be ingested by the assembler. After a couple of cherished minutes spent writing code in Borland C++ 3.0, I had a replacement executable built, and away I went!
The build process completed successfully and generated a COFF file, with the important parts of the ROM split into a couple of sections. I wanted to keep the whole process of producing a ready-to-use ROM image under the same MS-DOS environment, so I returned briefly to my good friend Borland and wrote another program to parse the COFF, generating a binary with the sections correctly placed and the checksum in the ROM header calculated.
On my first attempt at running the ROM inside an emulator, I was greeted with a screen that made me very happy:
There was, of course, no head tracker. The game successfully recognized that fact and proceeded to the title screen with the headset support disabled. The title music began to play and everything seemed fine, until I hit start and made it into the main menu. The game continued running, but it looked like the palette being used by the menu font had just been stomped. I could still read the text, so I tried loading into the first level…and that’s when the program counter took a dive off a cliff and everything exploded.
So, as is often the case with these projects, it was time to debug! I dumped all of the debug symbols from the COFF, then used the symbol addresses to place a few breakpoints and see how far I was making it into the level load before things took a bad turn.
After some trial and error, I determined that the game managed to load into the first level and even display a frame or two on occasion, then the scaler pulled a bad offset out of a sprite, which had some very bad consequences down the line. You might recall that I mentioned having to take some of these tools from another game’s data, and that turned out to be the problem. The tools were modified at some point after Nuclear Rush, and it came down to this little bit of code near the top of the scaler’s main entrypoint:
wptr++; /* Skip number of sprites in frame */
wptr++; /* Skip hotspot offset */
Those last 2 pointer increments are skipping over data that goes completely unused, and it seems that at some point the data was removed from the ANM2FPA.EXE output. Since this data was already unused, the fix was simple — I just had to remove those two additional pointer increments.
With that, the level loaded up, but the tiles in the background and some of the HUD sprites were still garbled. Those turned out to be similar issues, more changes to the data formats shared by the Monster Hunter tools. In those cases, the problems manifested were just a bit less catastrophic. So, finally, with the ROM built and all of these issues fixed over the course of one very busy Saturday, I managed to successfully load into a level and enjoy my first session of Nuclear Rush!
With the game up and running, I wanted to go back and fix the other bugs I’d encountered along the way. I wasn’t sure how many of those bugs were the result of the hybrid toolset, and how many might’ve been left in the final version of the game. Remember that “final” here doesn’t mean “retail final”, and although the game was effectively complete, it hadn’t enjoyed the typical bug rattling that comes with one last quality assurance push. Speculatively, based on a handful of WCES94 preprocessor checks appearing throughout the code, Nuclear Rush had planned another showing at Winter CES 1994. As far as I can tell, the game never appeared at the show, and that may have played a part in foreshadowing the end of the project. Being far enough removed from those events, it’s fair to guess that Kenneth Hurley’s CD-ROM from August 6, 1994 represents a pretty final development snapshot — somewhere close to the finish line but not quite ready for a commercial release.
With that said, I figured there was a good chance that the rampant palette-stomping I saw throughout the game would be toolset-related again, and it turned out to be an interesting problem. CVTSCE.EXE had also been modified at some point after Nuclear Rush, and as you might’ve noticed from the usage screenshot a little while ago, the program has a couple of options for changing the way palette data is packed into the final binary. It seemed like some of those argument defaults had been changed, but there was a deeper problem.
Many of the tile-based source images being ingested by CVTSCE.EXE contain multiple palettes, with varying numbers of palettes at varying base offsets. For example, one image might use palette 1 and palette 2, while another might only use palette 0, and so on. The output format was designed to specify an index for the first palette in the file, and the number of palettes. When the image is loaded, it uses that index and count to determine which existing palettes to stomp and transfer over to CRAM. The problem here is that no universal default for palette index/count makes sense for all of the images in Nuclear Rush, and I didn’t see any per-file data that might help indicate the correct values to use outside of the contents of the tile map itself.
That led me to modify CVTSCE.EXE. I added an option to scan the tile map, figure out which palettes were referenced within, and use that data to determine the palette range and count to output. I have no idea if this is what the tool was originally doing for Nuclear Rush, but it seemed to get the job done!
I encountered the next major issue while testing on real hardware. Nuclear Rush combines horizontal scrolling with scroll table based vertical scrolling, which later Genesis models are able to handle without issue. Older models, however, will blank out the left column of the background plane. When that happens, it looks like this:
Nuclear Rush attempts to sidestep this problem by checking the hardware version (register $A10001), and if the version is 0, it falls back to truncating horizontal scroll bits. This means that the background is effectively only allowed to scroll one tile at a time, hiding the column blanking issue. The result is very chunky rather than smooth horizontal scrolling, and because it appears without warning on certain Sega Genesis models, it isn’t an ideal solution. I happen to have a Model 2 Genesis which reports a version greater than 0, but still lacks the later VDP changes which addressed this scrolling issue. When I first ran the game on hardware, I was greeted with this blank vertical column on the left side of the screen because the test hadn’t managed to catch my particular hardware version.
Nuclear Rush makes use of the vertical scroll table in order to achieve a “roll” effect, which becomes visible when turning. The effect works by accumulating a small offset for each column entry in the scroll table, and compensating sprite positions by applying the offset from the column that the object falls within:
Rather than completely overhauling the effect, I instituted a simple fix by exposing an option in the menu. The first mode disables the vertical scroll table (allowing for smooth scrolling on older hardware versions), the second mode forces smooth scrolling (even if the hardware can’t support it without column blanking), the third mode forces truncated scrolling, and the fourth mode reverts to the original hardware version check. It’s not quite a catch-all, and could be refined given my Model 2 case, but it’s there for posterity.
After this, I continued fixing issues and making minor additions until I was happy with the state of the game. This included fixing a DMA timing bug and some crashes, making the password entry screen functional again, and fixing a bunch of stuff that only cropped up when the game was running in stereoscopic mode.
Virtual Virtual Reality
Finally, it was time to work on emulating the Sega VR headset! This is what I was really excited about the whole time. If I only had a pre-built ROM image to work with here rather than the source code, this still would’ve been possible. It would’ve been a lot more time-consuming, and involved a lot of sifting through disassembly to work out what kind of data the game was expecting from the headset. Having the source code, however, made it a lot easier!
The Nuclear Rush source code has two files to reference for Sega VR headset communication. HEADSET.ASM is the original driver source code provided by Sega of America, and VRDRV.ASM is a slightly modified version of the code which is used by Nuclear Rush. The core routines which deal with reading the headset are identical in both copies. The driver source also references “VR.DOC/VR.TXT”, which is likely to contain some interesting technical information on the headset implementation, but is unfortunately not present in the repository. Even though it would’ve been nice to have another source of information, there’s still more than enough information to create a headset implementation from the source code alone.
Reading the headset is done through a multiplexing scheme, just like a normal Sega Genesis controller. In fact, all interaction with the headset happens through controller port 2, where the driver assumes the headset is plugged in.
Headset initialization on the software side begins with a handshake. The software sets both TR and TH pins to write via register $A1000B. Unlike a standard controller where only the TH pin is set to write, this gives us two bits to specify different types of commands when writing to the headset via register $A10005. A value write of $60 (TH+TR) instructs the headset to go into idle mode, and $40 (TH) instructs the headset to perform a reset. My implementation waits until it receives a reset command, and uses that to kick the emulator into Sega VR mode. After an idle command, the software toggles $20 (TR) in order to advance the data which can be read back from $A10005. The software immediately expects to read $70 as an acknowledgement response after an idle command, verifying that TH, TR, and TL are set as expected. The acknowledgement is followed by the headset’s identifier, $08, $00, the second half of which is reserved. If any part of that transaction doesn’t go as expected, the driver calls the whole thing off, and we proceed in “no headset” mode.
After the initial handshake is complete, all headset reads are done in series to request the current yaw, pitch, and left/right eye bits. Roll is notably absent, meaning the headset can only be used to track two axes of rotation. I don’t know what kind of drift we might’ve seen from the IMU or what kind of filter was applied before accumulating motion in the angles, because the headset is responsible for providing angles in absolute coordinates. This means that the software just reads the output and applies it as-is, no questions asked. Although this makes it harder to know what was going on inside of the hardware based on the software alone, this implementation detail makes good sense when we consider the lack of processing power (and/or specialized hardware) in the Genesis. I’m speculating, but it’s possible that dedicated hardware for this purpose existed outside of the headset, sitting somewhere between the headset and the Genesis.
Now I needed to understand the format of the headset’s post-initialization data stream, and luckily the headset driver source lays it all out nicely in a comment:
* xxxx|xxxx|xxxx|L R X8 Y8|X7 X6 X5 X4|X3 X2 X1 X0|Y7 Y6 Y5 Y4|Y3 Y2 Y1 Y0
* X0-X8 represent absolute YAW values from 0 to 360 degrees in HEX
* Y0-Y7 represents absolute PITCH values from 0to +/- 30 degrees
* Y8 is sign bit look up Y8 = 0
* look down Y8 = 1
This is describing the format of the data returned by the headset read function, but it’s 1:1 with the format of the data that’s actually read from the headset. The x bits are the unused bits at the top of the 32-bit register which contains the accumulation of headset data reads when the read function returns.
This means that we get 9 bits to represent 360 degrees of yaw and 9 bits (including sign) to represent 60 degrees of pitch, with a good portion of the potential range going unused. The “L R” bits specify which eye the headset is expecting to be scanned out next. In my implementation, I track angles as 32-bit floating point values so that they can remain sensitive to high-precision input. When feeding the angles back to the Genesis, I clamp them and quantize/encode them as such:
uint32_t encode_headset_angles(const float *pAngles)
//assumes angles have already been clamped
const uint32_t pitch = (pAngles >= 0.0f) ? (uint8_t)pAngles : ((uint8_t)-pAngles ^ 0xFF) | (1 << 16);
const uint32_t yaw = (uint32_t)pAngles;
return pitch | ((yaw & 0xFF) << 8) | ((yaw & 0x100) << 9);
The output of this function is combined with the emulated state of the eye bits, which brings us to the interesting topic of display synchronization.
Oh, My Eye Bits!
Nuclear Rush is designed to run locked at 15Hz, meaning the full game loop is intended to run 15 times per second. However, it’s still hitting VBlank interrupts at 60Hz. The headset is read at the end of the VBlank interrupt, but at the start of the VBlank, we’re reading the headset data from the previous VBlank to determine which eye to set up (potentially kicking off DMA transfers) for scan-out. To add to the interesting order of events here, although the headset driver source states that the headset supports operating at 60Hz, Nuclear Rush is only reading the headset at the end of every other VBlank.
When you have your Genesis hooked up to a good old NTSC display, assuming the VDP isn’t operating in interlaced mode, it’s scanning out new frames at just about 60Hz. Normally, this means scanning out to two separate interlaced fields, but we could just as easily interpret the same signal as interleaved progressive-scan images for two separate eye displays, each updating at 30Hz. I expected this is what the Sega VR headset would be doing, which made the way Nuclear Rush is reading the headset and scanning out frames seem a bit weird.
Now, I’ll warn you ahead of time, this is probably going to be hard to follow. But let’s give it a try! If we just hop right in at the start of a VBlank, Nuclear Rush’s frame output can be broken down like this:
- Enter VBlank.
- Read the previous VBlank’s headset results. Left eye bit is set.
- Set the next scan up with the left eye display list.
- Read the headset at the end of the VBlank. Right eye bit is set.
- Scan out for the left eye.
- Enter VBlank.
- Read previous headset results. Right eye bit is set.
- Set the next scan up for the right eye.
- We skip the headset read, we’re on an odd VBlank.
- Scan out for the right eye.
- Enter VBlank.
- Read previous headset results. Right eye bit is still set. We skipped the headset read on the last VBlank.
- Set the next scan up for the right eye once again.
- Read the headset. Left eye bit is set.
- Scan out for the right eye.
- Enter VBlank.
- Read previous headset results. Left eye bit is set.
- Set the next scan up for the left eye.
- Skip reading the headset, this is an odd VBlank again.
- Scan out for the left eye.
- Loop back to 1, and note that we’ll still be on the left eye.
Alright! So in the sequence given above, we’re scanning out images in a “left, right, right, left” order, or “left, left, right, right” depending on how you want to straddle it. This is confirmed if we look directly at the output frames while the game is running in stereoscopic mode. From left to right, starting on a frame where we’ve just switched to scanning out for the left eye, this is what we’ll see for 4 sequential frames:
The reason this struck me as odd is that I would expect the headset to want to switch displays with every scan-out, and it seems unlikely that the hardware would be designed around limiting each display to only updating with a unique frame at 15Hz. The alternative to that idea is that the headset is only changing its display target on read. This works out for every case, then, whether we read the headset at the end of every VBlank or only at the end of every other VBlank. The headset expects to be read at the end of a VBlank, and tells the software which eye it’s going to expect to be scanned out following the next VBlank. At least, that’s my theory! I’ve taken this approach with my implementation, switching display targets only when the headset is read (with an optional minimum VBlank interval), and it seems to work well.
At this point in the implementation, I wanted a quick way to visualize the game’s stereoscopic output, so I implemented an anaglyph 3D post-process which combines left and right eye outputs after mapping colors to lightness. I made each eye color customizable in the emulator configuration, and tweaked it a bit for the glasses I was using at the time. The result was kind of beautiful:
Once I started playing the game this way, though, I noticed something odd. While turning, I’d occasionally see the eyes desynchronize, which was especially apparent in this anaglyph 3D mode:
Initially, I thought I might’ve broken something on my end, and I started debugging again. Everything seemed to check out, so I implemented a feature to take four sequential screenshots starting from a fresh swap to a left eye frame (which was used to produce that string of shots earlier on as well), and I managed to snag one of the desynchronized frames to confirm what I thought I was seeing:
We’re looking at sequential frames in left, left, right, right order again here, but you can see that the second frame for the right eye lurches ahead. I was turning left in the headset as I snapped this series, and what’s happened here is that the game has actually stepped ahead right in between the two images for the right eye.
It turns out that there’s nothing really preventing this, and it can happen at any time, even in between the left and right eyes. The game tries to loop and wait for 4 VBlanks at the end of each frame in order to lock itself to 15Hz, but occasionally a timing hiccup will push it off this boundary and it’ll wrap around to set up the next eye without ever synchronizing to the headset. In all likelihood, this behavior was present in the real headset as well. However, because the real thing relied heavily on persistence of vision to compensate for low resolutions, low refresh rates, and timing limitations when presenting the active signal, my theory is that everything was already trailing so badly that the issue was never noticed or was never really seen as a problem.
I ended up implementing some code in the actual game to synchronize with the headset at the end of the game loop, which cleaned everything up nicely. I’d be curious to know if this changes the experience in a real Sega VR headset, or if there’s an especially formidable cost to sampling the headset at the end of every VBlank (beyond the extra cycles we’d have to spend switching eyes twice as often inside the VBlank), but perhaps we’ll never know!
An Original Developer’s Perspective
My headset emulation was working nicely at this point, but I still felt compelled to try to confirm some of the assumptions I’d made in my implementation. I decided to check in with Dylan Mansfield again to see if he might be able to relay a few questions to Kenneth Hurley for me, which is when I discovered that Dylan had already been in contact with the lead programmer on Nuclear Rush, Kevin McGrath. Dylan was kind enough to introduce me to Kevin, and Kevin was kind enough to talk tech with me!
Straight away, Kevin was able to confirm that each display in the headset was updating at 30Hz. This is something which had initially seemed like a given to me. However, with the way Nuclear Rush scans out duplicate frames and only reads the headset on every other VBlank, this was an important detail to confirm.
We proceeded to discuss the anatomy of a Nuclear Rush frame at length. The discussion included the game’s VBlank logic, scan order, headset read ordering, and the eye desynchronization issue I’d encountered. Kevin wasn’t able to recall all of the details, but he did find the “headset only switches display targets on read” theory plausible, and seemed to agree that it made the most sense to explain how the headset was coping with the Nuclear Rush frame ordering.
Ultimately, I didn’t need to change anything in the implementation, and Kevin was able to confirm enough of the fundamentals for me to feel a bit more confident in my implementation assumptions. Kevin also provided a lot of great insight and anecdotal information on the development of Nuclear Rush and his experience with the prototype Sega VR hardware.
Another point of interest which came out of this discussion was that Kevin spent much of the Nuclear Rush development cycle working without access to Sega VR hardware. Prior to receiving the Sega VR prototype headset, Kevin even tried to rig up his own solution to simulate a stereo flicker effect by rapidly flipping a composite signal between monitors.
Difficulty obtaining prototype hardware may have been a common theme among developers, given Kevin’s experience in conjunction with another account from Alex Smith, lead programmer on the Sega VR title Outlaw Racing. In response to one of Dylan’s questions about the headset, Alex shared that he never saw the prototype hardware. This would seem to further narrow the list of games which actually implemented Sega VR support, even among the games developed with Sega VR in mind. We’re all the more fortunate to have come upon one of the games which did manage to fully implement support for the headset!
The New Sega VR Experience
I was running around in anaglyph 3D and controlling headset angles with an analog stick, and it was great. So, naturally, I wanted to make it better!
I made a specially targeted build of Nuclear Rush with some timing fixes to allow the game to run at 30Hz when not in stereoscopic mode, changed the headset sampling to occur on every VBlank, and enabled the above mentioned fix to keep the game synchronized with the headset at a consistent 15Hz in stereoscopic mode. To complement this build, I added an option to overclock the M68K in the emulator with my Sega VR implementation. Although 30Hz results in the actual game logic running twice as often as it was originally intended to run, it still feels pretty good! It’s interesting to think that we might be able to take advantage of the fact that we’re no longer constrained by 2MB of ROM in order to statically scale more sprite data, and paired with a few well-targeted optimizations, we might not be that far off from getting the game to run this well on normal hardware.
With the game ready to run in a slightly more demanding VR environment (where a lack of eye synchronization might increase one’s tendency toward loss of lunch), it was time to shift the focus back toward recreating the original Sega VR experience. The next step would be to add modern VR headset support to the emulator.
The main goal here was to take the stereoscopic output from the emulator and feed it straight over to the real headset. That seemed like it would be simple enough, but it wasn’t an entirely common use case, so there were a couple of special considerations. I had to take care to base my orthographic projection on the “raw” projection provided by the library, in order to account for eye/lens variation. I also had to convince the library that I really didn’t need any kind of motion smoothing or extrapolation, which meant taking care to update the headset at its desired interval while keeping Genesis output locked to 60Hz.
Once I had the OpenVR timing issues straightened out, I had some more Sega VR timing issues to deal with. It doesn’t seem that the Sega VR headset made any attempt to synchronize eye displays. In order for Nuclear Rush to run correctly without any changes to the ROM, I had to model that behavior, so I added an option to the emulator called dgen_openvr_eyes_sync. If you enable this option and try to play the version of Nuclear Rush without my sync fix, you’ll find yourself going cross-eyed pretty often. This is because, as discussed earlier, there’s really nothing preventing the eyes from becoming desynchronized on the game side. In order to cope like a real Sega VR headset, I update each eye buffer just as soon as it gets scanned out. When both eyes are fed to the real headset at once, they won’t necessarily be from the same game frame, and you’ll get the occasional flicker that you’d probably get on a real headset.
If you turn the sync option on and play with the sync fixed build of the game, on the other hand, everything works fine!
Aspect ratio and perspective were another couple of significant issues to deal with. By default, I’m crunching the image in each eye to adjust for the Genesis aspect ratio, but settings are exposed so that you can disable that or change it to whatever relative aspect you’d like.
Since I don’t have exact physical specifications for the screens or lenses in the Sega VR headset, I can only guess at effective field of view, distortion, and so on. I found that when I corrected for aspect ratio and just plastered the images up on each eye, it was fairly disorienting. I could see myself eventually going crazy and implementing a bunch of different perspective filters, but as a simple first step, I added an option called dgen_openvr_imgpscale which can be used to change the size of the image in eye projection space. After that, I decided to add customizable lens geometry. Using a Noesis script, you can export from most common model formats to a custom lens format, which is then used by the emulator to render each image in eye projection space. The above image is a fisheye test, showing the source model for the lens as well.
This approach allows us to mathematically generate any lens shape we’d like without having to consider the computational cost of the model, or literally just eyeball it and rely on interpolation to fill in the gaps. In addition to the custom lenses, bilinear filtering can be enabled with the option dgen_openvr_bilinear. Although I’m not usually a fan of scaling with bilinear filtering in emulators, it can help reduce the sense of disorientation in this case.
So, here we are! We’ve got a real VR headset driving our Sega VR emulation, and a whole bunch of tools in place for fine-tuning. It may not be exact, but here it is: The Sega VR experience!
VR at 15Hz ended up being a lot more playable than I’d expected, and the lack of roll wasn’t too jarring either. I expect the experience won’t sit so well with everyone, but I’ve had a good time with it! We’re probably benefitting a lot from low latency head tracking here as well, even with the software’s limited sampling rate.
There are a lot of fun next steps that could be taken from here. I like the idea of piggybacking on the existing Sega VR protocol to add support for room-scale tracking, or creating an interface to emulate the headset on real hardware. There’s also plenty of room for best-guess attempts at approximating more aspects of the real experience, for better or for worse, with different perspective and motion filters.
Source Code and Downloads
I’ve put two repositories up on GitHub for this project, where you’ll find both source code and binaries.
Nuclear Rush is ready to be played in most emulators or on real hardware, but you’ll have to use the emulator provided for a real Sega VR experience. The emulator is also stuffed full of options that it’s managed to pick up from some of my other projects, so take a look through the configuration file if you’re feeling adventurous. You might also have to make some changes in the configuration to get your input device of choice to map to the headset angles. If you’re going for the real VR experience with a headset of your own, there are more than a few options to adjust on that side of things too. In case you find that you feel disoriented or have a hard time tracking movement with the default settings, one of the first things to try is reducing the value for int_openvr_imgpscale until you feel more comfortable.
If you spot any mistakes, either in the code or in the article here, feel free to contact me. This project had a lot of moving parts, so I might have overlooked a few things.
Also of note, in the Nuclear Rush repository, you’ll find plenty of art and other resources which aren’t actually used in the game, including some interesting HUD designs. Have fun exploring!
You wouldn’t be reading this right now if Dylan Mansfield hadn’t taken that first step in contacting a Sega VR developer. Indeed, Nuclear Rush and the additional insight it’s given us into Sega VR may have been lost entirely if Kenneth Hurley hadn’t thought to store backups of his early work, or if he hadn’t been so generous in granting us access to the Nuclear Rush source code. We owe a great deal of thanks to Dylan Mansfield (and by extension, Gaming Alexandria) and Kenneth Hurley for their roles in helping us to preserve this important piece of video game history.
I also want to personally thank Kevin McGrath for working through my huge walls of technical questions and providing some very thoughtful replies. Thanks, Kevin! Special thanks should also be given to Sega Retro. Nearly every time I went looking for a good link to provide further reading on a topic broached by this article, there was already a Sega Retro article to fit the bill. They’re doing great work over there.
Nuclear Rush is another excellent example of the importance of source code (and data) preservation. The game’s repository as a whole tells an even deeper story about the game’s development under some rather unique circumstances. The source code, through comments and structure which would be lost in compiled binary form, provides even greater insight into both the game and the Sega VR hardware. Nuclear Rush has given us the unique ability to recreate key parts of a hardware-based experience, over one quarter of a century after the hardware was last seen in public. That’s pretty amazing!
As we enjoy all of the new material and knowledge stemming from this project, we should remember that all of it may have been lost if human circumstance had been just a little bit different, or if Kenneth Hurley’s CD-ROM had become a corroded chunk of unreadable sectors over the course of 26 years. Safely preserving source code like this requires a lot of care and dedication, and right now, it’s still way too easy for important pieces of video game history to fall through the cracks. But for every Nuclear Rush that we do manage to preserve, the future will thank us!