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.
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
hltin inline assembly. Unfortunately, this crashed my program. Looking for the reason, it is because the defaultdpmiserver used runs the program inring 3...hltis reserved toring 0. So to use it, you have to modify the loader stub to load adpmiserver running your program inring 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
dpmiserver 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 privilegedhlt.So, putting it all together, the source for DOS now looks like this:
In order for this to work on plain DOS, the loader stub in the compiled executable must be modified like this:
CWSDPR0.EXEis adpmiserver running all code inring 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
hltinstruction breaks compatibility withdosbox 0.74in a weird way .. the program seems to hang forever when trying to do a blockinggetch()through PDcurses. This doesn't happen however on a real MS-DOS 6.22 invirtualbox. Update: This is a bug indosbox 0.74that is fixed in the currentSVNtree.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:
If yield is supported, use this (we are running in a multitasking environment)
If idle is supported, use this. Optionally, if we're in ring-0, do a
hlteach time before calling idle, because idle is documented to return immediately when no other program is ready to run.Otherwise, in ring-0 just use plain
hltinstructions.Busy-waiting as a last resort.
Here's a little example program (DJGPP) that tests for all possibilities:
The code in my github repo reflects the changes.