2

I recently wrote a little curses game and as all it needs to work is some timer mechanism and a curses implementation, the idea to try building it for DOS comes kind of naturally. Curses is provided by pdcurses for DOS.

Timing is already different between POSIX and Win32, so I have defined this interface:

#ifndef CSNAKE_TICKER_H
#define CSNAKE_TICKER_H

void ticker_init(void);
void ticker_done(void);

void ticker_start(int msec);
void ticker_stop(void);
void ticker_wait(void);

#endif

The game calls ticker_init() and ticker_done() once, ticker_start() with a millisecond interval as soon as it needs ticks and ticker_wait() in its main loop to wait for the next tick.

Using the same implementation on DOS as the one for POSIX platforms, using setitimer(), didn't work. One reason was that the C lib coming with djgpp doesn't implement waitsig(). So I created a new implementation of my interface for DOS:

#undef __STRICT_ANSI__
#include <time.h>

uclock_t tick;
uclock_t nextTick;
uclock_t tickTime;

void
ticker_init(void)
{
}

void
ticker_done(void)
{
}

void
ticker_start(int msec)
{
    tickTime = msec * UCLOCKS_PER_SEC / 1000;
    tick = uclock();
    nextTick = tick + tickTime;
}

void
ticker_stop()
{
}

void
ticker_wait(void)
{
    while ((tick = uclock()) < nextTick);
    nextTick = tick + tickTime;
}

This works like a charm in dosbox (I don't have a real DOS system right now). But my concern is: Is busy waiting really the best I can do on this platform? I'd like to have a solution allowing the CPU to at least save some energy.

For reference, here's the whole source.

  • 1
    I doubt that DOS provides this. The [Wikipedia page on the HLT instruction](https://en.wikipedia.org/wiki/HLT) says that "it was not used by MS-DOS and was not specifically designed to reduce power consumption until the release of the Intel DX4 processor in 1994", a time when DOS was being supplanted by Windows. Busy-waiting was pretty typical back then, AFAIK. – Wyzard Jul 25 '15 at 14:54
  • Have you considered an interrupt-based keyboard routine? When you hit a key in DOS an interrupt is generated, you then figure out which key has been hit and store that in an array of keys. Then when your game logic needs to process input, it simply checks the array to see if the buttons are pressed which would cause your player to move, fire, etc. You could also use a library like Allegro to make game-related tasks in DJGPP easier to do. – Bert Jul 26 '15 at 17:34
  • Most of the time, I just need this game to run at constant speed (it's a variant of snake), therefore the ticks. Input is handled through curses to keep it portable. And if I did my own input interrupt routine just for the DOS port, wouldn't the problem (that I *think* is solved, see my own answer, but comments are welcome) persist that I'd have to do *something* (like e.g. `hlt`) waiting for the interrupt? –  Jul 26 '15 at 17:42

1 Answers1

3

Ok, I think I can finally answer my own question (thanks Wyzard for the helpful comment!)

The obvious solution, as there doesn't seem any library call doing this, is putting a hlt in inline assembly. Unfortunately, this crashed my program. Looking for the reason, it is because the default dpmi server used runs the program in ring 3 ... hlt is reserved to ring 0. So to use it, you have to modify the loader stub to load a dpmi server running your program in ring 0. See later.

Browsing through the docs, I came across __dpmi_yield(). If we are running in a multitasking environment (Win 3.x or 9x ...), there will already be a dpmi server provided by the operating system, and of course, in that case we want to give up our time slice while waiting instead of trying the privileged hlt.

So, putting it all together, the source for DOS now looks like this:

#undef __STRICT_ANSI__
#include <time.h>
#include <dpmi.h>
#include <errno.h>

static uclock_t nextTick;
static uclock_t tickTime;
static int haveYield;

void
ticker_init(void)
{
    errno = 0;
    __dpmi_yield();
    haveYield = errno ? 0 : 1;
}

void
ticker_done(void)
{
}

void
ticker_start(int msec)
{
    tickTime = msec * UCLOCKS_PER_SEC / 1000;
    nextTick = uclock() + tickTime;
}

void
ticker_stop()
{
}

void
ticker_wait(void)
{
    if (haveYield)
    {
        while (uclock() < nextTick) __dpmi_yield();
    }
    else
    {
        while (uclock() < nextTick) __asm__ volatile ("hlt");
    }
    nextTick += tickTime;
}

In order for this to work on plain DOS, the loader stub in the compiled executable must be modified like this:

<path to>/stubedit bin/csnake.exe dpmi=CWSDPR0.EXE

CWSDPR0.EXE is a dpmi server running all code in ring 0.

Still to test is whether yielding will mess with the timing when running under win 3.x / 9x. Maybe the time slices are too long, will have to check that. Update: It works great in Windows 95 with this code above.

The usage of the hlt instruction breaks compatibility with dosbox 0.74 in a weird way .. the program seems to hang forever when trying to do a blocking getch() through PDcurses. This doesn't happen however on a real MS-DOS 6.22 in virtualbox. Update: This is a bug in dosbox 0.74 that is fixed in the current SVN tree.

Given those findings, I assume this is the best way to wait "nicely" in a DOS program.

Update: It's possible to do even better by checking all available methods and picking the best one. I found a DOS idle call that should be considered as well. The strategy:

  1. If yield is supported, use this (we are running in a multitasking environment)

  2. If idle is supported, use this. Optionally, if we're in ring-0, do a hlt each time before calling idle, because idle is documented to return immediately when no other program is ready to run.

  3. Otherwise, in ring-0 just use plain hlt instructions.

  4. Busy-waiting as a last resort.

Here's a little example program (DJGPP) that tests for all possibilities:

#include <stdio.h>
#include <dpmi.h>
#include <errno.h>

static unsigned int ring;

static int
haveDosidle(void)
{
    __dpmi_regs regs;
    regs.x.ax = 0x1680;
    __dpmi_int(0x28, &regs);
    return regs.h.al ? 0 : 1;
}

int main()
{
    puts("checking idle methods:");

    fputs("yield (int 0x2f 0x1680): ", stdout);
    errno = 0;
    __dpmi_yield();

    if (errno)
    {
        puts("not supported.");
    }
    else
    {
        puts("supported.");
    }

    fputs("idle (int 0x28 0x1680): ", stdout);

    if (!haveDosidle())
    {
        puts("not supported.");
    }
    else
    {
        puts("supported.");
    }

    fputs("ring-0 HLT instruction: ", stdout);
    __asm__ ("mov %%cs, %0\n\t"
             "and $3, %0" : "=r" (ring));

    if (ring)
    {
        printf("not supported. (running in ring-%u)\n", ring);
    }
    else
    {
        puts("supported. (running in ring-0)");
    }
}

The code in my github repo reflects the changes.

Community
  • 1
  • 1