Porting Quake II to MS-DOS pt1

(In this series, I’m going to go over the process of porting Quake II to MS-DOS.  Now of course the question will pop up why? And the answer is simple enough, after [HCI]Mara’akate added in gamespy support, something was noticeably off, and that is fewer and fewer people are playing Quake 1 these days. So what to do?

Well apparently Quake II has an active following, so it’s time to move our creeky but favorite OS, MS-DOS into the Quake II Arena!

So, the first thing to do is to grab a copy of the GPL source to Quake 2, along with a copy of the game, and get started on making a ‘null’ version of the game.  Null versions of the game have no graphical output, no sound, and ‘function’ at a very basic level.  It’s something to strive for as a first base in hitting that home run of a functional port.  Thankfully iD wrote really portable and modular software.  Unfortunately, they tended to let their ports drift as they were writing the engine, so the null code doesn’t actually work out of the box.  And the project makfiles leave a little to be desired for me, as they have a bunch of i386/Dec Alpha magic which doesn’t leave much in the room for weird ports like MS-DOS.

Now I should mention that before I’d gotten started the first thing, I decided that like QuakeWorld for MS-DOS (and OS/2) I would use GCC as it is a known working compiler out of the box.  If you can, don’t fight so many battles of unknowns at once.  Another thing is that I am going to cross compile from OS X and test with DOSBox.  Of course you may want to use something else, and I know my tastes drift, but for now this is what I’m using. I’m using my old OSX to DJGPP cross compiler with GCC 2.95.3, which serves me well.

So, the first thing was to compile and run the null version with native tools on OS X.  After a bit of struggle, I got here:

$ ./q2
Added packfile ./baseq2/pak0.pak (3307 files)
Added packfile ./baseq2/pak1.pak (279 files)
Added packfile ./baseq2/pak2.pak (2 files)
Console initialized.
Sys_Error: ref_soft::R_BeginFrame() – catastrophic mode change failure

Well that’s great, how to figure this one out?

One thing I did to make it easier to work with the flow of Quake is to make the Sys_Error procedure contain a divide by zero.  Now why would I purposely put a divide by zero in the code?  Simple it lets me back trace the code when Quake catches its own faults so I can see what went wrong where, vs what would look like a clean exit.  From my example:

(lldb) bt
* thread #1: tid = 0xf14f9, 0x000c0853 q2`Sys_Error(error=0x000c17d4) + 98 at sys_null.c:28, queue = ‘com.apple.main-thread’, stop reason = EXC_ARITHMETIC (code=EXC_I386_DIV, subcode=0x0)
* frame #0: 0x000c0853 q2`Sys_Error(error=0x000c17d4) + 98 at sys_null.c:28
frame #1: 0x0003a0cb q2`Com_Error(code=0, fmt=0x000c17d4) + 368 at common.c:215
frame #2: 0x000c0523 q2`VID_Error(err_level=0, fmt=0x000cc8a8) + 81 at vid_null.c:45
frame #3: 0x000a6d38 q2`R_BeginFrame(camera_separation=0) + 592 at r_main.c:1145
frame #4: 0x000a4cfc q2`R_Init(hInstance=0x00000000, wndProc=0x00000000) + 276 at r_main.c:333
frame #5: 0x000c0740 q2`VID_Init + 362 at vid_null.c:122
frame #6: 0x00007900 q2`CL_Init + 55 at cl_main.c:1795
frame #7: 0x0003c2cb q2`Qcommon_Init(argc=1, argv=0xbffffc40) + 754 at common.c:1469
frame #8: 0x000c09ac q2`main(argc=1, argv=0xbffffc40) + 24 at sys_null.c:128
frame #9: 0x000019ad q2`_start + 212
frame #10: 0x000018d8 q2`start + 40

Now I know that R_BeginFrame was where the error was occurring, and looking at the code:

if ( ( err = SWimp_SetMode( &vid.width, &vid.height, sw_mode->value, vid_fullscreen->value ) ) == rserr_ok )

ri.Sys_Error( ERR_FATAL, “ref_soft::R_BeginFrame() – catastrophic mode change failure\n” );

We can see that the vid.width/vid.height aren’t being setup correctly.  Turns out there was a bunch more work to be done setting up vid_null!

After looking closer at the files, I notice as I’m stitching them together is that Quake II relies on DLL’s as part of its base functionality.  I drifted out of Quake after Quake 1, so I never played II before. Â So I didn’t know this.  Obviously DJGPP doesn’t support DLL’s that can be loaded and un-loaded at will (Yes I know about DXE’s, but as the FAQ states, they cannot be un-loaded.  And I’m not going to fight DJGPP’s LIBC). Â So looking further in the source, I saw these fun defines:


So, at one point there was support for ‘hard linking’ in the ‘REF’ video driver, and the ‘game’ logic driver. But it did kind of drift out of the code.  But looking at the Win32 version I could see that putting this functionality back in should be easy.  And to be honest if I learned any lesson from this, is that I should have been pulling the Win32 version apart by injecting null into it until it ran as a null platform, then use that as the basis.  Lesson learned.  Always start with a known good!  Quake II was built with Visual C++ 6, but I only have Visual C++ 4.2 installed on Crossover.  Yes, I know again this is me being difficult.  But it didn’t take much time to get a simple project that has two DLL’s and a Win32 exe running.  Then I took on the ‘ref’ video driver and got that linking inside of the main EXE.  Now with one DLL ‘eliminated’ it was time to work on the game DLL.

The game DLL posed the biggest challenge because it passes a reference to internal functions to it, and exports various functions back to the game engine.  So, I ended up altering the engine to not call the game import/export directly but setting it up myself.  The hardest thing was that I couldn’t pull in the game header file, but rather I had to copy the prototypes myself.  Another interesting thing with the way Quake II works is that the game DLL has to be able to be unloaded and loaded at will.  It wasn’t hard to simulate this, but I wasn’t expecting it.  Again, this is probably because I never really played Quake II.

Now that I had Quake II building without DLL’s I could then take the next step of removing all the IO and re-replacing it with the null code, and now I had something that looked like it was doing something.

Added packfile ./baseq2/pak0.pak (3307 files)
Added packfile ./baseq2/pak1.pak (279 files)
Added packfile ./baseq2/pak2.pak (2 files)
execing default.cfg
execing config.cfg
NULLsock Initialized
Console initialized.
768k surface cache
ref_soft version: SOFT 0.01
——- sound initialization ——-
Sys_FindFirst [./baseq2/autoexec.cfg]: UNIMPLEMENTED!
==== InitGame ====
——- Server Initialization ——-
0 entities inhibited
0 teams with 0 entities
====== Quake2 Initialized ======
loopback: client_connect
==== ShutdownGame ====
Sys_Error: Draw_Pic: bad coordinates

This turns out because I didn’t allocate the screen properly.  Looking at the code:

if ((x < 0) || (x + w > vid.width) ||
(y + h > vid.height))
ri.Sys_Error (ERR_FATAL,”Draw_Pic: bad coordinates”);

We can see it pretty plainly.

Now since we were going somewhere I started to write some MS-DOS code, and switch out of the null set of mind!

First a simple VGA mode 13 setup which gives us 320×200 resolution with 256 available colours.  And for good measure I did a simple VGA palette setup that I knew worked from a prior program.  Next we just blit our buffer onto the screen, and we get this:

Quake II on MS-DOS first screen

Quake II on MS-DOS first screen

Which is exciting and disappointing at the same time.  I then took the palette code from DOS Quake and got something just as ugly.  I tried the code from OS/2. Same thing.  I tried all kinds of things and was going nowhere.

At this point Mara’akate added in the Linux clock code, and now we had this!

Quake II with a bad palette

Quake II with a bad palette

It wasn’t until much more digging around I saw some 320×240 screen setups that I realized there was something wrong there, and then I saw this gem in the linux port’s code:

** SWimp_SetPalette
** System specific palette setting routine. A NULL palette means
** to use the existing palette. The palette is expected to be in
** a padded 4-byte xRGB format.

In traditional VGA palette setups it’s 768 bytes that needs to be read/and pushed to the card.  I even checked Quake 1 is 768 bytes, but now in Quake II, it’s 1024 bytes!  OOPS!  Sometimes (ok a lot of times) you really need to check other ports or a ‘known good’ to see how they did things.

Quake II running on MS-DOS!

Quake II running on MS-DOS!

Pretty awesome!

So where to go from here? Obviously things like better keyboard input, the mouse, sound and networking need to be done.

Continued in pt2, pt3, pt4, and part5.

48 thoughts on “Porting Quake II to MS-DOS pt1

    • Quake3 has no software driver for the 3d support. Instead we would have to rely on mesa3d, which would require something incredibly powerful.

      While Quake II has been found to be barely playable on a P1 160, I would imagine Q3’s requirements would be something far more beefier, and less likelier to have MS-DOS ISA sound card support.

  1. Thanks for providing the compile guides! I compiled a binary in DJGPP (without requiring the dynamic loaded libraries). I also nearly built a binary in VC6 without the dll files – mainly using Alien Arena source code as a guide for removing the export/import of functions and to reference functions internally. I also made a simple makefile by placing source in a single directory.

    I noted that the DJGPP SvgaLib is available already built, but it doesn’t contain full functionality (although there are reports of building newer and more functional versions, such as from the mplayer project files). Instead, I followed your method of directly accessing the video mode in the initialization function and it mirrors your result. Also, I linked the WATT32 library which allowed the build to progress instead of halting on missed functions, such as ‘gethostbyname’.

    Your methods are excellent of both compiling with null functions during testing and finding a working binary without the external dependencies by testing with the win32 build.

    I noted that the inline assembly is in a different format that gcc, but there are reports that these functions may be compiled outside gcc and then linked with a gcc build.

    The Winquake source has examples for use of virtual memory, but I think incorporating assembly functions will have by far the greatest impact, given your forward progress.

    • I actually ported over the virtual memory/zone.c stuff in a separate branch and it’s mostly complete. The Mod_LoadAlias and a few other cacheing routines aren’t there. Basically you can load the game, but models and sprites aren’t rendered because their frames aren’t cached.

      In any case, I didn’t notice a real improvement or stability change by doing so. For now, it’s just dormant unless I decide to finish the rest of that up.

      Quake 2 already has a very similar version using the Z_* functions and I noticed after I fixed a lot of the issues with sound bugs and tightening up the dos drivers any issues with running out of memory were gone. I was able to load the game and play it just fine with 64MB RAM allocated to dosbox. It’s hard to test on my real computers because the P1 has 384MB RAM and the P2 has 1GB RAM so running out of memory for these games isn’t an issue for me.

      • Your work is excellent! I didn’t have any issue with 64mb of RAM in dosbox either. Thank you for testing and sharing your results. 🙂

    • If you can share your source modifications that would be cool, as you used our modifications. We would like to add that sound into Quake 2, along with other projects. As you may or not be aware, QWDOS and QDOS are GPL 2.0 projects. Which means that you have to make the source available, which we are now requesting you do so.

        • It’s in our DJGPP 2.05 testing branch right now. Works great on Qemu. Thanks for providing the source, as we were going *NOWHERE* fast, but being able to see the other side really REALLY helped.

          I assume we can credit you as ‘Ruslan Starodubov’ right?

          • Hi
            I tested the work Quake2 for DOS. Good work! Few retarding in some scenes. Sound HDA works. (With a slight clicking of 44100).
            I looked in the file code snddma_dos. There’s little wrong done.
            It is necessary to do so for the function AU_search:
            char *c;
            int ln;
            ln = COM_CheckParm(“-spk”);

            AU_search(0) for LINE_OUT AU_search(1) for STEREO_SPEAKER_OUT
            This is only for Intel HDA chips embedded in the notebook’s internal speakers.
            Cleaning the buffer is not required, as is done in the function AU_start()
            Option -HDA I think the excess (the user may not be aware of it). Just go through all PCI devices and then ISA device.
            I realized that there were minor problems for syntax DJGPP 2.05. (version GCC> 3). I used DJGPP 2.03. It makes a short code.

    • Me too! Surprisingly, it really wasn’t too hard. Quake 2 is very portable on it’s own. And it’s a superset of the Quake 1 driver infrastructure so it was easy to upgrade on that.

      Play with nullquake, and you can see just what functions you need to make it do anything. At a minimum if you can increment an integer, and blit a buffer to a screen, you too are 1/2 the way to quake! Add in reading a keyboard, and outputing audio and you are home free!

      Check out the GIT repository, and you can see how the code evolved!

  2. oops sorry i post the same in the ms-dos question but i cant delete the other comment on that…

    when i try to build q2dos with clean or makealldxe than i got this:

    could not find *.d
    could not find *.o
    could not find *.obj

    ….and all the “del”‘s of the clean.bat

    and when i try to build makesse than it says:

    “Q2DOSDEVBASE not defined!”

    but i wrote in every batfile the q2dos location like this:

    SET SEVENZIPPATH=C:\Program Files\7-Zip

    but nothing works.

      • i just download the https://bitbucket.org/neozeed/q2dos revision 2017-12-31.

        i use windows 10.

        i dont know. if it not working for me than can you prepare new latest base and sse build for download? i see that the latest revision of your build is 2016-11-25 but the sourcecode is much newer and you make many significant changes since then (or not?) and i want the new build 😛

        at least the 2016-11-25 revision works very well on my ms-dos 7.1 machine with 85 fps 🙂

  3. I tried playing Q2DOS in DOSBox. The base game was a pain to setup at first, but it works great now. But I’m having trouble with the mission packs, since certain objects won’t spawn or function properly. I ended up having to noclip through parts of Units 2 and 3 of The Reckoning due to the tank bosses and green keycards not spawning.

    • Which exe are you using?

      Not that I’d be able to help much I’m terrible at q2, and we kinda relied on the world to qa it.

      I used qemu to test builds after the basic stuff as dosbox was just too slow. Although speaking of dosbox, the latest SVN is way faster than the release code. It only needs 2 fixes for Visual C++ 2003 as well..

Leave a Reply to Ruslan Cancel reply